ふるつき

私は素直に思ったことを書いてるけど、上から目線だって言われる

OCaml入門 ひとりAdvent Calendar

この記事は、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 のインストール

opamOCaml のパッケージマネージャであり、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 は多分、emacsocaml-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-modeemacs の補完ライブラリです。 auto-complete というのもあるみたいですが時勢がこっちに傾いているっぽさがある。

company-mode は ELPA で提供されていて、 emacs では M-xpackage-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.printfHello 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_allstdinを渡すのもできるようになってくれる。

引数をとって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 をうまくまとめる方法がわからなかったのと stdinclose_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

いろいろ新しく触った部分があるので紹介していく、ます。

|>

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

(...) の構文糖衣。こっちのほうがみためがよさそうなので使ってます。基本的にOcamlLispというか一つの式しかかけないので、() でくくって ; でつなげて、無理くり一つの式にしてあげる。 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なところをやりたいですね。

今日の目標

昨日のライフゲームを、GUIなViewerに移したい。

lablgtk のインストール

OCamlgtkバインディングのようなものだと思います。 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 というツールを使ってみることにします。おまけ。

omakeopam install omake で入れて、プロジェクトのディレクトリで omake --install とすると OMakerootOMakefile が生成されます。

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[] = lablgtk2FILES[] = lifegame_guiPROGRAM = 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-targetsomake が生成するターゲットのリストを返してくれるらしいので、よくわからない加工をして、 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 に相当するものが欲しくなりますね。あと fieldref になっちゃったのでキモい

昨日のライフゲームとくっつける。

まずは lifegame.ml をカレントにコピーしてきます。 main はきっと邪魔になるので削除しました。

OMakefileFILES[]

FILES[] =
    lifegame
    lifegame_gui

に更新します。

そして、 lifegame_gui.ml の先頭に open Lifegame と書けば準備は完了で、あとは timer_funcfield の更新行を 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にも慣れないのですごい時間取られた……。手抜きのつもりだったのに。

f:id:Furutsuki:20171219212504g:plain

OCaml入門 ひとりAdventCalendar 6日目

そろそろレコードとか出てきてほしいんですが使う機械がないですね……ということは触らなくてもいいということ……。

今日の目標

昨日でてきた `BLACK `WHITE の正体を探っていきたいですね。これだけだと内容が少ないようなら、よく知らないOCamlのクラスシステムにふれていきたいところです 。

`BLACK とは何か

調べたところ、「多相バリアント」らしいです。「バリアント」ってカタカナで書くのが無限にダサいので、以後は "polymorphic variant" と書きます。

variant

polymorphic でない variant もあって、これは既に使っていました。type lifegame_cell = Live | Dead がそうです。 LiveDead が 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_eventkeyboard_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 で受け止めて投げ直すコードで関数全部をまるっと括ってやるとかになるんですかね。

あーあと相互再帰がありました。これは踏んだときになんでエラーになるのかわからないコンパイルエラーが出て困りましたね。

parseparse_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 を使って書かざるを得ませんでした。ちなみに accaccumlator とか accumlation の略です。

Hashtbl に map が入ってないのは型が ('a, 'b) -> 'c -> ('a , 'b) Hashtbl.t -> 'c list みたいになって(Hashtblがレシーバなのにlistで返すところが)気持ち悪いからですかね。

次何すればいいかな……