この記事は、OCaml入門 ひとりAdventCalendar 1日目の記事ですが、そんなAdventCalendarはなくって、AdventCalendarの季節にかこつけてOCamlの勉強をしてみるだけです。こんな記事がn個もブログにはびこるのもどうかと思うけど公開しないのも勿体無いみたいな気持ちの折衷案としてこの記事に全部収める。
思い立って OCaml に触れてみることにしました。当方C言語からプログラミングの世界に入って未だに初心者をやっています。
この Advent Calendar は次のような環境でお送りします。
~> cat /etc/os-release NAME="Ubuntu" VERSION="17.10 (Artful Aardvark)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 17.10" VERSION_ID="17.10" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=artful UBUNTU_CODENAME=artful
今日の目標
今日は OCaml を取り巻くエコシステムを構築していきたいです。最後にHello worldを書いて締めます。ちなみにいまのところ7日目まで書きましたが、すでにエディタ周りの環境はVimになりました……。
opam のインストール
opam は OCaml のパッケージマネージャであり、Environment Managerっぽいです。Ubuntu 17.10 では apt
を使って入れられるので楽そうです。
~> sudo apt install opam -y
よくわからないけど、 opam init
しておいたほうが良さそうな風潮があるので、 opam init
します。
~> opam init
すると $HOME の下に .opam
ディレクトリが作成されそうです。 Do you want OPAM to modify ~/.zshrc and ~/.ocamlinit?
と訪ねてくるので快く y
と答えておきます(n
と答えても opam config setup -a
で同様のことができそうというきもちになりました)。
eval `opam config env`
してね。と言われるのでしておきます。
環境の準備
opam が依存しているので ocaml コンパイラは既にインストールされていそうですが、なんか分けといたほうがためになりそうなので分けます。これには opam switch
を使いそうなんで opam switch list
で利用可能なコンパイラの一覧を見て後、一番新しそうなのを入れます。
ここで普通にバージョン番号を指定するとそのバージョン番号の環境が作成されると思ってるんですが、 名前を指定して環境をつくることも当然できるっぽいです。
ここでは adc という名前で環境を作ることにしました。
~> opam switch install adc --alias-of 4.06.0
まあこれはそれなりに時間がかかります。
終わると 例によって例のごとく eval `opam config env`
してくれといわれるのですると、環境は adc になってます。
utop の導入
今でも ocaml
と打てばREPLが起動しますが、もう少しリッチな utop を導入しておきます。 opam install utop
でインストールできて慣習が生きてます。
~> opam install utop
emacsのインストール
血迷ってしまったので emacs
の環境をつくっていきます。まずはインストールからやね。 apt install emacs -y
です。
~> sudo apt install emacs -y
tuaregの導入
tuareg は多分、emacsに ocaml-mode をもたらしてくれる偉い子だとおもいます。私は「つあれぐ」って読んでますけど読み方わからん。インストールは opam を使ってやっていきます。 melpa と marmalade にもあるけど古いかもねって github が言ってます。
~> opam install tuareg
インストール後のメッセージに、「これこれこうしてくれ」という指示がありますが無視します。なぜなら、 opam user-setup install
という必殺技があるからです。
これをやると、初回は必要なパッケージのインストールが行われ、後にエディタの設定ファイルをいじってくれます。
merlinの導入
merlin (私は親しみを込めて「めるりん」と呼ぶことにしました)は、 tuareg と一緒に使って、「補完」「型の表示」をしてくれそうな感じがします。
これも opam で入れましょう。
~> opam install merlin
ついでに opam user-setup install
もしておきます。
company-mode の導入のための el-get の導入
company-mode は emacs の補完ライブラリです。 auto-complete というのもあるみたいですが時勢がこっちに傾いているっぽさがある。
company-mode は ELPA で提供されていて、 emacs では M-x
の package-install <RET> company-mode <RET>
とかでインストールできそうなのですが、ちょっとこれはあんまり好きくない感じなので、 ついでということでemacsのパッケージマネージャの一つ el-get
を使ってみることにしました。 el-get
を選んだ理由はなんとなくです。
というわけで、 el-get
をインストールしていきます。
el-get
のインストールをするために emacs の設定ファイルであるところの ~/.emacs
を編集していきます。user-setup
が作ってくれた ~/.emacs
が転がっていたので、これをいじります。いろいろ既に書いてあって触りたくない感じなので、これの上の方に必要な設定を書いていきます。
とはいえ、 el-get の README から引っ張ってくるだけなのでかんたんです。
次の行を足しておきました。
(add-to-list 'load-path "~/.emacs.d/el-get/el-get") (unless (require 'el-get nil 'noerror) (with-current-buffer (url-retrieve-synchronously "https://raw.githubusercontent.com/dimitri/el-get/master/el-get-install.el") (goto-char (point-max)) (eval-print-last-sexp))) (add-to-list 'el-get-recipe-path "~/.emacs.d/el-get-user/recipes") (el-get 'sync)
この状態で、 C-x C-s
で保存して、 M-x eval-buffer
としてあげると、勝手に el-get
をインストールしてくれます。そんなきがする。
これが終わったら↑の下にもう一行追加します。 company-mode のインストールです。
(el-get-bundle company-mode/company-mode)
やはりこれも保存して、 eval-buffer
します。さらに、もう一文、company-modeを自動で有効化する設定を追加します。
(add-hook 'after-init-hook 'global-company-mode)
で、 実は company-mode を入れたのは merlin のためなので、 merlin と連携させます。 ここ にあったコードを ~/.emacs
に足します。
; Make company aware of merlin (with-eval-after-load 'company (add-to-list 'company-backends 'merlin-company-backend)) ; Enable company on merlin managed buffers ; (add-hook 'merlin-mode-hook 'company-mode) ; Or enable it globally: ; (add-hook 'after-init-hook 'global-company-mode)
hook は既に行っているのでコメントアウトしておきました。
これで多分設定が完了しました。
Hello worldを書く
C-x f
して適当な hello.ml
ファイルを開きます。モード表示が (Tuareg utop Merlin company)
になっているといい感じです。
はじめは Printf.printf
でHello Worldしていたのですが、 kosen14s の面々に、「標準は print_string
じゃないかなあ」という指摘を受けたので、 print_string
で書きます。
let () = print_string "Hello World\n"
print_string
だと恩恵が感じられなくて寂しいですが、 Printf.
まで打つと、補完の候補が表示されると思います。うれしいね。
実行する
実は main を実行するようなコマンドはないので、二行目を選択して、 C-x C-r
などしましょう。これは utop-eval-region
のショートカットで、 utop repl が開いて、選択した部分を実行して結果を返してくれます。
utop[0]> print_string "Hello World\n";; Hello World - : unit = ()
Hello World ができました。
やる気次第で、 2日目につづきます。
OCaml入門 ひとりAdventCalendar 2日目
昨日が楽しかったので今日もやります。環境構築って一番楽しい時間だと思うのね。
今日の目標
FizzBuzz を書きたい
自然な感情が生まれました。
試しにFizzBuzzを書いてみると、こうなりそうです。
let fizzbuzz = function |x when x mod 15 == 0 -> "FizzBuzz" |x when x mod 3 == 0 -> "Fizz" |x when x mod 5 == 0 -> "Buzz" |x -> string_of_int x let main = for i=1 to 100 do print_string ((fizzbuzz i) ^ "\n") done
雰囲気で読んでください。 ^
は文字列の結合を行う(たぶん)関数です。 Pervasives
といういつでも読み込まれている標準のモジュールで定義されてたはず。ぺるばしぶす。
けどまあ、forを使ってるのがOCamlっぽくない気がするので、forみたいな感じでリストを返す関数 range
を作ってみます。
let fizzbuzz = function |x when x mod 15 == 0 -> "FizzBuzz" |x when x mod 3 == 0 -> "Fizz" |x when x mod 5 == 0 -> "Buzz" |x -> string_of_int x let rec range a b = if a > b then [] else a :: range (a+1) b let main = List.iter (fun x -> (print_string ((fizzbuzz x)^"\n"))) (range 1 100)
書きましたけど、 List.iter に渡してる関数が汚いですね……。
こういうときは関数合成を使ってあげるのがいい気がするんですが、OCamlって関数合成の演算子ないんですね。なんでだ。
ということで定義します。F# にならって、記号 >>
を使います。
let (>>) f g x = g (f x)
中置記号は ()
でくくって let
してあげて、これを使うとFizzBuzzがもうちょっときれいになるはず。
let fizzbuzz = function |x when x mod 15 == 0 -> "FizzBuzz" |x when x mod 3 == 0 -> "Fizz" |x when x mod 5 == 0 -> "Buzz" |x -> string_of_int x let rec range a b = if a > b then [] else a :: range (a+1) b let (>>) f g x = g (f x) let print_string_line s = print_string (s ^ "\n") let main = List.iter (fizzbuzz >> print_string_line) (range 1 100)
print_string
して改行もする print_string_line
つくっちゃった。まあいいよね。 (fizzbuzz >> (fun x -> x ^ "\n") >> print_string)
って書いても同じことになりそう。あとで見たら、 print_endline
という関数がありましたね……。
ちなみに >>
が三引数取りそうなのに二引数でもエラーになってないのは、カリー化というやつです。僕はなんとなく知ってた。
OCaml入門 ひとりAdventCalendar 3日目
飽きてきた。ちなみに 7日目までしかやってないので。
今日の目標
cat
を書きたい。にゃーん。
出来心で man cat
したら普通にオプションとかあってびっくりしてる、ます。
オプションの実装はちょっと面倒なのでやりません。というか cat
の実装くらい知っててもいいかもなぁという気持ちが強くなってきたぞ。
適当に読んだ一行を吐く
こういうのは調べるだけでできます。
let main = print_endline @@ read_line()
emacs 上の utop で入力する方法がわからなかったので ocaml cat.ml
とかします。 @@
は 開き括弧のようなものです。 read_line は引数を持たないので、呼び出すときには ()
をつけるらしいです。
いつまでも読んで吐く
let main = while true do print_endline @@ read_line() done
なんだか当然のように while
があって、これで無限に読んでは吐けます。ただ、 EOF
に達すると End_of_file
例外を吐きます。
ので、例外処理をする
let main = try while true do print_endline @@ read_line() done with End_of_file -> ()
try-with
で例外を捕まえられそうです。捕まえたあとは何もしないので ()
。
ファイルを読む
次はファイルを読みたいので読みます。ファイルを開くのは open_in
で、これは当然のように読み込み用途でファイルを開きます。
これを let
で束縛して、 input_line
やなんかの引数として使えそうです。例えばこう。
let main = let chan = open_in "cat.ml" in try while true do print_endline @@ (input_line chan) done with End_of_file -> ()
関数にまとめる
あんまりきれいじゃない気がするけど echo_all
とか適当な名前をつけてまとめます。
let echo_all chan = try while true do print_endline @@ (input_line chan) done with End_of_file -> () let main = let chan = open_in "cat.ml" in echo_all chan
こうすると echo_all
に stdin
を渡すのもできるようになってくれる。
引数をとってcatする
コマンドライン引数は Sys.argv
で取れそう。これを for
で回します。 iter
できれいにかけそうなの思いつかなかったので。
let echo_all chan = try while true do print_endline @@ (input_line chan) done with End_of_file -> () let main = for i = 1 to Array.length Sys.argv - 1 do let chan = open_in Sys.argv.(i) in echo_all chan; close_in chan done
二文以上書くときは ;
でつなげるとか普通は知らないですよね。怒られた。配列のランダムアクセスが .(n)
とか知らないですよね。困った。
完成させる
はい。
let echo_all chan = try while true do print_endline @@ (input_line chan) done with End_of_file -> () let main = if Array.length Sys.argv == 1 then echo_all stdin else for i = 1 to Array.length Sys.argv - 1 do if Sys.argv.(i) = "-" then echo_all stdin else let chan = open_in Sys.argv.(i) in echo_all chan; close_in chan done
こう、
let chan = match Sys.argv.(i) with |"-" -> stdin |f -> open_in f
みたいに書いて echo_all chan
一個にまとめたかったんですが、 match
式と let in
をうまくまとめる方法がわからなかったのと stdin
を close_in
すると怒られるのとで諦めました。
実行可能な形式にする
ocamlc
は バイトコードへのコンパイラで、 ocamlopt
はネイティブコードへのコンパイラっぽいです。
ocamlopt cat.ml -o cat
とかすると、 cat
が生成されて、いい感じに動いてくれそうですね。
OCaml入門 ひとりAdventCalendar 4日目
惰性がもーちょい続いてほしい。
今日はライフゲームを実装した
やりたくなったからやったけど、時間の都合でちまちま記事を書いてないので、成果物だけ貼ることになる。
type lifegame_cell = Live | Dead type lifegame_type = lifegame_cell array array let lifegame_construct size = Array.make_matrix size size Dead let random_choice xs = let len = Array.length xs in xs.(Random.int len) let lifegame_initialize size = let field = lifegame_construct size in begin for y=0 to size-1 do for x=0 to size-1 do field.(y).(x) <- random_choice [|Live; Dead|] done done; field end let lifegame_cell_string = function | Dead -> "□" | Live -> "■" let lifegame_at field x y = let len = Array.length field in field.((y+len) mod len).((x + len) mod len) let lifegame_cell_update field x y = let envcount = ref 0 in begin for i = -1 to 1 do for j = -1 to 1 do if not (i == 0 && j == 0) then if lifegame_at field (x+j) (y+i) == Live then envcount := !envcount + 1 done done; match (lifegame_at field x y, !envcount) with |(Dead, 3) | (Live, 2) | (Live, 3) -> Live |_ -> Dead end let lifegame_update field = let size = Array.length field in let new_field = lifegame_construct size in begin for y=0 to size-1 do for x=0 to size-1 do new_field.(y).(x) <- lifegame_cell_update field x y done done; new_field end let lifegame_print field = Array.iter (fun row -> Array.map lifegame_cell_string row |> Array.to_list |> String.concat "" |> print_endline) field let main = begin Random.self_init(); let turn = ref 1 in let field = ref (lifegame_initialize 10) in while true do print_string "\027c"; lifegame_print !field; print_endline ("turn:"^string_of_int !turn); turn := !turn + 1; field := lifegame_update !field; Unix.sleep 1; done end
(後日追記)
lifegame_cell_update
関数の実装を間違えています。インデントが深くなっている if lifegame_at field (x+j) (y+i) == Live then
の部分は lifegame_at field (y+i) (x+j)
と呼び出すべきところでした。これのせいで↓のGIFも途中ですぼんでしまうライフゲームになりました。
これは座標をまとめて一つのtypeにするとかで対処できるところだったと思うんですが、じゃあどこまではPosition型でどこからはint->int型でというのは難しいですよね……。
(追記ここまで)
いろいろ新しく触った部分があるので紹介していく、ます。
|>
F# で大人気のパイプライン演算子です。 f a
って言う関数適用を a |> f
って書けるようにする演算子で、実装は let (|>) a f = f a
とかだと思う。中置記号強いね。
これがあると流れるプログラムが書ける。UFCSと似たりよったり。
type
type
を使うと独自型を定義できる。特定のデータ構造に依るプログラムを書くときは定義したほうがバグを生みにくそうだし読みやすそう。 0, 1って書くよりも Live
, Dead
って書いたほうがわかるという enum みたいな type と、
type lifegame_type = lifegame_cell array array
っていう typedef
みたいな type とがあった。
ref
OCamlは純粋じゃないから、 mutable な変数を作れて、それを作るときには let x = ref v
ってする。値を参照するときは !x
で、何かを代入するときは :=
を使ってあげる。
begin ... end
(...)
の構文糖衣。こっちのほうがみためがよさそうなので使ってます。基本的にOcamlはLispというか一つの式しかかけないので、()
でくくって ;
でつなげて、無理くり一つの式にしてあげる。 Lisp 系の do
とか begin
と同じだね。ということは do ... done
は暗黙の begin
のような気がしてきた。
<-
Array は実は mutable なデータ構造で、任意のデータを書き換えられるんだけど xs.set i v
ってする代わりに xs.(i) <- v
って書いてもいいそうです。
print_string "\027c"
画面のクリア。 printf "\x1bc"
みたいなことをします。OCamlでは '\xxx
で任意の文字コードを持つ文字を書ける(必ず三桁。そして10進)。
だいたいこんな感じです。スリープのために Unix.sleep
を使ったから、実行とかコンパイルのときは ocaml unix.cma lifegame.ml
ってする。順番を遵守しないといけない……。
ところで lifegame_cell_update
の match 式の部分、最初は
match (lifegame_at field x y, !envcount) with |(a,b) when a == Dead && b == 3 -> Live |(a,b) when a == Live && (b == 2 || b == 3) -> Live |(a,b) when a == Live && b <= 1 -> Dead |(a,b) when a == Live && b >= 4 -> Dead |_ -> Dead
って超絶まどろっこしく書いてたけど、 shinkwhek くんのおかげで今のように短く読みやすくなりました。感謝 :pray:
OCaml入門 ひとりAdventCalendar 5日目
そろそろOCamlのObjectiveなところをやりたいですね。
今日の目標
lablgtk のインストール
OCamlのgtkバインディングのようなものだと思います。 GTK2 に対応。GTK3に対応してくれ。
普通に opam install lablgtk
したら失敗するので、 sudo apt install libgtk2.0-dev
してから opam install lablgtk
したら成功しました。
ただのWindowの表示
とにかくWindowを表示します。
let main = begin let _ = GtkMain.Main.init () in let window = GWindow.window ()in window#show(); GMain.Main.main() end
サードパーティのライブラリを使ってるので、愚直に ocaml lifegame_gui.ml
としてもうまく動きません。そこで、
ocamlfind ocamlc -package lablgtk2 -linkpkg lifegame_gui.ml
とします。 ocamlfind ocamlc
はサードパーティのライブラリを使うときには定番のコンパイル方法だってチュートリアルさんが言ってました。
-package <name>
オプションで利用するパッケージ(ここでは lablegtk2
)を指定して、 -linkpkg
でパッケージをちゃんとリンクしてくれるようにお願いします。これで a.out
を吐いてくれるので、実行してみると、Windowがでます。ぺけを叩くとWindowは消えますが、プログラムは終了しないので C-c
で止めます。
ビルドの自動化
毎回 ocamlfind ...
を叩くのはしんどい気がするので、今のうちにまとめてしまいます。これくらいならシェルスクリプトにぺってするのが一番なんですが、入門ということもあるので、OCamlistic な方法を取っていきたいです。
標準では ocamlbuild
というコマンドがあるみたいですが、なんかそりが泡なさそうな気がしたので、サードパーティの、 omake
というツールを使ってみることにします。おまけ。
omake
を opam install omake
で入れて、プロジェクトのディレクトリで omake --install
とすると OMakeroot
と OMakefile
が生成されます。
OMakeroot
は基本的にいじらなくていいらしいので OMakefile
を今回のプロジェクトっぽく編集したいですね。
デフォルトでは160行にも及ぶ長いコメントが書いてあるので、これをいい感じにしていきます。
感覚で適当にやるとこうなりました
# This project requires ocamlfind (default - false). USE_OCAMLFIND = true OCAMLPACKS[] = lablgtk2 if $(not $(OCAMLFIND_EXISTS)) eprintln(This project requires ocamlfind, but is was not found.) eprintln(You need to install ocamlfind and run "omake --configure".) exit 1 # # Include path # # OCAMLINCLUDES += NATIVE_ENABLED = $(OCAMLOPT_EXISTS) BYTE_ENABLED = $(not $(OCAMLOPT_EXISTS)) ################################################ # Build an OCaml program FILES[] = lifegame_gui PROGRAM = lifegame_gui .DEFAULT: $(OCamlProgram $(PROGRAM), $(FILES))
私が編集したのは、 OCAMLPACKS[] = lablgtk2
と FILES[] = lifegame_gui
、 PROGRAM = lifegame_gui
の部分だけです。
これで omake
してみます。ファイルがどががっと生成されて、その中には lifegame_gui
もありました。最高。ちゃんと動きました。
自動実行とかclean とか
make clean
とか make run
的なことがしたいですよね。調子に乗って書きます。
……。一瞬で書けてしまった……。
.PHONY: clean, run clean: rm $(filter-proper-targets $(ls R, .)) run: .DEFAULT ./$(PROGRAM)
filter-proper-targets
は omake
が生成するターゲットのリストを返してくれるらしいので、よくわからない加工をして、 rm
すれば clean
が完成します。
run
は .DEFAULT
呼んでプログラムをビルドしたあとに ./$(PROGRAM)
ってして成果物を実行します。いい感じですね。
さらにやばいことに -p
オプションをつけて omake -p run
とするか、 -P
オプションでもいいですが、ファイルを監視して更新されたら run
してくれます。多分だけど -P
はコンパイルエラーしてもめげない。
Window の破棄と同時に終了
window#connect#destroy ~callback:GMain.Main.quit;
を追加します。
矩形を書く
ライフゲームのために、矩形を書きたいです。
結構苦労しながら、書くことができました。
let draw_rect drawing_area _ = begin let drawing = ( drawing_area#misc#realize(); new GDraw.drawable (drawing_area#misc#window) ) in drawing#set_foreground `BLACK; drawing#rectangle ~x:0 ~y:0 ~width:100 ~height:100 ~filled:true (); true end let main = begin let _ = GtkMain.Main.init () in let width = 640 in let height = 480 in let window = GWindow.window ~width:width ~height:height ~title:"Lifegame" ()in let drawing_area = GMisc.drawing_area ~width:width ~height:height ~packing:window#add () in drawing_area#misc#set_double_buffered true; drawing_area#event#connect#expose (draw_rect drawing_area); window#show(); window#connect#destroy ~callback:GMain.Main.quit; GMain.Main.main() end
とりあえずWindowの大きさなどを変更しています。 ~width:width
と書いてあるのは、ラベル付きのオプション引数 width
に 変数 width
を渡している図です。
描画は、 drawing_area
というものを作って、これを window
に追加します(これは ~paking:window#add
でやってそう)、それから expose
にコールバックを登録して、exposeのタイミングで描画を行います。 expose
は再描画要求を受け付けたときに呼び出されて、 GdkEvent.Expose.t
というよくわからない型を受けて bool
を返す関数をコールバック引数に取りますが、このよくわからないイベント型は使ってないので draw_rect
の第二引数にダミーの _
をいれてあります。
あとはわかりそう。 new
は他の言語で言うところの new
です。こいつは関数じゃなさそうだよね。
個人的に新しかったのは let
の右辺に (...)
って二式以上を書くやつ。考えてみればできるだろうけどなるほどなって感じになってしまった。
あ、 set_foreground
の引数は全然わかってないです `BLACK
を `WHITE
にしたらたしかに白で描画されるんだけどこれなに……。
↓こんな感じになります
ライフゲームの描画っぽくする
ががっとこういう関数を書きます
let draw_lifegame_field drawing_area field _ ?(cell_size=5) = begin let drawing = ( drawing_area#misc#realize(); new GDraw.drawable (drawing_area#misc#window) ) in let size = Array.length field in begin for y=0 to size-1 do for x=0 to size-1 do drawing#set_foreground `BLACK; drawing#rectangle ~x:(cell_size*x) ~y:(cell_size*y) ~width:cell_size ~height:cell_size ~filled:false (); if field.(y).(x) = Live then drawing#set_foreground `BLACK else drawing#set_foreground `WHITE; drawing#rectangle ~x:(cell_size*x+1) ~y:(cell_size*y+1) ~width:(cell_size-1) ~height:(cell_size-1) ~filled:true (); done done; true end end
かなり読みにくいですがやってることは素直なのでよめる。 cell_size
はラベル付きオプション引数にしてみました。
定期的に再描画する
探したら、多分こういうのは timeout
を使えば行けそうということがわかりました。
GMain.Timeout.add ~ms:1000 ~callback:(timer_func drawing_area field);
こういう行を追加しておいて、 timer_func
の中身はこんな感じです。
let timer_func drawing_area field = fun () -> begin field := (lifegame_initialize 10); draw_lifegame_field drawing_area !field ~cell_size:10; true end
Timeout.add
のコールバックは ()->bool
型になるので、そうなるようなクロージャを作って返しています。
なんか気持ち悪くなってきたので、 draw_lifegame_field
の _
になってる引数を消し飛ばして、 expose
にはこんな感じ (fun _ -> draw_lifegame_field drawing_area !field ~cell_size:10);
で渡しておきます。
それにしても、 timer_func
から直接 draw_lifegame_field
を呼び出してるの気持ち悪い……。 repaint
に相当するものが欲しくなりますね。あと field
が ref
になっちゃったのでキモい
昨日のライフゲームとくっつける。
まずは lifegame.ml
をカレントにコピーしてきます。 main
はきっと邪魔になるので削除しました。
OMakefile
の FILES[]
を
FILES[] = lifegame lifegame_gui
に更新します。
そして、 lifegame_gui.ml
の先頭に open Lifegame
と書けば準備は完了で、あとは timer_func
の field
の更新行を field := update_field !field
と書けば終わりです。
最終的にこうなった
open Lifegame let lifegame_cell_color = function | Live -> `BLACK | Dead -> `WHITE let draw_lifegame_field drawing_area field ?(cell_size=5) = begin let drawing = ( drawing_area#misc#realize(); new GDraw.drawable (drawing_area#misc#window) ) in let size = Array.length field in begin for y=0 to size-1 do for x=0 to size-1 do drawing#set_foreground `BLACK; drawing#rectangle ~x:(cell_size*x) ~y:(cell_size*y) ~width:cell_size ~height:cell_size ~filled:false (); drawing#set_foreground (lifegame_cell_color field.(y).(x)); drawing#rectangle ~x:(cell_size*x+1) ~y:(cell_size*y+1) ~width:(cell_size-1) ~height:(cell_size-1) ~filled:true (); done done end end let update_draw drawing_area field = begin field := lifegame_update !field; draw_lifegame_field drawing_area !field ~cell_size:10 end let main = begin Random.self_init(); let field = ref(lifegame_initialize 10) in let _ = GtkMain.Main.init () in let width = 640 in let height = 480 in let window = GWindow.window ~width:width ~height:height ~title:"Lifegame" () in let drawing_area = GMisc.drawing_area ~width:width ~height:height ~packing:window#add () in drawing_area#misc#set_double_buffered true; GMain.Timeout.add ~ms:100 ~callback:(fun _ -> update_draw drawing_area field; true); drawing_area#event#connect#expose (fun _ -> draw_lifegame_field drawing_area !field ~cell_size:10; true); window#show(); window#connect#destroy ~callback:GMain.Main.quit; GMain.Main.main(); end
rectangle
のあたりで横に長い、 50行のプログラムができました。ロジックは昨日出来上がっていたので、行数としては短いです。
OCamlにもGtkにも慣れないのですごい時間取られた……。手抜きのつもりだったのに。
OCaml入門 ひとりAdventCalendar 6日目
そろそろレコードとか出てきてほしいんですが使う機械がないですね……ということは触らなくてもいいということ……。
今日の目標
昨日でてきた `BLACK
`WHITE
の正体を探っていきたいですね。これだけだと内容が少ないようなら、よく知らないOCamlのクラスシステムにふれていきたいところです 。
`BLACK とは何か
調べたところ、「多相バリアント」らしいです。「バリアント」ってカタカナで書くのが無限にダサいので、以後は "polymorphic variant" と書きます。
variant
polymorphic でない variant もあって、これは既に使っていました。type lifegame_cell = Live | Dead
がそうです。 Live
や Dead
が variantで lifegame_cell
は variant type です。
ここまで知って、variant って enum みたいなものか、と思ったのですが、曰く
バリアントは、C でいうところの enum 及び union を複合したもの
ということです。 variant は値を持つこともできて、有名な例が option
です。
option
option は None
または値を持つことができる variant type で、
type norec 'a option = None | Some of 'a
と定義されています。これは OCaml Toplevel で調べた。 'a
は型変数で、任意の型になりそうです。'a option
は適当な型を引数としてとって int option
とか string option
みたいな型になる。
polymorphic variant
ですが、勉強したところ、なんとなく何かはわかったし必要になる場面が出てきそうということもわかったのですが、説明をしろといわれると途端に難易度がEclipseなので
http://osiire.hatenablog.com/entry/20090510/1241957550
あたりを読んで勉強するのが楽だよということだけメモしておきます。個人的には mouse_event
と keyboard_event
の統合のところが分かりやすかった。
拡張しやすさを残したり、あるいは明らかに他のところでもおんなじようにこの概念つかいそう、というときは polymorphic variant にしておけばいいんですかね……。
これってある種の interface
のようなものかな。
昨日サボりそこねたのできょうはこんな感じでおわりです。
実は記事のストックは明日までしかなくて実質入門してない
OCaml入門 ひとりAdventCalendar 7日目
今日の目標
だんだんStep-by-Stepで進めなくなってるので、今日の目標というか「今日の成果物」になりつつありますね……。
ということで、簡易jsonパーサを書きました。ちゃんとしたやつじゃないです。
少し前に、まだ一週間経ってないくらいですが、D言語でjsonのパーサを書きました。ちゃんとしたやつじゃないです。ので、OCamlでも書いてみようという気持ちになったので、D言語で書いたのと同じような仕様のやつを書きました。色々足りてなくて、 1e10
とかもちろんパースできないし、 "\uxxxx"
もだめです。 "\n"
は読めるけど "\t"
は実装してません。
D言語だと、2時間半とすこしかけて https://github.com/theoldmoon0602/jparse を書いたんですが、OCaml だと更に1時間くらいかかりましたかね……。
今回のコードは https://github.com/theoldmoon0602/jparse-ocaml にあります。
コード中のいろいろ
jparse.ml
の一番最初に、 json_type
を定義しています。これは、そのまま json を表すための型で、 variant がすごく生きてる気がします。
D言語では 型を表す enum と 値を表す union を使いましたが、OCamlでは、すっと短く、スマートにjson型を表すことができました。
続く parse_info
は初登場のレコードです。名前付きタプルみたいなもの、あるいは Cのstruct
のようなものと認識してます。 i
はどこまで読んだかを表すポインタで、 mutable
な値なので info.i <- info.i+1
としてるコードが散見されます。関数型でプログラミングするならここは immutable
で毎度新しい parse_info
を作ったりするんでしょうが、明らかに面倒なのにバグが減る嬉しい作用もなさそうなのでこうしました。
更に続いて exception
です。OCamlでは例外を自分でぱぱっと定義してしまうのが良さげのようなのでやりました。
あとはだいたい素直です。エラー処理がいちいち可読性を下げてる気しかしません。特に文字列の範囲外エラーはめっちゃ気を使って色んな所に同じようなコードを書く羽目になったので、もっとまとめたいです。例外が出てから try with
で受け止めて投げ直すコードで関数全部をまるっと括ってやるとかになるんですかね。
あーあと相互再帰がありました。これは踏んだときになんでエラーになるのかわからないコンパイルエラーが出て困りましたね。
parse
と parse_array
parse_object
は相互に呼び出し合う関係なのですが、こういうときは
let rec A = ... and B = ...
と特別な書き方をしないとだめで萎え萎えです。まあいいか。
それから、これは jparse_main.ml
の方ですが、何故かOCamlのHashtblには iter
はあるのに map
がなく、自分で
let hashtbl_map f tbl = Hashtbl.fold (fun k v acc -> (f k v) :: acc) tbl []
と fold
を使って書かざるを得ませんでした。ちなみに acc
は accumlator
とか accumlation
の略です。
Hashtbl に map が入ってないのは型が ('a, 'b) -> 'c -> ('a , 'b) Hashtbl.t -> 'c list
みたいになって(Hashtblがレシーバなのにlistで返すところが)気持ち悪いからですかね。
次何すればいいかな……