F# to ocaml

『脳と機械をつないでみたら』(岩波現代全書)なんて本を読んでみた。 古くはエイトマン、最近では攻殻機動隊やロボコップでお馴染みのSF界の定番、脳だけで機械を動かしちゃえ っていう研究者の手による本。事故とか病気で体を動かすのが不自由になった時、 体の代わりを機械(ロボット)にさせ、そのロボットの頭脳は、人間様の脳を 拝借出来たらいいねって、夢の話。

まだ研究段階、脳に多数の電極を埋め込んで脳の活動信号を取り出す。それを コンピュータで解析して、機械に指令する。 脳ー機械ーインターフェースって事で、BMIって呼ぶようだ。

脳梗塞とかで、手が不自由になった。BMIを取り付ける。目の前にあるご飯を 食べたいなーって念じると、それがコンピュータで解析されて、ロボットが ご飯を口に運んでくれる。いつ実現するんでしょうね?

脳というものの動きが分からないので、研究は大変らしい。人間に適用できる ようになるのはずっとずっと先と悲観的な意見を述べておられた。

実験は、今の所ラットが主に使われているらしい。人間に近いということでは 猿って事になるんだけど、猿の脳でも非常に個性があり過ぎて、実験には適さない らしい。

脳は20歳ぐらいで完成し、後は年齢と共に衰えるばかりと言うのが今までの定説 だったらしいけど、最近はこれを否定する事実が次々に見つかっているとか。 脳の部位によって、聴覚担当とか言語担当とか、部位担当説が有ったようだけど、 これもそうとは言い切れない事実が見つかっているらしい。

心はどこに存在するか?事故等で手足を無くした方が経験する、仮想の手足の 感覚とか、眠るってどういう事ってのも、よく分かっていないそうだ。

勿論、最近話題になる心の病気、(うつは心の風邪)ってやってるけど、正確な 診断は難しいらしい。精神安定剤の普及と共に、うつ等の病気も増えたという 統計があるそうな。薬屋の陰謀かもって疑問を投げかけておられた。 なかなか刺激的な本でしたよ。

そうそう、著者の方は私と同年代だった。ラットを教育する為の装置を作ったは いいが、その装置にずっと張り付いているのは癪。当時売り出されていたパソコン シャープのMZ-80Kを使って、自動化を図ったとか。懐かしいねぇ。おいらが初めて 持ったパソコンがMZ-80Kでしたよ。 MZ-80K SYSTEM PROGRAM なんてのを見て、昔を思い出そうかな。

移植するぞ

折角久しぶりにocamlに出会ったんだから、何かやってみよう。色々と題材を考えたけど 安直に、retro.fsxをocamlに移植する事にしよう。ああ、移植と言うより翻訳かな。 F#もocamlも両方分かっていないと出来ないだろうから、一度で二度美味しい体験に なるかどうか。

例によってemacsを使うので、ocamlモードを入れておく事にする。んだけど、ocamlの ソースに付属してるocamlモードは評判が余り宜しくないので、tuaregってのを入れた。 最近のemacsはrubyのgem並みに楽チンしていろいろ入れられる。

