Hatena::ブログ(Diary)

Oh, you `re no (fun _ → more) このページをアンテナに追加 RSSフィード

Ah well, a dromedary has one hump and a caml has reference cells, buffers, and a garbage collector.

2014-06-17

4.02 予習その2 新しい module type、 (module M)

OCaml 4.02 に追加された新しいモジュール型、 (module M) が結構いけているのです。

OCaml である既存のモジュールの型は?というと、 module type of M というのがあって、 module type of Unix とやると Unix モジュールの signature を貰えるというものでした、が、これがあまり使いたいときに使えないものでした。

OCaml で既存モジュールに何か関数を足したいことがあります。特に元から付いている標準のライブラリは足りないものが多すぎる。たとえば Unix.usleep が欲しい:

module Unix = struct
  include Unix
  let usleep x = ignore (select [] [] [] x)
end

はい、簡単にできました。ではこのオレオレ Unix の型を書きたい。これは元の Unix モジュールのシグナチャに usleep を足したものですが、どう書いたらいいだろうか:

module Unix : sig
  (* 何を書いたらいいだろう? *)
  val usleep : float -> unit
  (** [usleep n] sleeps for [n] seconds! Cool! *)
end

unix.mli の内容をコピペするのはあまりに悲しすぎますね、結論から言うと、 include (module Unix) で嬉しい!なのです。

が、 4.02 以前にはこれはなかったんだよね。 3.12.0 で module type of M が入ったとき、おっ、これは module type of Unix と書けばよいのではないか、そう思いました:

module Unix : sig
  include module type of Unix
  val sleep : float -> unit
  (** [usleep n] sleeps for [n] seconds! Cool! *)
end

いやーこれですっきり解決!と思ったら駄目だったんです。なぜかというと元の Unix にある Unix.file_descr とオレオレ Unix にある Unix.file_descr が違う型という扱いになっている。なので、たとえばオレオレ Unix で Unix.open した file_descr を元 Unix.read で使う、とかその逆ができません。これは、オレオレライブラリだけで住んでいれば問題ないのですが、いろんな Unix を使うライブラリと組み合わせることができなくなり、つらい。これを解決するには、この二つの file_descr が同じであることを明示してやる必要があります:

module Unix : sig
  include module type of Unix with type file_descr = Unix.file_descr
  val sleep : float -> unit
  (** [usleep n] sleeps for [n] seconds! Cool! *)
end

ああ、なるほど、これで一件落着、かと思いきや、 Unix にあるデータ型は file_descr だけじゃない。 error もあるし process_status もある。これみんな with type t = Unix.t て書かなきゃいけないのです。

クソですね。

なので、オレオレで既存モジュールを拡張したいときはこうしなければいけなかった:

module XUnix : sig
  val sleep : float -> unit
  (** [usleep n] sleeps for [n] seconds! Cool! *)
end = struct
  let usleep x = ignore (select [] [] [] x)
end

module Unix = struct
  include Unix
  include XUnix
end

つまり、拡張部分だけのモジュール XUnix を書いてそこで拡張部分だけに関するシグナチャを書き、元の Unix と拡張 XUnix の統合はシグナチャ書かないでやると。この部分の sig や mli は提供しないが空気を読めよと、そういうわけだ。

で、なんかさっきから頭に引っかかってたんだけど、同じことを三年前に書いてるわけ:

ヤンナルネ…

まあ、これが問題だったんだけど、 (module M) というので解決、というか、なんでまずこれを入れなかったんだ…まあこうやって標準ライブラリをガンガン拡張する立場にあるインダストリィですか、エンタープライズですか、そういう経験のある人がまだ OCaml の内部にやいやい口を出す時代ではなかったのですね…

で、 (module M) というのはモジュール M のシグナチャ。型は元と全部同じ!というナイスなモジュール型なので、

module Unix : sig
  include (module Unix)
  val sleep : float -> unit
  (** [usleep n] sleeps for [n] seconds! Cool! *)
end = struct
  include Unix
  let usleep x = ignore (select [] [] [] x)
end

と書けるようになりました。よかった!

2014-06-11

大事なことは全部MLが教えてくれた 〜 Apple の Swift の mutability 周りの件を理解する

開発者アカウントに金が出せない貧乏人の方々が、次の AppleSwift のコードの挙動がわからない、というので盛り上がっております:

let a = [1,2]          // a = [1,2]
var b = a;             // b = [1,2]
b[1] = 3;              // a = [1,3]  b = [1,3]
b.append(5);           // a = [1,3]  b = [1,3,5]
b[1] = 4;              // a = [1,3]  b = [1,4,5]

もちろんわたしも貧乏ですからわかりやすい炎上案件を待っておるわけです。これはわかりやすいわからないが来たね。

だいたい

  • b[1] = 3 とやると a[1] も変化する、これがわからないという人
  • b[1] = 4 とやると a[1] が変化しない、これがわからないという人

二種類いるようです。私はまず、 b[1] に代入できることがわかりませんでしたのでそれ以前の人間です。

こういうとき、このコードが何をやっているかを理解するには同じ挙動をする ML のコードを書くとわかりやすいです。なぜ ML かというと、もちろんみんなの好きな関数型言語であって、かつ代入などの副作用を持っていて、でも基本は不変データなので可変データの部分はちゃんと明示してやらなきゃいけない言語なので、こういう副作用でわけわからんコードを読み解くには明示する分都合がよろしいからです。もちろんみんなが好きな関数型言語というのはここではとくに意味がありません。Haskell は本題に行く前に Haskell は純粋で純粋なのに副作用があったりなかったりうだうだモナドがああだこうだで圏論があしたどしたでドヤってめんどくさ過ぎるのでこういうのには向きません。

ええっと、断っておきますが、これは上の Apple の Swift コードの挙動と同じ挙動を持つ一番単純な ML のコードって何かしらん、という試みです。オッカムの剃刀の原理を信じれば、そのコードと同じことがおそらく Apple の Swift の中で起こっておるであろう、ということです。実際の Apple の Swift の中がどうなっているかは私貧乏ですから知りません。ただ、まあ ML のコードを見れば、まあ多かれ少なかれ、こんな内部実装なんであろうなあ、と十分推測できるわけです。

そんな手探りせずに仕様書読めという突っ込みもありですがね。まあこういう群貧撫AppleのSwiftみたいな話もたまにはいいんじゃないですか、仕様書探すの難しいらしいし (http://cpplover.blogspot.jp/2014/06/appleswift.html)、どんなプログラミング言語であれ、仕様書を読まないとわからない言語仕様というのはできるだけ少ないに越したことはありません。

で、書くとこんなんになります。 http://try.ocamlpro.com/ で一行づつ入れながら、途中で a;; とか b;; して変化を見ながら試してみるといいよ:

let a = [| 1; 2 |];;
let b = ref a;;                (* 明示的に参照ですって言わないといけないんです *)
!b.(1) <- 3;;                  (* 参照の中身を見るには !b って書かないといけないんです *)
b := Array.append !b [|5|];;   (* 参照先を変えたいときは b := ほげほげ って書くんです *)
!b.(1) <- 4;;                  (* 配列のn番目要素は a.(n) て書くんですキモイ *)
  • b[1] = 3 とやると a[1] が変化するのがわからない人は、 var b = a がコピーは行わないってことをわからない人です
  • b[1] = 4 とやると a[1] が変化しないのがわからない人は、b.append(5) が b 自体を変えてしまうことがわからない人です、ってかこれ普通わかんないでしょ

でもこれ両方とも ML で書いたコードではハッキリわかりますよね。少なくともこの例を見る限りは、

  • var b = e は e の値への参照を作るだけでコピーはしない
  • b.append(5) は参照する値に 5 をくっつける。配列だから結果はコピーになる。 b はそこに参照先を変える

という意味だとわかる。(Array.append はコピーするんですよ。というか配列の後ろに何かつけてもコピーじゃないってそんなすごい配列があったらそれは配列じゃない) copy-on-write とかカッコイイ機能を入れようとしたけどバグっているんだ!とかそういうオシャレ事案ではない。

クソですね。

オブジェクトメソッド呼び出しのような外見なんだけど、呼ばれた後、オブジェクト自身は他のに摩り替わっている。高度なナリスマシ事案だ。これはヒドイ仕様だと思います。 ML なら b := Array.append !b [|5|] と書くわけなので、 b の参照先が変わっていることがわかる。append すると配列がコピーされるかどうかはもちろん配列というデータ構造の特性を知らないといけないのであれだけど。

せっかくだから Apple の Swift の人の立場になって考えましょう。これはどう直せばいいですかね。

  • var b = e は e の値のコピーを作ってそれを参照することにする

これはパフォーマンス落ちる…のなら、気休めに copy-on-write 導入しますか。 b[1] = 3 とやっても a[1] が変わらない… まあよく判んない人相手にはこの挙動が一番いいのかもしれませんね。

  • 配列を頭よくして b.append(5) とやっても b の中身は成りすましにならず、ちゃんと b[1] = 4 の後 a[1] = 4 になる。

これは配列はもはや配列ではなくて何か別の rope みたいなものになります。うーんそれは配列ではない。

  • b.append(5) などというクソいメソッドはやめて b = b ++ [5] みたいにする。

これならコピーしていることがわかる?かな?いやー無理でしょうねぇ。でも少なくとも現時点での挙動でメソッド呼び出しの形は誤解をまねきやすすぎるよね。

この中でどれが一番いいか…やはり ML を使うことだと思いますね。おわり。

2014-05-27

純粋関数型のコンセプトを「関数型言語ではー」とか広げて言わんといて!

そもそも「関数型言語」という言葉自体、どうなんやいうのもあるんですけど http://www.slideshare.net/ksknac/120901fp-key

とある純粋関数型言語の特徴でもって「関数型言語ではー」とか言われると、非純粋関数型言語のユーザーとしては(いやお仕事では純粋関数型言語使ってますけどね)もにょっとしてしまうんですよ

  • 関数型言語は遅延評価だから、すばらしい/クソだ (いやいや純粋関数型言語でも先行評価の言語あるからー、あんたのは違うかもしれんけどよー
  • 関数型言語は副作用が状態がないので、すばらしい/クソだ (いやいやまず純粋関数型の人たちでまず副作用が何指してるか決めてから来てよー
  • 関数型言語はインデントで意味が、型クラスが、、、すばらしい/クソだ

Haskell の話なら Haskell ではーとか、やっぱり GHC だね!とか書いてよ!「アジアではー、ニンジャがゲーシャとハラキリでテンプラ!」みたいなの見たら突っ込まざるを得ないじゃん!純粋アーリア人(問題のある発言)がドイツ語しゃべったら、ワタシ、インドから来たナンディさんネー、はドイツ語しゃべれるのか?ちゃうやろ?

「OOPでは多重継承はさまざまな問題が発生するのでジャバーのようにインターフェース以外では許されないほうがいいですよー」とかなら是非はともかくええのよ。

「OOPでは多重継承はさまざまな問題が発生するのでインターフェース以外では許されていないんですよー」とか言わないっしょ?

純粋関数型言語だけが関数型言語じゃ、ないんで、、、他のやれとは言わんけど、、、

2014-04-25

ソウルフル小話: 型付き関数型言語と C++ は同時に使ってはいけない

いやまじで使っちゃいけないです。

三年前こういうことがあったんです。 Haskell と C++ を使っているコードなんですが、

  • 突然ビルドができなくなる。さっきまで上手く行っていたのに、意味不明な C++ のエラーがゴボゴボ出てくる。 git status や hg status してもそんなとこ何も触ってない。そもそも C++ なんか触ってねえし、俺触ってたの Haskell だし!
  • 変更を全部元に戻してビルド ⇒ やはり意味不明な C++ のエラー。エラー行数が多すぎて読む気も起きない。俺は C++ 専従じゃねえからこんなもん読めるか

ビルドできたはずのコードに戻して make clean してもビルドエラーが出るわけ。まさに「何もいじってないのに壊れましたぁ」状態。ほら、さすがにプロだから、この台詞は絶対言いたくないんだけど、この台詞でしか説明できない。そのビルドディレクトリをあきらめて別のディレクトリに git clone / hg clone しなおすと…ビルドできる。わけわかんない

で、昨日ね、まさにそういう同僚がいて私に助けを求めましたですよ。「ちょっとわかんねーなー、これは三年位前俺もひっかかって…あきらめた」とか話しながら見ていたんですが、ビルドルートディレクトリで ls すると、なんか変なファイルがあるわけ

$ ls
[String] .... Map Maybe ... String

あれーこれは Haskell の型の名前ですやん。なんでこんなものが…

そこであなたも経験ある型付きの関数型言語プログラマなら必ずやったことがあるはずだと思いますが、型名をコピペしますよな?んで間違って shell にペーストして実行しちゃったことありますよな?

$ Int -> [String]
bash: Int: command not found

したら [String] とかいう空ファイルができてしまいますやん。あーそういうファイルだなやーと思ったし、当然中身空だし、消したの。

したらビルドの問題が綺麗さっぱりなくなった。なんでーーー!

C++ の STL のヘッダファイルって拡張子なくって string とか list とかなんですねーひどいね!ビルドルートディレクトリを基点に C++ コードをコンパイルしようとすると、どっかで STL の include があって、

#include <string>
#include <list>

とか書いてある。書いてあったらどうなる?ビルドルートに string とか list とかのファイルがあったら読みに行っちゃうんだね!そんなこたぁ起こらない?いや上で見たとおり型付き関数型言語使ってると起こす人いるんだねぇ。んでそれって空のファイルだから、そこではエラーでない訳よ。(変なごみの入ったファイルだったらビルド時のエラーメッセージですぐわかるんだけどね。)後のほーで、多分 STL まわりの意味不明なエラーがゴンゴン出る。え? String だから #include <string> は読み込まないはず?おめぇ、大文字小文字の違い無視する OS で働いたことねえのな。

結論:型付きの関数型言語と C++ を組み合わせて開発をしてはいけない。

ってか STL 作った人って頭悪い。おわり。

ちょっと考えた

ビルドルートがなんでインクルードパスに入ってんねんと思ったが、 ROOT/A/B/B.h とかが山ほどあるので、 #include <A/B/B.h> とかのために入ってないと困る。必須。

やっぱり拡張子無しのヘッダファイルなんか作るやつが悪いってことで終わり。

2013-12-31

OutsideIn(X) と OCaml

Haskell の実装 GHC の新しめのバージョンでは 多相let の型付けが今までの HM (Hindley Milner) 方式から新しい OutsideIn(X) に変わっています。(言語拡張でどうたらあるらしいがシラネ) 詳しい動機はまあいろいろあるみたいですが GADT とか Type family の型推論の効率とか完全性とかそういう方面らしいです。正直両方とも使わないのであまりありがたみがわかりません。で、世の中 Haskell のやることは外でも全て正しいという考えの方がおられまして、 OutsideIn(X) は Haskell で問題ないのだから他でも問題が無いはずだとかおっしゃるわけです。

あんまり科学的な態度じゃないですよね。まあプログラミング言語論のこれは便利だ便利じゃないなんて思想であって自然科学じゃあないので究極的には好き嫌いの問題だと私は思うからまあいいっちゃあいいんですが。

というわけで OutsideIn(X) を OCaml でやったらどうなるか確かめました: https://github.com/camlspotter/ocaml/tree/outsideinx

  • local let は基本 generalization なし
  • let! と書いた場合のみ generalization あり
  • top let は generalization あり
  • NOOUTSIDEINX という環境変数が定義されていると let でも let! でも generalization あり

という実装です。実装は簡単です。ブートストラップで微妙に手間かかりますので一気にはできません。少しづつブートストラップしていく感じ。

ちなみに OCaml で OutsideIn(X) して何がうれしいというと何もありません。OCaml にも GADT があるじゃないか?うんなんかそれは惹玖が OutsideIn(X) 使わなくても上手くなんかいく方法とかを発表するらしいよ。GADT は使わないんで、正直良くわかんないよ俺は。

さてさて、じゃあ let! がどれくらい必要かということですね。 OCaml コンパイラ一式をこの弱い let と let! で make world 通すのにどれだけ変更がいるかってことでまあ計ります。 find . -name *.ml | xargs grep -n 'let!' だすなー:

./stdlib/camlinternalOO.ml:347:  let! undef = fun _ -> raise (Undefined_recursive_module loc) in
./stdlib/arg.ml:110:  let! stop error =
./stdlib/printf.ml:488:  let! get_arg spec n =
./stdlib/scanf.ml:1328:  let! stack f = delay (return f) in
./stdlib/scanf.ml:1329:  let! no_stack f _x = f in
./stdlib/scanf.ml:1381:      let! stack = if skip then no_stack else stack in
./ocamldoc/odoc_info.ml:211:  let! p = Printf.bprintf in
./ocamldoc/odoc_html.ml:509:      let! index_if_not_empty l url m =
./ocamldoc/odoc_html.ml:978:        let! link_if_not_empty l m url =
./camlp4/boot/camlp4boot.ml:927:          let! grammar_entry_create = Gram.Entry.mk in
./camlp4/boot/camlp4boot.ml:10824:          let! grammar_entry_create = Gram.Entry.mk in
./camlp4/boot/camlp4boot.ml:12147:          let! grammar_entry_create = Gram.Entry.mk in
./camlp4/boot/camlp4boot.ml:14093:          let! grammar_entry_create = Gram.Entry.mk in
./camlp4/boot/camlp4boot.ml:14911:          let! grammar_entry_create = Gram.Entry.mk in
./camlp4/boot/camlp4boot.ml:15214:          let! grammar_entry_create = Gram.Entry.mk in
./bytecomp/translmod.ml:695:  let! rec make_sequence fn pos arg =
./typing/typemod.ml:593:  let! transition env_c curr =
./typing/includecore.ml:135:  let! pr fmt = Format.fprintf ppf fmt in
./typing/typecore.ml:1087:  let! bad_conversion fmt i c =
./typing/typecore.ml:1089:  let! incomplete_format fmt =
./ocamlbuild/hygiene.ml:157:            let! fp = Printf.fprintf in
./ocamlbuild/command.ml:361:  let! list = List.fold_right in
./ocamlbuild/rule.ml:255:  let! res_add import xs xopt =
./ocamlbuild/main.ml:52:  let! pp fmt = Log.raw_dprintf (-1) fmt in

おっこれだけしかない!!そうなんですねー、 OCaml でもローカル let bound な名前が polymorphic に使われることはほとんどないのですね。

OCaml で local polymorphism が使われるパターン:

  • 既存の多相型の値の別名をつける: let list = List.fold_right in ...
  • 最後に例外を投げるので返り値が多相: let stop error = prerr_endline error; raise Error in ...
  • カスタム printf : let pp fmt = Log.raw_dprintf (-1) fmt in ...

OCaml コンパイラでは無闇にこのカスタム printf 系が多い。

じゃあ、あんまりないし、OCaml でも OutsideIn(X) 問題ないよね?!という結論に傾きかけますがそれはどうなんかなとわしは思ったね。

上を見たらわかるんだけど let! が必要なのはほとんどの場合、既存の多相関数に別名をつけているところよね。たとえば let! pr fmt = Format.fprintf ppf fmt とか。OCaml 結構こういう使い方します。Haskell だと一部は import Format(fprintf) とかなるところです。

さらに OCaml は Haskell と違って local なコードは local let にするのが好きな文化です。何故か知らないが Haskell の人は結構外に出しちゃいますよね。Haskell なら外に出しちゃうコードが OCaml では外に出さない文化なので問題出ました、とかありそうです。

OCaml は Haskell と違ってコード中に型をほとんど書きません。(.mli には書くけどこれは実装終わってからおもむろに自動生成する方が普通)なので polymorphic な型書けば元の let になるよ!という GHC の方式は受け入れられないと思いますね。なんでここでは let! と書くことで元の挙動に戻すということをやってみたんですが。それでも、今までの let に慣れている人が、こういう時いちいち let かなー let! かなーとか考えなきゃいけないってのはつらいですね。あと、後から let! つけるの大変ですよ結構。型エラーから読み解くいて let! つけていくの、 OCaml コンパイラのブートストラップで実は時間かかりました。この関数が多相的だったら良かったのにね!みたいな親切なコンパイラエラー出すのはできるんかもしれんけど、多分 let 周りの型付けでこれ多相にできたけど OutsideIn(X) だからあきらめたわーみたいな情報をつけなきゃいけない。すごく手間かかりそうですね…

OutsideIn(X) の論文 http://research.microsoft.com/en-us/um/people/simonpj/papers/constraints/jfp-outsidein.pdf では

  • library : 30 packages 94954行で 127行の変更が必要だった
  • 793個の Hackage をコンパイルしなおしたら 95個ができなかった(あるパッケージ自体が問題なくても依存パッケージで問題があれば失敗になる、という一番楽なテストみたい)

らしいんでちょっとやってみたんですが

  • OCamlFind ですでに修正が必要
  • OPAM パッケージは… OASIS が作った setup.ml が入っているとそこで local polymorphism 使っているから自動ビルド絶対無理

ということになり、数字出す前からやる気出ませんね…別に論文書くわけじゃなし。 ちなみに polymorphic variant とか class 使っていると困るかなと思って手で LablGtk2 やってみたんですが 17箇所変更が必要、しかしこれは polymorphic variant も class も関係ありませんでした。結構多いよね。

OCaml に OutsideIn(X) 入れたら既存コードの修正は Haskell より結構大変だと思います。まそれよりなにより OCaml は Backward compatibility を GHC より大切にするから絶対採用できませんね。まあそもそも旨みがないのだから採用する意味も無いが。