Island Life

< Y Combinatorの7年 | ハイポハイポハイポのシューリンガン >

2012/03/17

直交世界の地図

ライブラリや言語要素は、なるべく独立した、互いに直交な機能を提供すべきだ、 という原理を私は信奉している。(1)学習すべき言語機能やAPIの数を抑えつつ、 (2)それらをシンプルに組み合わせることで「できること」を最大化する、 その最適解だからだ。

(単に(1)を最小化したいならS,Kコンビネータでもチューリングマシンでも いいんだけど、それだと(2)で組み合わせる方法が複雑になる。 必要な機能そのものを実現するAPIをその都度作っていると(2)の組み合わせは 最小化されるけれど(1)が爆発する。)

なんだけど、この直交する「軸」が増えてくると、 ほんの2要素の組み合わせであっても、発見が難しくなりすぎるかもしれない。

最近、Gaucheでコンマ区切りファイルを読んで処理するにはどうする、 という話題を見かけた。Gaucheリファレンスで"CSV"を検索したらすぐに text.csvモジュールは見つかる (refj:text.csv)。でも そこにある読み込みAPIはひとつ、 「CVSファイルの1レコードを読み込む手続きを作る」関数 make-csv-reader だけしかない。

これが例えばRubyのCSVクラスをだと、色々なAPIが提供されていて、 このページだけ見ればすぐに使えそうだ。

http://doc.ruby-lang.org/ja/1.9.3/class/CSV.html

# ファイルから一行ずつ
CSV.foreach("path/to/file.csv") do |row|
  # use row here...
end

# ファイルから一度に
arr_of_arrs = CSV.read("path/to/file.csv")

# 文字列から一行ずつ
CSV.parse("CSV,data,String") do |row|
  # use row here...
end

# 文字列から一度に
arr_of_arrs = CSV.parse("CSV,data,String")

Gaucheでは、make-csv-readerの戻り値を 他の手続きと組み合わせて使うことが想定されている。 (do-ecを使う場合は(use srfi-42)しておく)。