(require 'package)
(add-to-list 'package-archives '("melpa" . "http://melpa.milkbox.net/packages/"))
(unless package-archive-contents (package-refresh-contents))
(package-initialize)

;; ocaml tuareg-mode tuareg-run-ocaml
(load "~/.emacs.d/tuareg/tuareg.el")
(autoload 'ocamldebug "ocamldebug" "Run the Caml debugger" t)
(setq auto-mode-alist
          (cons '("\\.ml[iylp]?$" . tuareg-mode) auto-mode-alist))

retro.fsxをz.mlって名前でcopyして、それを改変してく。無味乾燥な短い名前は、単に 入力しやすいからだ。他意は無い。

まず、openなんて言う甘っちょろい宣言は容赦なく削除。WindowsのF#の事はきっぱりと 忘れてくれ。これで、C-c C-b すると、画面が割れてocamlのreplが走り出し、今編集してる ファイル内容が評価される。

大文字で定数を表すってのはRubyあたりでも普通に使われていてF#でもそれに倣っていたけど、 おフランス人は違う考えみたい。大文字は構成子かモジュール名にしか使っちゃだめって いう制限を付けている。小文字で始まるのが変数だよと。。。そもそも変数へは一度しか 代入出来ないんだから、全てが定数と思ってよい。従って、定数を文法で表現するってのは ナンセンスなのよ。 そんなこんなで、エラーを一つづつ丹念に潰していきます。

で、やっとエラーが消えたんで走らせてみると、

sakae@debian7:~/src/ml$ ocaml z.ml
Exception: Invalid_argument "index out of bounds".

indexが範囲外ですって。indexって言うと配列の添え字だな。どんな風になってるか ocamlのrepl上で確かめてみるか。

# #use "z.ml" ;;
 :
val load : unit -> in_channel -> unit = <fun>
val exec : unit -> unit = <fun>
- : in_channel -> unit = <fun>
Exception: Invalid_argument "index out of bounds".
# load () ;;
- : in_channel -> unit = <fun>
# memory.(0) ;;
- : int = 134217728
# memory.(1) ;;
- : int = 941948928
# memory.(2) ;;
- : int = -598147072

retroImageのファイル内容をメモリーに読み込んで実行した所でやはりエラー。何か 返り値もおかしいぞ。まずは、メモリーの内容を確認。。。異常値っぽいぞ。

こういう時は、お手本のretro.fsxを参照してみるに限る。えと、monodebelop上だと debuggerも使えるんだけど、CUI上ではどうするか?適当に走らせてみる。

> #load "retro.fsx" ;;
[Loading /home/sakae/src/ml/retro.fsx]


/home/sakae/src/ml/retro.fsx(114,13): warning FS0058: Possible incorrect indentation: this token is offside of context started at position (113:22). Try indenting this token further or using standard formatting conventions.
Retro 11.6

ok  bye

namespace FSI_0002
  val MEM_SIZE : int
   :
  val load : unit -> unit
 
> memory.(0) ;;

  memory.(0) ;;
  ^^^^^^^^^^
  memory.(0) ;;
  ^^^^^^
/home/sakae/src/ml/stdin(6,1): error FS0039: The value or constructor 'memory' is not defined

どうも、retroが走り終わった後では、環境にアクセス出来ないみたいだ。しょうがないので スクリプトを分解して実行してみる。

> open System
- open System.IO
- open System.Text
- ;;
> let MEM_SIZE = 1024 * 1024
- let IMAGE_FILE = "retroImage"
- ;;

val MEM_SIZE : int = 1048576
val IMAGE_FILE : string = "retroImage"

> let mutable memory = Array.create MEM_SIZE 0;;

val mutable memory : int [] =
  [|0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0;
    0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0;
    0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0;
    0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0; 0;
    ...|]
> let load() =
-     use binReader = new BinaryReader(File.Open(IMAGE_FILE, FileMode.Open))
-     for i in 0 .. int (binReader.BaseStream.Length / 4L) - 1 do
-         memory.[i] <- binReader.ReadInt32()
- ;;

val load : unit -> unit

> load () ;;
val it : unit = ()
> memory.[0];;
val it : int = 8
> memory.[1];;
val it : int = 9656
> memory.[2];;
val it : int = 22876

load () に関係する所だけ、fsharpi上に切り貼りしてみた。メモリー内容もまともそう。 ちなみに、retroImageはどうなっているかと言うと

sakae@debian7:~/src/ml$ hd retroImage | head -n 2
00000000  08 00 00 00 b8 25 00 00  5c 59 00 00 f1 59 00 00  |.....%..\Y...Y..|
00000010  00 00 00 00 04 21 00 00  40 42 0f 00 00 00 00 00  |.....!..@B......|

Hex並びを逆に嫁ってのは糞インテルのいやがらせでしょうか。めんどくさい。

移植したやつは、

let load () =
  let binReader = open_in_bin image_file in
    for i = 0 to  (in_channel_length binReader / 4) - 1 do
        memory.(i) <- input_binary_int binReader
    done;
    close_in;;

こんな風に最初書いてた。input_binary_intって、ビッグエンディアンなのね、フランス人も インテル嫌いなんでしょうかね。

このままでは埒があかないので、インテル風に馴染むように、コードを書き換え。

let b4 = Array.make 4 0 (* buf for big endian to little endian *)

let load () =
  let binReader = open_in_bin image_file in
    for i = 0 to  (in_channel_length binReader / 4  - 1) do
        b4.(0) <- input_byte binReader;   (* LSB *)
        b4.(1) <- input_byte binReader;
        b4.(2) <- input_byte binReader;
        b4.(3) <- input_byte binReader;
        memory.(i) <- b4.(3) lsl 24 + b4.(2) lsl 16 + b4.(1) lsl 8 + b4.(0)
    done;
  close_in binReader ;;

わざわざ、こういうコードを書かせるフランス人は頑固だと思うぞ。

ocamldebug

ロード問題をクリアしたんで走らせてみると、今度は即終了しちゃう。何でだ?

# #use "z.ml" ;;
val mem_size : int = 1048576
val image_file : string = "retroImage"
 :
val load : unit -> unit = <fun>
val key : unit -> int = <fun>
val devices : unit -> unit = <fun>
val dyadic : (int -> int -> int) -> unit = <fun>
val dyadic2 : (int -> int -> int * int) -> unit = <fun>
val incIp : unit -> unit = <fun>
val condJump : (int -> int -> bool) -> unit = <fun>
val jump : unit -> unit = <fun>
val drop : unit -> unit = <fun>
val loop : unit -> unit = <fun>
val exec : unit -> unit = <fun>
- : unit = ()
- : unit = ()

最後の2つは、loadとexecだ。矯正的にexec () しても即終了。なお、最初おいらも 馴染めなかったんだけど、execの後ろにある、()は、関数呼び出しの括弧じゃなくて、 無意味な値って意味ね。要するに、execは引数無しよって事。lispあたりだと、nilって 事だろうけど、ocaml用語では、unitって言うらしい。

講釈はこれぐらいにして、こういう時はdebuggerで地道に追跡かな。幸いocamlにも ocamldebug が用意されてるから。ocamldebugを使うには、ちょっとした下準備が必要だ。

[sakae@pcbsd ~/src/ml]$ ocamlc -g z.ml -o z

ソースファイルと、作成されたオブジェクトファイルの名前を合わせておく事。一致して いないと、ファイルが見つからんと文句を言われる。尚、ソースの主要部分は以下の 通り。

    78  let rec exec () =
    79      if not (!halt) then begin
    80          match memory.(!ip) with
    81              | 0 -> ()
    82              | 1 -> incIp (); memory.(!ip) |> push
    83              | 2 -> tos () |> push
     :
   110              | 29 -> ports.(pop ()) <- pop ()
   111              | 30 -> devices ()
   112              | _ -> pushr !ip; jump () ;
   113          ip := !ip + 1 ;
   114          exec () ;
   115       end ;;
   116
   117  load () ;;
   118  exec () ;;

オリジナルでは、execの中に、incIpとかjump等の関数を内部定義してたけど(こうするのが 関数名が外に漏れないので良い習慣です)あえて、外に出して、execの動きがより 分かりやすくしました。以下、CPUの動きの説明。

関数execは、プログラムをロード後(117行目)、一度だけ呼び出される。(118行目) 昔々その昔、ミニコンを動かす方法は、入力SWがたくさん付いたコンソールから、 パチパチをプログラムを入力して、startのスイッチを押す事だったな。それと そっくりだよ。ああ、どの番地から実行を始めるかは、ipに設定しとく事になってた。 ここでは、ipの初期値が示されていないけど、0番地から実行するように、設定 してある。

実行の主体ルーチンは、78行目から定義してある。ぐるぐると何回もこのルーチンを 呼び出すので、再帰のシンボルrecが付いてる。

ルーチンの中で一番最初の確認は、停止(halt)フラグが立っているかどうかだ。haltは、 別の所(devices)で、特殊IO命令を実行した時セットされるようになってる。

停止じゃ無かったら一命令実行する。それには、ipの示す番地から命令を取り出し(フェッチ と言ったような)、それを解析(デコード)する。この流れが、80行目から、112行目 までのmatchで実現されている。

それぞれの項の矢印(ハスケルさんは、括弧よくアロー、アローって騒ぐみたいだけど)の 右側は、各命令の実行部分だ。命令実行の共通部分をうまくくくり出して、マクロ みたいにしてある。このあたり、ocamlの特性を旨く使ってますなあ。超感心!!!

一つ命令を実行したら、matchを抜けて、113行目に移り、Ipをインクリメントしてから 次を実行する為、またexecを呼び出す(再起)する。これがあらまし。

昔々は、これをハードで実現してたのよ。ミニコンの回路図を引っ張り出してきて、 タイミングチャートを描きながら、解析したものだ。(と、遠い目)

上記を見ると、命令は、オペコードが0のnop(何もしない)から始まって、30のdevicesって いうIO命令、112行目のアンダーバーは、それ以外に分類されてる。(設計されてる) 全体の機構は、IO空間がメモリー空間と分離されたインテル風味でいいのかな。

なお、それ以外は、call命令と解釈される。本来、call命令は、どこを呼び出すかって オペランドが必要になるんだけど、どうせ煩雑にcallが表れるんなら、オペランド (呼び出し先)で、call命令って分かるんじゃねぇ。そうそれば、無駄なcallって opを省けるし、サイクル数も少なく済むな。しめしめって訳。

無駄話が長くなったな。早速、debuggerを使って細かく追ってみるか。昔のミニコンにも ステップ実行なんてスイッチが付いていたな。ああ、懐かしい。(さっきから、これ ばっかりだな。年寄りになった証拠だぞ)

emacsのtuaregモード上から、M-x ocamldebug すると2つの質問が出てくるので、返答する。

Run ocamldebug on file: ~/src/ml/z

OCaml debugguer to run: ocamldebug

画面が割れるんで、ソース画面側のif行の所にカーソルを合わせて、C-x C-a C-b して ブレークする場所を指定。次に、debugger側の画面に移動して、C-c C-r で、プログラムを 走らせる。

        Objective Caml Debugger version 3.12.1

(ocd) Loading program... done.
Breakpoint 1 at 14868 : file z.ml, line 79, characters 25-1359
(ocd) Time : 322378 - pc : 14868 - module Z
Breakpoint : 1
(ocd) Time : 322379 - pc : 14888 - module Z
(ocd) Time : 322380 - pc : 15376 - module Z
(ocd) Time : 322382 - pc : 15392 - module Z
(ocd) Time : 322383 - pc : 16480 - module Z
(ocd) Time : 322384 - pc : 16500 - module Z
(ocd) Time : 322385 - pc : 16516 - module Z
(ocd) Time : 322386 - pc : 16544 - module Z
(ocd) Time : 322387 - pc : 16572 - module Z
(ocd) Time : 322388 - pc : 16600 - module Z
(ocd) Time : 322389 - pc : 19116 - module Z
(ocd) Time : 322404 - pc : 19380 - module Std_exit

ステップ実行してくと、一命令実行して、終了しちゃったよ。誤動作してんな。 こういう時は、見通しよいコードを書いてみるに限る。

実験基板作成

上のコードを抜き出し出来ればいいんだろうけど、いろいろなしがらみがあるので 下記のような模造品を作ってみた。

[sakae@pcbsd ~/src/ml]$ cat test.ml

type sig_t = Red | Yellow | Blue ;;

let s = Yellow ;;
let halt = false ;;

let rec exec () =
     if not halt then begin
        match s with
              Red -> 3.44
            | Yellow -> 123.0
            | Blue -> 3.2 ;
        print_string "HellowWorld\n" ;
        exec ();
     end else 1.2 ;;

exec ();;

丁度、例の本を読んでる時、バリアント型なんてのが出てきたので、試してみつつ。。。 信号は、赤・黄色・青から出来ています。そしてその信号が黄色になりました。

黄色を評価したら、123.0です。それから、挨拶して、以下繰り返ししましょってコードです。 これを書いていて、初めて知ったんだけど、match式は、そこぞれの項が返す型が、全て 同じじゃないと、エラーに落とされるのね。こういう制限は、if式だけかと思っていたよ。

で、実行してみると、挨拶無く終了しちゃったぞ。

val exec : unit -> float = <fun>
#   - : float = 123.

match式の後ろに式を書いても 実行してくれないものなの? よう分からんな。大いに参考にさせてもらっている OCaml備忘録 のパターンマッチの所に、こんな説明が有った。

パターンマッチはfunctionキーワードと一緒に使うときには省略記法がある。
これは一般には次のような形になる。

function | p1 -> expr1 
         | p2 -> expr2
         | p3 -> expr3
         ...
         | pn -> exprn

この例は次のように使う。

let func1 = function
 | 1 -> print_endline "a"
 | _ -> print_endline "b";;
let _ =
 func1 1;
 func1 2;;

使い方を限定しているのだろうか? まあいい、悩んでいてもしょうがないんで、 そういうものと受け取って、回避策を考えよう。

let rec exec () =
     if not halt then
         let dummy = match s with
            | Red -> 3.44
            | Yellow -> 123.0
            | Blue -> 3.2 in
        print_string "HellowWorld\n" ;
        exec () ;;

こうしてあげたら、挨拶が続くようになったよ。上のfunctionの使い方は、制御構造、 おいらが発見した方法は、式の値の取得って事で、match(ifも)は、二面性を持って いるんだな。あれ? lispのcond式も同じだったかな。

随分長くなってきたので、今日はここまで。