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はデフォなんで、コマンドラインから 引数として、別のものを与えても良い。