;; ファイルから一行ずつ
(call-with-input-file "path/to/file.csv"
  (^p (port-for-each (^[row] #|use row here|#)
                     (cute (make-csv-reader #\,) p))))

;; あるいは
(call-with-input-file "path/to/file.csv"
  (^p (do-ec (: row p (make-csv-reader #\,))
             #|use row here|#)))

;; ファイルから一度に
(use util.file)
(file->list (make-csv-reader #\,) "path/to/file.csv")

;; 文字列から一行ずつ
(call-with-input-string "CSV,data,String"
  (^p (port-for-each (^[row] #|use row here|#)
                     (cute (make-csv-reader #\,) p))))

;; あるいは
(call-with-input-string "CSV,data,String"
  (^p (do-ec (: row p (make-csv-reader #\,))
             #|use row here|#)))

;; 文字列から一度に
(call-with-input-string "CSV,data,String"
  (cut port->list (make-csv-reader #\,) <>))

まあ、RubyのAPIよりやや冗長であることは否定しないけど、 「ポートから1レコード読み込む」という操作さえ提供しておけば、 後の組み合わせは共通だ。 「ファイルから読むAPI」「文字列から読むAPI」… などを個別に提供した場合、 今後パーズすべきフォーマットが増えていった場合に、 各フォーマットモジュール毎に統一した形でそれらを定義していかないとならない。 とはいえ、良く使う操作なら簡潔に書けるようにしておくっていうのも重要なんで、 バランスの問題ではある。

ただ、今回難しいなと思ったのは、 初心者が「CSVファイルを処理したいな」と思って検索してtext.csvのページに たどりついても、さてそこからどうするか、次の一手が見えないだろうな、ってことだ。 各モジュールの独立性を高くしておいたことが、使い方を探す立場からは障害になる。

リファレンスマニュアルに具体例をたくさん書いておくというのは当面の解になるだろうけれど、 直交性の軸が増えると組み合わせの数が爆発するわけで、全ての可能性について 例をあげるという方針はスケールしない。

静的型言語だと、「この型の関数をはめ込めるパターンはどれ?」っていう具合に 検索をかけることができる。何らかのメタなアノテーションをAPIにつけられるように しといて、それを手がかりに探索できるようにするって手はあるかもしれない。

もう一つの問題は、要素群 X = {a, b, c,...} と要素群 Y = {j, k, l,...} から 要素を取ってきて組み合わせる、という使い方がポピュラーな場合に、 使う側は、たとえばbとjを組み合わせるなら(use b)(use j)を 書かないとならないこと。具体例としては、ダイジェストのハッシュを計算して ハッシュ値の16進文字列を得たい、というとき、ハッシュアルゴリズム (e.g. (use rfc.md5) ) とダイジェスト汎用のユーティリティ (use util.digest) が必要になる。 こういう組み合わせが増えてくと、アプリケーションの冒頭にuseがずらずらと 並ぶことになって、これがどうも気になっている。ダイジェストアルゴリズムのモジュール それぞれに16進文字列化のAPIをつければuseが減るんだけれど、 モジュール間の直交性がそのぶん減ることになる。なんかうまい方法はないかのう。

Tags: Programming, Gauche

Past comment(s)

narumij (2012/03/18 09:53:50):

Convinientな層があればいいんでは?と浮かんだのですが、そんな安直なことではないのかな・・・。

narumij (2012/03/18 10:08:54):

度々すみません。 身近な小さな仕事をこなすのに使おうとして、久しぶりすぎてすっかり忘れてる状況でも少ない学習コストで目的を達成できるとしたら、非常にありがたいです。時々コツコツ使えることで積もるものって大きいですし。

shiro (2012/03/18 11:46:34):

個々のケースを見ればConvenientなAPIを作れば解決するでしょうが、個別にそういうことをやっているとAPIの数が爆発して、しかもそれらの一貫性を保つのが難しい、ってことになりませんか。

例えば、RubyのCSVのように、あるフォーマットのレコードをパーズするモジュールを書いた時は、ファイルからの読み込み(レコード別、一括)と文字列からの読み込み(レコード別、一括)をこういうAPIで提供しときましょう、としたとします。後で言語(処理系)にlazy sequenceが足されて、(1)文字のlazy sequenceから読み込みたい、(2)ファイル、文字列、その他のソースをパーズした結果を、レコードのlazy sequenceとして得たい、という要求が上がったとします。すると、これらの新たなAPIを既存の全てのフォーマットパーザのモジュールに足さないとなりません。一気に変更出来ない場合、モジュールによってAPIが使えたり使えなかったり、という非一貫性が生じます。

考えたいのは、この例のように要素群X (いろんなファイルフォーマットのパージング) と要素群Y (入力形式や返す形式) の組み合わせがあって、例えばYに新たな要素が加わった時に、Xの方を何も変えないでも素直に使えて、そのことを初心者にもわかりやすくナビゲートするにはどうすればいいか、ってことですね。

narumij (2012/03/18 15:06:32):

よく見かける問題にだけ対処することを目的にそれ以外を切り捨てたものが必要なんだろうなと文章だけ読んで感じて、上の書き込みをしました。意図がくみ取れていなかったようですみません。

Rui (2012/03/18 17:44:38):

useは、Javaもずらずらimport文が並ぶけどfully qualified nameでかけばimportは書かなくて済みますね。それに似たような何かとか。あるいは先頭に書くから目立つのであってファイル末尾に書いてみるとか。。

Kei (2012/03/18 20:51:32):

Javaだと結構IDEのサポートに頼っている部分が大きいかもしれません。import文は勝手に折りたたんでくれるとか。JavaDocはAPIの説明だけなので高度に抽象化されていると同じ問題が起きますが。上記の例ならCSVクラスはReaderを一つ受け取ってString[](もしくはCollection<String>)を返すだけとか。 でもJavaでそれがあまり問題にならないのは、圧倒的なユーザー数でもってWeb上にちりばめられた情報量かなぁと思います。Gauche(Scheme)はJavaに比べればユーザー数も情報も少ないので初心者には多少敷居が高いのかもしれません。

aka (2012/03/19 01:43:26):

なんらかのコーパスを運用するのはどうでしょう?

shiro (2012/03/19 03:04:32):

Javaのimport文は名前空間の操作だけで依存関係は表現しませんね。 Common Lispでもuse-packageは名前空間の操作で、依存関係はビルド時に解決ってことになってます。 Gaucheでは、importは名前空間の操作、requireが依存関係、useが両者を合わせたものです。

スクリプト言語では実行時に依存関係を見つけられないとならないので、別の情報 (CLにおけるasdファイルみたいな) が必要な方法は向かないです。fully qualified nameみたいなものを導入して、モジュール名とファイル名をuseと同じ方法で決め打ちして探しにゆくことにすれば、「useずらずら問題」は低減されるかな (ただ、モジュールを読み込むタイミングが、コンパイルの途中で読むことになるので、何やら面倒な問題が発生する危惧もちょいと)。

前から module#symbol という構文で特定のモジュール内のシンボルを参照できるようにしようかなあというアイディアはあるんですが、モジュールベースで可視性をコントロールしているメカニズムに穴を開けることになるので (例えばwith-moduleなどを上書きしてsandboxを実現する場合など)、まだ入れてません。read時にmodule#symbolを(with-module module symbol)に変換して、さらにwith-moduleに「moduleが未定義だったらuseを試みる」みたいな機能をつければ実現は可能です。

エディタのサポートを当てにするなら、間数名書いてコマンドを入れると必要なuseを自動挿入してくれる、くらいはできそうですね。

ryoakg (2012/04/23 07:39:54):

> 静的型言語だと、 一部しか解決しませんが define-method を調べれば引数に関してだけは、情報がえられるかもしれませんね。 でも、高階関数だとダメそうですね。 なんとなく費用対効果は低い気がします。

> もう一つの問題は、 問題を理解できているか分かりませんが (define-module md5 (make-digest-operators rfc.md5)) などと書くと、util.digest と rfc.md5 が組合さった関数群が出来る良いなぁという話になりますか? (よく知りませんが OCaml の functor と同じ感じだと思います)

でも、こう書けた場合と、現状のプログラムの文字数を比較すると、結局importなどが2回必要だと思うので、現状の方が短いかもしれません。 ただ、使い方とか意味が制限されているという面で、気持ち悪さは低い気がします。

やるには、<module> はそのまま使えない気がするので、関数群の型を定義する方法が必要そうですね。 意味にこだわらなけらば、<class>と<generic>でも出来るのかもしれませんが。

shiro (2012/04/23 07:56:18):

> util.digest と rfc.md5 が組合さった関数群

それは今でもextendを使えばできます。でも、そうすると結局X={a,b,c...}とY={j,k,l...}の直積のぶんだけモジュールを用意しないとならなくなりますよね。(digestに関してはある程度使い方が固定されてるので何とかなるかもしれませんが)

ryoakg (2012/04/23 08:39:15):

ちょっと独善的に書きすぎていたかもしれません。

> そうすると結局X={a,b,c...}とY={j,k,l...}

僕の頭の中では、「Xの個数 + Yの個数」だけ作る、という事になるつもりでした。

>> util.digest と rfc.md5 が組合さった関数群 の表現も微妙ですね。

「util.digestの関数群 と rfc.md5の関数群 が組合さった関数群」が出来る予定でした。 ただ僕が最初に書いたやつだと、新たに出来た関数群のそれぞれの関数がどういう名前になるか? という事については触れていません。名前の話は色んな意味で面倒だと思うので、避けたのですが、分かりにくくしてしまったかもしれません。

僕の理解だと OCaml の functor は、例えば C++ の template みたいに機能が組合さった関数を作る事ができると思いますが、extend は名前空間を合成して新しい名前空間を作るものだと思うので、大分違うものだと思っています。間違っていたらごめんなさい。

shiro (2012/04/23 08:59:28):

いや、元の話題が抽象的/曖昧な定義の話なので、話題が発散しちゃうのは仕方ないですね。でも色々発散するこういう話もとても有益です。

「組み合わさった」というのがその意味でのパラメタライゼーションなら、digestに関してはジェネリック関数で既に実現できてないかな? つまり、rfc.md5が<md5>というアルゴリズムを提供し、util.digestはアルゴリズムがパラメタライズされたdigestやdigest-stringという操作を提供する。ユーザはdigestを<md5>という具体的なアルゴリズムでインスタンス化して使う。

で、私のここでの問題意識は、

  • ユーザは組み合わせるために、util.digestとrfc.md5の両方をuseしないとならない。
  • そもそも「『rfc.md5』と『util.digest』を組み合わせられる」という情報をどうやってわかりやすく提示すべきか (digestに関してはリファレンスに双方向にポインタを張ってありますが、一般的に組み合わせが増えていった場合になんかうまい方法はないか)

ってあたりなんですが、一つ目に関しては確かに、「util.digestとrfc.md5は合わせて使うためにuseしてるんだよ」というのが明示されてると、気分的にわかりやすいということはありますね。

ryoakg (2012/04/23 14:58:17):

考えていたら頭がゴチャゴチャしてきたので、これまで自分が書いた事は軽く無視して下さい(つまりこれまでと一貫性とない事を書いています)。

こういうのはどうでしょう? という話を書いてみます。 ざっくり表現すると、use を特殊化して引数が取れる様にするという話です。

  1. 何と呼ぶかは知りませんが use の引数には util.digest, rfc.md5 などを指定すると思います、これと型情報を対にして require や load する前から分かる様にしておく。本文の例だと rfc.md5 側。この情報の名前を A とする。
  2. 特殊化した use に A を引数として渡す(型情報を見て失敗する可能性もある)。本文の例だと util.digest 側。この特殊化した use は、最初から autoload されている様な形で、import, require, use など書かなくていい。特殊化した use の引数は、複数でもいいし、A でなくてもいい。
  3. その結果、本文の例だと、関数 digest, digest-string の引数 class を指定しないで使える関数が使える様になったり、Ruby 程度書けば使えるAPIが使える様になったり。

これだと、高いレベルの情報は増えても、特殊化した use の名前をいちいち覚える事になって良くないですかね?

引数が省略された関数が出来たり、Ruby程度書けば使える様になる、というのは特殊化した use を使うところで、モジュール(?)同士の関係を指定すれば、組合せ方を考えなくて済むんじゃないか? 減るんじゃないか? と思ったからです。

でも、勝手にこういう機能を作られると、「ココだけ下層に行きたい」という場合に手詰りになってあまり好ましくないでしょうか?

むしろ本文にあるcsvのコードの様なものを、なるべく誰でもパッと書ける様にする事を目的おくべきだったでしょうか?

shiro (2012/04/23 16:07:29):

何となくのイメージはわかる気がするんですが、多分Gaucheの実装の観点からだといくつかイシューが混ざってる感じがします。例えば、今、「md5でファイルをdigestして16進数の文字列のハッシュ値を得たい」って場合はこんな感じになるんですが:

  (use util.digest)
  (use rfc.md5)

  (with-input-from-file file
    (cut digest-hexify (digest <md5>)))

「特殊化したuse」を使うとどうなるんでしょう。

上のコメントからは、モジュールをパラメタライズすることかな、という印象を受けたので、例えばこんなことかなと思ったんですが、合ってますか?

  (use (util.digest rfc.md5)) ;; util.digestをrfc.md5で特殊化

  (with-input-from-file file
    (cut digest-hexify (digest))) ;;するとこちらで<md5>を渡さなくていい

ryoakg (2012/04/23 22:00:20):

はい、書かれている例については、そういうものを想定しました。というか、分かっているなら僕が例を書けば良かったですね。

(use (util.digest rfc.md5))

については、util.digest が作る関係が1種類かどうか分からなかったので

 (XXXX-util.digest-XXXX rfc.md5)

の様に考えました

 ((XXXX util.digest) rfc.md5)
 (XXXX util.digest rfc.md5)

などの方がいいかもしれません

あと、<md5> を渡さなくて良くなった関数 digest の名前が digest のままでいいか? という点については、何も考えていません。

多分Gaucheの実装の観点からだといくつかイシューが混ざってる感じがします。

僕は実装については、ほぼ何も考えていません。そもそもほとんど知らないので。こうすれば色々一編に解決するかもしれないなぁという、都合の良い事を考えて書きました。

shiro (2012/04/24 01:05:46):

ok、わかりました。

  • digestの名前を変える方法は:prefixとか:renameとか既にモジュールにあるメカニズムで何とでもなるので考察から除外して良いでしょう。
  • rfc.md5とutil.digestとを組み合わせるに当たって、現在ではrfc.md5を書く人がutil.digestの存在を前提にする必要があります。(util.digestが定義するgeneric functionに対して、rfc.md5がメソッドを追加する)。これを、型情報をもとに後付けで組み合わせられるようにするというメカニズムは、ライブラリ作者にとって役に立つ機能だと思います。今でもメソッドはいくらでも後付けできるので、利用者が両者を明示的にブリッジしてやることはできますが、何らかの方法で自動化できるとおもしろいですね。
  • ただ、ユーザにとっては、構文はどうあれ (1) util.digestrfc.md5 を一緒に使う、ということを発見しないとならない (2) 両方をプログラム内に明示しないとならない、ということは変わらないですね。これがここのエントリで問題にしていることです (とはいえ、絶対解決すべき問題、というのではなく、なんかうまい方法があったらいいなあ、程度の話ですが)

Post a comment

Name: