今日から OCaml 学習帳

OCaml は半年に一度くらい触ることになってるので、ここは今日から OCaml 学習帳になります。たぶん明日くらいにやめてます。(ちなみにここまで書いたのは一昨日なので既に終わってるような)

で、なんとなく麻雀の上がり判定とか書いてました。こんなの。

http://shinh.skr.jp/tmp/agari.ml

OCaml は毎度毎度いいものだなぁと思いつつまぁ色々不満もあるのですが、そのへんも含めて考えたことの記録みたいな感じでー。

書き終わってから思ったんですが、これ一体誰に向けた文章なんだろうか…個人的に面白いと思った部分をこんな感じですよーっていうのと、ここが気にいらんってのが混じってるようなそんな文章です。

ゲーム脳と OO 脳は不治の病なのでとりあえず OO は使わん方向で考えることになっています。型は全部 type 予約語でやるんだけど、やり方によって役割変わりまくりというか。

(* typedef *)
type pai = int
(* enum *)
type tsuhai = E | S | W | N | H | T | C
(* union *)
type pai_expr = Man of int | Pin of int | Sou of int | Tsu of tsuhai
(* struct *)
type agari = {
  yakus : yaku list;
  han : int;
  hu : int;
  score : int
}

となんか似たような構文で全然違うもの作るんだなぁ…と。ていうか type pai = int は消しても動いたっていうか途中で型変更したから意味なくなってたぽい。まぁ enum とか union とか言っても大部違うんだけど。

enum

なんか enum みたいなヤツは、 match っていうスーパー switch 文みたいなヤツで使うと、全部のケースがチェックされてるかどうかコンパイラがチェックしてくれてこれが便利なのですが、弱点があって int 値として使えないというか連番機能として使えないというか。

type yaku = TSUMO | RICHI | IPPATSU | PINHU | TANYAO | IPEKO | HAKU | HATSU

こーんな型があったら

let yaku_name = [| "ツモ"; "リーチ"; ... |]

みたいな配列使って yaku_name.[yaku] とかでアクセスしたいんですなー。んでなんか探したら Obj.magic とかいうのが使えるようですがいかにも使うな危険な物体ですがあんま気にしない方針で。

type tsuhai = E | S | W | N | H | T | C
let tsuhai_name = [|
  "東"; "南"; "西"; "北"; "白"; "發"; "中"
|]
let _ =
  print_string tsuhai_name.(Obj.magic E);
  print_newline ()

てな感じでー。ていうかなんかどうすりゃいいんですか?と 偉いかた に聞いたところ Obj.magic しか無いべーどうせ実装1つしか無いしいいんじゃーん、みたいなありがたいお話でしたのでまぁこれで行こうかと。

なんか上のコードはこういきなり色々言語要素が急に出てきたような。

配列とめんどいオペレータ

配列は [| |] でリストは [ ] です。配列にランダムアクセスする時は .() で文字列は .[] で要するに覚えられないし書きにくいし見た目もイマイチなのでこれは非常にイケてないのですが、型推論のためなのでしゃーないというか。

もっと致命的にうざいのはかの float 型専用の演算子 +. -. *. /. ですが、これには解決法があります。 float 使うプログラムは OCaml で書かないことです。同様に Python は class を作らないことによって self. 問題を解決できます。

_

main はなんか _ に代入するような感じでやるんが一般的みたいですねなんか。別に let main = とかでもいいっちゃいいし、これは単なる値の代入してるだけなんですが。

どうでもいいけど _ は関数名に使えないみたいなのです。

いやどうでもよくなくて、なんか関数型言語では関数内関数にいいかげんな名前をつけたくて、名前思いつかん場合に _ とか使いたくてしょうがないんですが使えません。かなしいので適当に関数名にシングルクォートつけた関数名を使ったりしてみてますがどうもキモい。

これにも解決法がありまして、コンパイラをいじれば…と思ってソースいじったんですがなんかよくわからないのでやめました。何も解決してない気がするし。

let 系

んで let はなんか二つの用途に使える感じで、変数束縛と関数定義です。引数が無い場合は変数束縛になる。

let tsuhai_name = [|
  "東"; "南"; "西"; "北"; "白"; "發"; "中"
|]

とかがそれ。引数は括弧とかつけずにコンマとかも無しで空白でくぎってならべる。呼ぶ時も同じような感じ。

let add x y = x + y
let _ = print_int (add 1 2)

とかそんなの。最後に評価された値が返り値になります。括弧とかコンマとかが無い理由はたぶんカリー化が見やすいからだと思います。2引数関数を1引数だけ与えてやると1引数関数に変化するっていうアレですね。

let add x y = x + y
let add2 = add 2
let _ = print_int (add2 3)

再帰させたい関数は let rec で始めますが、これがもう絶対忘れます。

let hoge () = hoge ()

とかコンパイルすると Unbound value hoge とかいう非常に意味のわからんエラーメッセージが出てやる気なくなりポイントの1つな気がします。だいたい再帰スキーな関数型なのになんでそんなめんどいの自動で適当になんかやってよとか思うんですけどなんか必要なんですかね。

unit

さらりと重要な物体が出てきたんですが、なんもしないプログラムを書くのは hello world より難しいみたいな話があった気がしますが OCaml は () でも書いときゃいいです。これは要するに void というかそんなのみたいです。 unit 型というらしいです。

let func = other_func 2

とかするとなんか func が変数になっちゃうんで、

let func () = other_func 2

とかしておきましょーという話。なんもしないプログラムは

let _ = ()

手続き脳

; で区切ると普通に次の行に行きます。なんというか副作用アリアリなのでとても嬉しいです。あとなんか ref ってついた変数つくと代入ができる変数になるので、変数に代入しないと生きていけない人も生きていけます。

let _ =
  let x = ref 0 in
  print_int !x; print_newline ();
  x := 1;
  print_int !x; print_newline ();
  ()

なんか ! とかつけないと値が見れないんですがこんくらいなら許容範囲。まぁあんま使わん方がいいんだろうけど。でも最終兵器があるというかね。 goto がある安心感みたいな、そんな。あ、あとなんか関数内で let した場合はおしりに in をつけましょーという(ザツ)。

あと手続き脳に嬉しいお知らせですが printf がありますとか。

Printf.printf "%d\n" 3

なんかこれどう見てもちっとも型に厳密な雰囲気じゃないわけでして、これの実装どうなってるねんというと Obj.magic が使われてました。 Obj.magic は enum の値を取るものじゃなくてん任意の型にキャストするためのものみたいなそんな感じのすばらしいものなんですよつまり。

union

なんかやっと union の話なのか。なんか enum の方と似たような定義するわりには割と別の物体というか。要は C でよくやる

struct VALUE {
  int type;
  union {
    int i;
    float f;
    char* s;
  } val;
}

みたいなのを両方一気に定義できちゃう、みたいなそんなノリ。上のコードはスクリプト言語の型情報つきの変数みたいなものの定義のつもりです。んでこれもスーパー switch 文と言ってた match 文で漏れがあったらエラー出してくれるので割とハッピー。

type value = Int of int | Str of string
let print_value v =
  match v with
  | Int i -> Printf.printf "int %d\n" i
  | Str s -> Printf.printf "str %s\n" s
let _ =
  let iv = Int 2
  and sv = Str "hoge" in
  print_value iv;
  print_value sv

これはなんか適当にサンプルコードとか書いてみた次第です。えーとつまりなんか値取り出す時は match 文使わんと取り出せないわけですな。 C の上記のコードなんかを思い返すと、 switch (type) した中でしか val は使わない気がするので、非常に合理的というか。なるほどなあと。あとなんかパターンマッチは when とか使うとさらになんか色々便利だったり。

んでコンストラクタとかいうらしいんですが、 Int 2 とか Str "hgoe" とかすると value 型が作れると。ここで重大な問題がありましてですね、 Int とか Str は関数として扱えないらしくて、なんかとても不便な感じなのですけどねこれ。なんじゃこれ。

option 型

union といえば(ていうか union じゃなくて variant 型とか言うらしいですが)、なんか option 型とかいうのが組み込まれてて、なにやらこれが便利で、えーと無効な値を表現できるとかそんな物体で。

let div x y =
  if y = 0 then
    None
  else
    Some (x / y)

let _ =
  match div 10 2 with
  | None -> print_string "invalid\n"
  | Some v -> Printf.printf "%d\n" v;
  match div 10 0 with
  | None -> print_string "invalid\n"
  | Some v -> Printf.printf "%d\n" v;

実行すると 5 invalid と表示されるわけですね。

飽きてきた

飽きてきたし record とかどうでもいいです。あれは struct 。たぶん。あとリストとかあった気がするけど別にいいやみたいな。大人気言語の Haskell にもあるしね。 [] とか :: とかが重要ですとか。

ていうか今気付いたけど一個上のサンプルに始めて if が出てきたような。 = と <> で比較すると値比較で、 == と != だと参照比較になるとか。 == で毎度ハマります。

リスト

パターンマッチとリストは重要なのだった。 C++ の for_each みたいなのを書くとして、

let rec for_each l f =
  match l with
  | [] -> ()
  | h :: t ->
      f h;
      for_each t f

let _ =
  for_each ["hoge"; "hage"; "hige"] print_endline

なんかこんなん。パターンマッチはハイパー switch なのでなんか色々できるわけです。空リストが引数の場合はなんもしないというのが以下の部分。

  | [] -> ()

次の h :: t にマッチするとーって部分はなんか、 h は head で t は tail ですよつまり。 :: は例えば int と [int] を連結するような表記で、まぁカラリストじゃない場合は h に先頭要素が入って、残りのリストが t に入るようになってるとかもう説明だるいこんな丁寧に書くんじゃなくて自分メモのつもりだったのに。

  | h :: t ->
      f h;
      for_each t f

あとさっき for_each にまた rec 書き忘れた。めんどい。

末尾再帰

なんか関数の最後で再帰呼び出しすると勝手にループになるらしいですよ。まぁ適当にデクリメントしながら画面に表示する関数を書く。

let rec print_decrement x =
  if x = 0 then
    ()
  else (
    print_int x;
    print_decrement (x-1);
   )

let _ =
  print_decrement 100000;
  print_newline ();

