ocaml to sml
所要で上京。この機を逃さず、銀座の林檎屋を偵察。初めて行ったけど混雑してますなあ。 老いも若きも皆真剣そのもので吟味中。スタップの方は、とっても親切でフレンドリィー。
おいらのipadは、まだ7にしてないんで、ここで7の感触を確かめたよ。いいんでないかい。 少なくても、窓7から8への移行よりは断然ユーザーの気持ちを考えてくれているよ。
窓7のマシンが若し壊れたら、窓8には多分しないね。その代替は林檎のマシンかな。 これなら、デフォでBSD(改)が入ってるから、デストロをあれこれする必要ないからね。 スタップの人に、コンソールはどうやって開くんですか? って聞いたよ。Appの中の ユーティrティに置いてあった。まあ、妥当な場所か。
bashが張り付いて80x24の端末が出てきた。topとかduとかrubyとかpythonとかemacsとか 普通に入ってた。林檎印のBSDを使うようになるのは何時だろう?
そうそう、おいらに取っちゃ大事な用件、英語キーボードモデルは有りますか? って聞いたら 勿論との事。在庫確認しましょうかと、向こうの人は売り込みモードに入ってましたよ。 こちらの方も英語キーボードを 選択されたようで、ウブに浮気(本命?)するためのハードとしてMACを選んでる風でした。
巣鴨でみやげを調達。四の市とかで、東京中の婆が集まってきた感じ。巣鴨信金の大 サービスとかもやってて、地域の皆様に愛されてますなあ。巣鴨名物の2枚で1000円也の 赤色ズロースが飛ぶように売れてました。縁起物だからねぇ。
おいらは、とげぬけ地蔵をすりすりしようと思ったんだけど、婆パワーに圧倒されて はじき飛ばされちゃったぞ。しょうがないので、地蔵さんのたもとで売ってた、幸福だんご を食べて、御利益を祈る。
すぐ近くにある有名なカレー屋に入ろうとした、大行列。諦めて、古奈屋のクリーミーカレーうどん 匠揚げを買って、食べた積もりになったよ。後は塩大福ですな。赤褌は見つからなかった、 残念。
上京中はお付き合い頂き、献本してくださった、某さんに感謝。
diff -c ocaml sml
前回、sml mlton smlsharp に出合ったので、折角だから例題をやっておく。例によって お題は、retro.mlの移植だ。これ、程よくいろいろな機能を使っていて勉強するには 好都合だ。
で、いきなり取り掛かってもいいんだけど、ocamlとsmlの機能の差を見ておこう。 ocaml人口を増やしたくて、ocaml.jpには、 関数型言語の比較 なんてのが用意されてるけど。。自分で調べてみるのも一興かと。
タプルは便利、ocamlだと、括弧でくくらんくて、カンマで区切ってデータを並べてく だけで、組になってくれる。対してsmlは、ちゃんと括弧でくくらないと組とは 認めんというかたくな態度。
これが迷いを生むのよ。移植元のコードの中にあった奴。
dyadic2 (fun x y -> y / x, y mod x) (* divmod *)
これの意味が明確に理解出来ず。 A Concise Introduction to Objective Caml のタプルの使い方を見て、やっと納得しましたよ。上記コードは、内部関数扱いになってて 状況が把握出来なった(と言い訳しとこう)というものあるけど。
smlで実験すると、こんな具合。
- fun hoge x y = (x div y, x mod y, x + y, x - y, x * y) ; val hoge = fn : int -> int -> int * int * int * int * int - hoge 10 3 ; val it = (3,1,13,7,30) : int * int * int * int * int
もう一つ、タプルの話題で、ocamlにある、fstとかsndはsmlには無いの? まさかね。 シャープ番号っていう関数らしくない関数が用意されてる。番号は1から始まる。 フランス人がfirstを短縮したfstを使ってるのに、ニュージャージーの人は番号指定で 余計な負荷を下げている。どんな風に使うかと言うと、上の例に続いて
- #4 it ; val it = 7 : int
何番までサポートしてるかは知らないけど、確かにこちらの方が融通はききそうだ。
続いてocamlでは、普通に使う演算子がデフォで、前置と中置をサポートしてるんだけど、 smlは、そうなっていない。
- 3 + 5 ; val it = 8 : int - + (3,5) ; stdIn:2.1 Error: expression or pattern begins with infix identifier "+" - nonfix + ; nonfix + - + (3,5) ; val it = 8 : int
nonfix ってのを使って、中置を解除しろとな。そして、引数は組で渡すんだぞってのは、 sml界の暗黙の了承事項です。
- 3 + 5 ; stdIn:4.1-4.6 Error: operator is not a function [literal] operator: int in expression: 3 + - infix + ; infix + - 3 + 5 ; val it = 8 : int
元に戻すには、ちゃんと指定する事。あいまいさの排除に効果があるのかな。infix と言えば、 smlには面白い機能が有ったんだ。関数合成。
- fun nix x = x * 2 ; val nix = fn : int -> int - val many = nix o nix o nix o nix ; val many = fn : int -> int - many 2 ; val it = 32 : int
最初nixって言う、引数を2倍する関数を定義。次はその関数をoというinfix演算子を使って 関数合成。定義をvalで行うのが味噌かな。合成を使わないで書くとするとnix(nix(nix(nix x)))って 具合に、lispっぽくなっちゃう。括弧が目障りな人、ご用達。自分で定義するなら、
- infix ~ ; infix ~ - fun f ~ g = fn x => f (g x) ; val ~ = fn : ('a -> 'b) * ('c -> 'a) -> 'c -> 'b - val my = nix ~ nix ~ nix ~ nix ; val my = fn : int -> int - my 2 ; val it = 32 : int
ocaml.jpでも、演算子って事で、取り上げ られていた。
そうそう、valとfunの使い分けも厳格だなあ。ocamlみたいに何でもletじゃないもん。 ここら辺は、commonLisp風なんだな。逆にocamlはscheme風か。どちらがいいかは、その 人の趣味というか過去の言語遍歴によるのかな。
smlには、WordとかWord8とかの型がある。Word型を使うとMS Office様ご一行のファイルを 取り込めるかと思ったら、そうじゃ無かった。別にswl業界の人はM$に癒着してる訳では無く、 Word型で、バイナリー型を表している。ちょっとセンスが悪いと思うぞ。
論理andとかxorを取ろうとすると、一度Word型に変換してからじゃないと駄目だ。 確かに整数をBit列と看做してandを取るなんてのは、便利ではあるけど、型に五月蝿い人 に取っては看破出来なかったんだろうな。で、その弊害が。。。
- val x = Word.fromInt ~1 ; [autoloading] [library $SMLNJ-BASIS/basis.cm is stable] [autoloading done] val x = 0wx7FFFFFFF : word - Word.toInt x ; uncaught exception Overflow [overflow] raised at: <file stdIn>
整数の世界(の -1)から一度、Word界に入り、また元に戻そうとしたんだ。そしたら エラーを喰らったぞ。これは重い足枷だぞ。おまけにintのBit幅は31Bitって言う伝統も あるしね。ああ、31Bit幅はocamlも一緒か。
手動patchと言うか、何というか
早速、上記の調査を元にocaml用ソースをsml用に変換してみる。変換と言うよりpatch当て と言った方が適切かな。
ocamlに有った制御構造、forがsmlには無い。代わりを務めるのはwhileだ。forからwhile への書き換えは簡単だけど、終了条件の用意が、今回のケースでは面倒そう。だって、ファイル サイズを調べているんだもん。で、がらっと書き換えてみた。EOFが出て来るまで、突き進め 方式にね。
fun load () = let open BinIO val p = openIn image_file fun buildInt (SOME a, SOME b, SOME c, SOME d) = if Word8.andb(d, 0wx40) = 0wx40 then (* negative *) (Word8.toInt (Word8.andb ((Word8.notb d), 0wx3f)) * 16777216 + Word8.toInt (Word8.notb c) * 65536 + Word8.toInt (Word8.notb b) * 256 + Word8.toInt (Word8.notb a)) * ~1 - 1 else (* positive *) Word8.toInt d * 16777216 + Word8.toInt c * 65536 + Word8.toInt b * 256 + Word8.toInt a fun rom (_, NONE, _, _, _ ) = () | rom (i,a,b,c,d) = ( update(ram, i, buildInt(a,b,c,d)); rom(i + 1, input1 p, input1 p, input1 p, input1 p) ) in rom(0, input1 p, input1 p, input1 p, input1 p) ; closeIn p end
再帰大好きバージョンで、ちまちまと1バイトづつ読み込んでいる。バイトが読めれば SOME a が帰ってくるし、EOFに達していたら、NONE が帰って来るんだ。4バイト読み込んで 、それを整数に組み立てている。負数か否かを判定して、負数なら、最上位バイトを反転し 2ビット分を落としてから整数領域へ持って行って桁数を掛ける、なんて面倒な事をやってる。 そして、-1を掛けてから1を引き、正しい負の数に戻してる。正直めんどう臭い。
実は上記コードを最初から書いていた訳では無い。最初に書いたコードを走らせたら
uncaught exception Overflow [overflow] raised at: <file j.sml> /usr/local/smlnj/bin/sml: Fatal error -- Uncaught exception Overflow with 0 raised at <file j.sml>
エラーが出たよっていうM$並みの報告を受けて、頭をかかえたのさ。exceptionの時、 もう少し詳細を報告してくれる方法って有るんかね? しょうがないので、romの所に print文を入れて、どの辺を読んでる時に発生したか確認。
00000180 00 00 00 00 0c 00 00 00 6e 00 00 00 01 00 00 00 |........n.......| 00000190 04 00 00 00 0e 00 00 00 01 00 00 00 ff ff ff ff |................|
ffffffffを処理しようとして落ちた。対策を練ったら、buildIntが生まれたって訳。 負数の扱いに齟齬をきたしていたんだ。
ああ、上記ではBinIOの返す値をSOME a して、aと言う中身を取り出したけど、val a = input1 p とか した場合、中身を取り出すには、valOf a すれば良い。これも、いろいろ歩き回った成果 です。なお、ocamlとsmlを同時にやると、精神分裂、情緒不安定になるそうだから、注意、注意、注意。 (3回警告しましたからね。情緒不安定になっても、おいらに責任を擦り付けないように。)
変換済みのコードは最後に載せるとして、 実行時間は例のloopの計測。byeが旨く動いていなくてsmlのpromptに戻っちゃうので、 すかさずCtrl-Dしたよ。
[sakae@fedora sml]$ time sml j.sml Standard ML of New Jersey v110.75 [built: Mon Nov 18 06:57:50 2013] [opening j.sml] : val it = () : unit - real 0m13.624s user 0m13.285s sys 0m0.225s
コンパイル
fedoraにはmltonとsmlsharpを入れてるんで、コンパイルしてみる
[sakae@fedora sml]$ smlsharp j.sml j.sml:51.1-61.16 Warning: match nonexhaustive (SOME a, SOME b, SOME c, SOME d) => ... [sakae@fedora sml]$ mlton j.sml Warning: j.sml 51.2. Function is not exhaustive. missing pattern: (NONE, _, _, _) | ((SOME _), NONE, _, _) | ((SOME _), (SOME _), NONE, _) | ((SOME _), (SOME _), (SOME _), NONE) in: buildInt (SOME a, SOME b, SOME c, ... 256 + Word8.toInt a
何か警告が出てきたぞ。両方共、パターンマッチで全てのケースを考慮してないでっせって いう、有り難い忠告。こういうのは、親の忠告と冷酒は後で効く っていうから、素直に 従うのが吉。今回は、無視するけどね。で、成果物は、smlsharpからは、a.outが得られ、 mltonからは、j だ。(本物のjはいつか試してみよう)
[sakae@fedora sml]$ time ./a.out uncaught exception: Underflow Aborted real 0m0.205s user 0m0.089s sys 0m0.102s [sakae@fedora sml]$ time ./j real 0m1.595s user 0m1.274s sys 0m0.282s
両方共、コンパイルしちゃったら、機能しなくなったぞ。もうお手上げだ。
hamlet
シェークスピアの有名な戯曲 ハムレットに有名なフレーズが出て来る。
To be, or Not to be.
それに倣えばおいらの今の心境は、
To be, or Not to be ocaml.
ocamlを使うべきか否か。そんな心境を知ってか知らずか、 HaMLetなんてのが出てきた。 これ、INSTALL.txtを読むと、SML界の統一理論っぽい。
To build a stand-alone HaMLet program under Unix-like systems, invoke one of the following commands: make with-alice (for Alice ML 1.4+) make with-mlkit (for ML Kit 4.3+) make with-mlton (for MLton 20010706+) make with-mosml (for Moscow ML 2.0+) make with-poly (for Poly/ML 5.0+) make with-smlnj (for SML/NJ 110+) make with-smlsharp (for SML# 0.20+) depending on what SML system you want to compile with. This will produce an executable named "hamlet".
ocamlを除くML系だったら、何でもお任せあれっていう悩み解消アプリっぽい。んでもって 早速入れてみた。
[sakae@pcbsd ~]$ /usr/local/hamlet/hamlet HaMLet 2.0.0 - To Be Or Not To Be Standard ML [loading standard basis library] -
manをみると
OPTIONS The mode option controls how HaMLet processes its input. It is one of : -x execution mode (parse, elaborate, and evaluate input) [default] -j compilation mode (parse, elaborate, and compile to JavaScript)
ここにも、JavaScriptへの変換モードが組み込まれていて、美味しい水ありますよと、 誘ってる。(お前は、ホタルか!)そして美味しいのは、ソースコードに有り。 上から下までsmlで書かれていて、お腹一杯になるぞ。
試しにJavascriptをやってみた。まずは、元ネタ。
[sakae@pcbsd ~/src/sml]$ cat aaa.sml fun f 0 = 1 | f n = n * f (n - 1) ; print( Int.toString (f 10) ^ "\n" ) ;
変換したぞ。
[sakae@pcbsd ~/src/sml]$ /usr/local/hamlet/hamlet -j aaa.sml [loading standard basis library] [processing aaa.sml] var f = function(_x2) { try { return (function() { if (_x2 !== 0) throw "FAIL"; return 1; })(); } catch(_x3) { if (_x3 !== "FAIL") throw _x3; return (function() { var n = _x2; return _SML._checkOverflow(n * f(_SML._checkOverflow(n - 1))); })(); } }; var it = print(_caret($Int.toString(f(10)), "\n"));
js用のライブラリが必要そうだなあ。hamlet-2.0.0/compile-js/runtime.jsがそれ用なのかなあ? 付属のdocを見たら、make js して出来上がる、basis.js と一緒に使えとな。
でも、何のかんの言っても、 結局結論はお決まりの vi vs. emacs 宗教論争に還元される訳ですよ。(ありきたりで御免)
不完全なコード
翻訳する旅に品質が劣化してくのは世の常で(伝言ゲームを思い出して納得してくれ) 劣化版を、晒しておきます。
(* import from z.ml *) open Array ; val mem_size = 1000000 ; val image_file = "retroImage" ; val ports_size = 12 ; val pc = ref 0 ; val ports = array( ports_size, 0) ; val ram = array( mem_size, 0xdeadbee) ; val halt = ref false ; val data = ref (nil : int list) ; val address = ref (nil : int list) ; exception Underflow ; fun pushVal d (x:int) = d := x :: !d ; fun popVal d = case !d of car :: cdr => ( d := cdr; car ) | _ => raise Underflow ; fun push v = pushVal data v ; fun pushr v = pushVal address v ; fun pop () = popVal data ; fun popr () = popVal address ; fun tos () = List.hd (!data) ; fun |> (x, f) = f x ; infix |> ; (* for debug, monitor data or address stack *) fun si i = if i < 0 then "-" ^ Int.toString (~i) else Int.toString i ; fun mon3 rx = let fun sis i = si i ^ "; " in case !rx of [] => "[]" | [a] => "[" ^ si a ^ "]" | [a, b] => "[" ^ sis a ^ si b ^ "]" | [a, b, c] => "[" ^ sis a ^ sis b ^ si c ^ "]" | a::b::c::_ => "[" ^ sis a ^ sis b ^ sis c ^ "..]" end fun load () = let open BinIO val p = openIn image_file fun buildInt (SOME a, SOME b, SOME c, SOME d) = if Word8.andb(d, 0wx40) = 0wx40 then (* negative *) (Word8.toInt (Word8.andb ((Word8.notb d), 0wx3f)) * 16777216 + Word8.toInt (Word8.notb c) * 65536 + Word8.toInt (Word8.notb b) * 256 + Word8.toInt (Word8.notb a)) * ~1 - 1 else (* positive *) Word8.toInt d * 16777216 + Word8.toInt c * 65536 + Word8.toInt b * 256 + Word8.toInt a fun rom (_, NONE, _, _, _ ) = () | rom (i,a,b,c,d) = ( update(ram, i, buildInt(a,b,c,d)); rom(i + 1, input1 p, input1 p, input1 p, input1 p) ) in rom(0, input1 p, input1 p, input1 p, input1 p) ; closeIn p end fun key () = ord (valOf (TextIO.input1 TextIO.stdIn)) ; fun ioProcess () = if sub(ports,0) = 0 then ( update(ports, 0, 1) ; if sub(ports,1) = 1 then update(ports, 1, key ()) else () ; if sub(ports,2) = 1 then ( update(ports,2, 0) ; let val x = pop () in (print (String.str (chr x)) ; TextIO.flushOut TextIO.stdOut ) end ) else () ; update(ports, 5, (case sub(ports,5) of ~1 => mem_size | ~5 => List.length (!data) | ~6 => List.length (!address) | ~9 => (halt := true; 0 ) | _ => 0 ))) else () fun incPC () = pc := (!pc) + 1 ; fun condJump f = let val x = pop (); in if f (pop (), x) then pc := sub(ram, !pc + 1) - 1 else incPC () end fun jump () = ( pc := sub (ram, !pc) - 1 ; if sub (ram, !pc + 1) = 0 then incPC () else () ; if sub (ram, !pc + 1) = 0 then incPC () else () ) ; fun drop () = pop () |> ignore ; fun loop () = ( pop () - 1 |> push ; if tos () > 0 then pc := sub (ram, !pc + 1) - 1 else (incPC (); drop ()) ) ; fun logic f = let val x = Word.fromInt (pop ()) ; val y = Word.fromInt (pop ()) ; val z = f (x, y) ; val v = if Word.andb (z, 0wx40000000) = 0wx40000000 then Word.toInt (Word.andb(Word.notb z, 0wx3fffffff)) * ~1 - 1 else Word.toInt z in v |> push end nonfix >; nonfix <; nonfix =; nonfix <>; fun doInst () = case sub (ram, !pc) of 0 => () (* nop *) | 1 => ( incPC () ; sub (ram, !pc) |> push ) (* lit *) | 2 => tos () |> push (* dup *) | 3 => drop () (* drop *) | 4 => let val x = pop (); val y = pop () in x |> push; y |> push end (* swap *) | 5 => pop () |> pushr (* push *) | 6 => popr () |> push (* pop *) | 7 => loop () (* call *) | 8 => ( incPC () ; jump () ) (* jmp *) | 9 => pc := popr () (* return *) | 10 => condJump > (* gt_jmp *) | 11 => condJump < (* lt_jmp *) | 12 => condJump <> (* ne_jmp *) | 13 => condJump = (* eq_jmp *) | 14 => sub (ram, pop ()) |> push (* fetch *) | 15 => let val p = pop (); val v = pop () in update(ram, p, v) end (* store *) | 16 => push (pop () + pop ()) (* add *) | 17 => push (pop () - pop ()) (* sub *) | 18 => push (pop () * pop ()) (* mul *) | 19 => let val x = pop(); val y = pop() in x div y |> push; x mod y |> push end (* divmod *) | 20 => logic (Word.andb) (* and *) | 21 => logic (Word.orb) (* or *) | 22 => logic (Word.xorb) (* xor *) | 23 => logic (Word.<<) (* lsl *) | 24 => logic (Word.>>) (* asr *) | 25 => if = (tos (), 0) then (drop (); pc := popr ()) else () (* z_ret *) | 26 => pop () + 1 |> push (* inc *) | 27 => pop () - 1 |> push (* dec *) | 28 => let val x = pop () in sub (ports, x) |> push; update (ports, x, 0) end (* in *) | 29 => let val p = pop (); val v = pop () in update (ports, p, v) end (* out *) | 30 => ioProcess () (* wait *) | _ => (!pc |> pushr ; jump ()) ; (* call by op-data *) fun runme () = if not (!halt) then ( (* print( (si (!pc)) ^ ":\t\t" ^ (mon3 data)^ " "^ (mon3 address)^"\n") ; *) doInst (); pc := !pc + 1 ; runme () ) else () ; load () ; runme () ;
smlで実行ファイル作成
ML演習を見てたら、exportFnの使い方なんてのが出てたんだけど、指示通りにやっても エラーになるばかり、例によって例を探してみたら見つかった。礼。
fun yes (_, args) = let val msg = case args of [] => "yes\n" | x::_ => x ^ "\n" in while true do print msg; OS.Process.success end; SMLofNJ.exportFn ("hoge",yes);
これをsmlに喰わせると、hoge.x86-bsdが出来るんで、これをロードする薄いラッパーを 用意する。
sml @SMLload=hoge "$@"
これを実行すれば、yesって永遠に答え続ける。yesはデフォなんで、コマンドラインから 引数として、別のものを与えても良い。