ここでポイントは else の後に ( を書くと手続き脳の世界に行けるというこないだ教えてもらったことなのですがそれはともかく。

080497b0 <camlTail__print_decrement_57>:
 80497b0:       83 ec 04                sub    $0x4,%esp
 80497b3:       83 f8 01                cmp    $0x1,%eax
 80497b6:       75 18                   jne    80497d0 <camlTail__print_decremen
t_57+0x20>
 80497b8:       b8 01 00 00 00          mov    $0x1,%eax
 80497bd:       83 c4 04                add    $0x4,%esp
 80497c0:       c3                      ret
 80497c1:       eb 0d                   jmp    80497d0 <camlTail__print_decremen
t_57+0x20>
 80497c3:       90                      nop
 80497c4:       90                      nop
 80497c5:       90                      nop
 80497c6:       90                      nop
 80497c7:       90                      nop
 80497c8:       90                      nop
 80497c9:       90                      nop
 80497ca:       90                      nop
 80497cb:       90                      nop
 80497cc:       90                      nop
 80497cd:       90                      nop
 80497ce:       90                      nop
 80497cf:       90                      nop
 80497d0:       89 04 24                mov    %eax,(%esp)
 80497d3:       e8 28 07 00 00          call   8049f00 <camlPervasives__string_o
f_int_153>
 80497d8:       89 c3                   mov    %eax,%ebx
 80497da:       a1 18 a6 05 08          mov    0x805a618,%eax
 80497df:       e8 9c 08 00 00          call   804a080 <camlPervasives__output_s
tring_214>
 80497e4:       8b 04 24                mov    (%esp),%eax
 80497e7:       83 c0 fe                add    $0xfffffffe,%eax
 80497ea:       eb c7                   jmp    80497b3 <camlTail__print_decremen
t_57+0x3>
 80497ec:       8d 74 26 00             lea    0x0(%esi),%esi

んーなんでそんな nop はさまってるねんと思いつつも、まぁ call print_decrement みたいなのが無いのは確認できますねーよかたですねー。んで print_decrement => print_int と呼び出し順を変えてやると、

 80497d6:       e8 d5 ff ff ff          call   80497b0 <camlTail__print_decrement_57>

とかが現れやがりまして、ああなるほど再帰しちゃってるのうと。実際実行してやるとスタックオーバーフローするわけですよ。

でもですね。ぶっちゃけ末尾かどうかなんて気にするのめんどいのでなんかどうでもいいというかですね。私はシグナルハンドラの中でなんでもやっちゃいたい人なんですよとかそういう話。

そんなこと気にしてたら LL の舞台には上がれませんよ。ていうか OCaml はどう見ても LL じゃないと思います。だって print_string とか print_int とか + とか +. とか .() とか .[] とかなんか気軽感がどこにもないですよ。あえて言うなら型書かなくていいという話ですけど、それなら C も LL ですな。

あ、ちなみに 新しい言語を学ぶときはアセンブリを出力しなさいというお告げ があったのでなんとなく objdump したとかそういう複雑な経緯がありまして。

型推論

そういや型はあるけど書かなくていいです。なんかわけのわからんエラーが出たら書くといい気がします。

let rec print_decrement (x : int) : unit =

とか書くと型を明示的に書けます。

そういえば型推論ある言語ってリファクタリングしやすくないですか。関数の引数の型をイチイチ変えていく手間が無いし、でも型エラーはキチンと出るし。つまり OCaml は XP 向け言語ということになります。

Emacs

型推論といえば emacs の caml-mode は型を表示する機能とかあるらしいですよ奥さん。これは別な偉い人 (woさん) に教えてもらいました。 ocamlc -dtypes hoge.ml とかすると hoge.annot っていうファイルが出力されるのですが、そのファイルがあると、 caml-types-show-type というコマンドで今カーソルがある式の型が表示されるます。

なんか私は tuareg モードとかいう OCaml のモードを使ってたらしいんですが、これ相当の機能が無いみたいなのでさっき乗り換えました。ただ一つ問題点がありまして、 raise っていう例外を投げる関数の色がコメントと同じ色なんですよ、これはどう考えてもおかしいとしか言いようがないので、適当に修正しました。 caml-font.el を

;   '("\\<raise\\>" . font-lock-comment-face)
   '("\\<raise\\>" . font-lock-reference-face)

とか。

コメント

コメントの話が出たので、最後はコメントです。 (* と *) で囲んだものはコメントで、ネスト可能です。複数行もオッケーです。ですがとてもウザい問題がありまして、1行コメントが無いという問題があります。 camlp4 …は私の理解ではこれできんと思いますが調べてないのでよく知りませんので cpp とか通せばいいんじゃねとか思ったりしてます。

でもこれには解決法がありまして、

i@u ~/src/ocaml-3.09.2> diff -u parsing/lexer.mll.orig parsing/lexer.mll
--- parsing/lexer.mll.orig      2005-04-12 01:44:26.000000000 +0900
+++ parsing/lexer.mll   2006-09-17 21:56:42.000000000 +0900
@@ -310,6 +310,7 @@
         let esc = String.sub l 1 (String.length l - 1) in
         raise (Error(Illegal_escape esc, Location.curr lexbuf))
       }
+  | "--" { comment_line lexbuf; token lexbuf }
   | "(*"
       { comment_start_loc := [Location.curr lexbuf];
         comment lexbuf;
@@ -396,6 +397,10 @@
                      Location.curr lexbuf))
       }

+and comment_line = parse
+    newline { update_loc lexbuf None 1 false 0 }
+  | _ { comment_line lexbuf }
+
 and comment = parse
     "(*"
       { comment_start_loc := (Location.curr lexbuf) :: !comment_start_loc;
なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h