|ハイブリッドOS|File System|ARM|Android|Java|制御システム|オープンシステム

 

技術者コラム

 
フォーム
 
補足:storyboardを使って表示するViewを切り替える
 
第23回目:OCamlでへびゲームを作る(Part5)
2014-07-20
筆者:村田
 
こんにちは。
 
ゲームループを作るところから始めます。ここで作るループとは「入力、状態更新、画面出力」を繰り返すことです。関数型言語ですので再帰ループで書いていきましょう。OCamlでの再帰関数の書き出しはlet recですね。
 
let rec game_loop = ...
 
最初は入力処理です。Part3で作成したキーボード入力から移動方向を取り出すget_move関数を使います。
 
let rec game_loop =
  let move = get_move() in
  ...
 
次にPart4で作成したupdate_state関数を呼び出して、へびやリンゴの状態を更新します。この関数にはへびの位置とリンゴの位置を渡してあげる必要があります。これらの状態はループする度に変わるので、game_loopの引数にしましょう。
 
let rec game_loop snake apple =
  let move = get_move() in
  let (snake’, apple’) = update_state move snake apple in
  ...
 
update_state関数によって更新されたへび(snake’)とリンゴ(apple’)をタプルで受け取りました。次は画面表示です。Part2~3で作成したshow_game関数を使って新しいへびとリンゴを表示させます。
 
let rec game_loop snake apple =
  let move = get_move() in
  let (snake’, apple’) = update_state move snake apple in
  show_game apple’ snake’;
  ...
 
む、show_game関数の引数の順番がイマイチですね。ちょっと残念ですがこのままにします。では新しいへびとリンゴの状態を引数にして再帰ループさせましょう。
 
let rec game_loop snake apple =
  let move = get_move() in
  let (snake’, apple’) = update_state move snake apple in
  show_game apple’ snake’;
  game_loop snake’ apple’;;
 
さて再帰呼び出しのポイントは「再帰呼び出しの度に引数が変わること」、それと「停止すること」ですね。game_loopはこのままだと停止しません。どうやって停止させましょう。ゲームオーバーになったらループを停止させれば良いですね。ではへびゲーのゲームオーバーはどうやって判定しましょう。まず、へびの頭がゲーム盤の端に触れてしまったらゲームオーバーになります。また、へびの頭が自分の胴体に触れてしまってもゲームオーバーになります。ではこの判定関数を作成しましょう。へびの位置を引数にもらいます。
 
let is_gameover snake = ...
 
まずはへびの頭がゲーム盤の端に触れていないかチェックします。今回もへびの頭(x,y)をパターンマッチで取り出しましょう。
 
let is_gameover snake =
  match snake with
    [] -> false
  | ((x,y) :: _) -> x = 0 || x = 6 || y = 0 || y = 6;;
 
x座標が0か6、またはy座標が0か6となった時はゲーム盤の端に触れているのでゲームオーバーです。直値で書いたのでイマイチですが、ゲーム盤表示を行うshow_board関数も5x5マス固定になっているのでまあいいかな。次にへびの頭が胴体に触れているかをチェックします。これはList.mem関数を使ってリストの先頭要素(へびの頭)がリストの残り要素(胴体)に含まれているかを調べれば分かります。上記関数ではへびの胴体をワイルドカードパターンにマッチさせて捨てていましたが、チェックに使うのでbody変数に束縛しましょう。また先頭要素は一段深いところの(x,y)にまで分解しちゃっています。このままでも「List.mem (x,y) body」という風にチェックすることはできます。しかしclojureのas節のように、OCamlでもasパターンを使うと分配束縛しつつ元の構造にも束縛することができます。適当にsnake変数を定義してasパターンを実験してみましょう。
 
# let snake = [(1,1); (2,2); (3,3)];;
val snake : (int * int) list = [(1, 1); (2, 2); (3, 3)]
 
# match snake with
    [] -> (0,0)
  | ((x,y) as head :: _) -> head;;
- : int * int = (1, 1)
 
snakeリストの先頭要素(1,1)を(x,y)と分配束縛しつつ、headにも束縛できていますね。ではasパターンをis_gameover関数に使ってみます。
 
let is_gameover snake =
  match snake with
    [] -> false
  | ((x,y) as head :: body) ->
      x = 0 || x = 6 || y = 0 || y = 6 || List.mem head body;;
 
List.mem head bodyで頭が胴体に触れているかをチェックしています。このままでもいいですが、「引数のパターンマッチ」をOCamlで書く場合にはfunction式を使うことも考えてみましょう。スッキリ書けることがあります。
 
let is_gameover = function
    [] -> false
  | ((x,y) as head :: body) ->
      x = 0 || x = 6 || y = 0 || y = 6 || List.mem head body;;
 
ではis_gameoverを試してみます。まずは頭がゲーム盤の端にある場合です。
# is_gameover [(2,6); (2,5); (2,4)];;
- : bool = true
 
次は頭が胴体に触れてしまう場合です。
# is_gameover [(2,3); (2,2); (2,3); (3,3)];;
- : bool = true
 
うまくいってそうなのでgame_loop関数に組み入れます。ゲームオーバーの時は画面にへびの長さを表示することにしました。
 
let rec game_loop snake apple =
  let move = get_move() in
  let (snake', apple') = update_state move snake apple in
  show_game apple' snake';
  if is_gameover snake' then
    print_endline ("Game over snake length:" ^
                   string_of_int (List.length snake'))
  else
    game_loop snake' apple';;
 
もう少しで完成です。残りのステップはゲームループをメイン関数から呼び出すところです。えいやと書き出してみます。
 
let () =
  game_loop snake apple;;
 
snakeとapple変数は初期値が必要ですね。へびは(1,1)からスタートさせます。リンゴはランダムに出現させます。ところでランダム関数は使う前にシード(種)を与えるべきですね。OCamlのライブラリを改めて確認するとRandom.self_init関数でシードを初期化できますので、これも最初に呼び出しておきましょう。
 
let () =
  Random.self_init();
  let snake = [(1,1)] in
  let apple = new_apple snake in
  game_loop snake apple;;
 
コンパイルして試したところ、バグがありました。game_loopはキーボード入力から始まるため、その前に最初のゲーム盤状態を画面に出しておく必要があります。修正して完成です。
 
let () =
  Random.self_init();                                                
  let snake = [(1,1)] in
  let apple = new_apple snake in
  show_game apple snake;
  game_loop snake apple;;
 
できた〜。コンパイルしてちょっと遊んでみましょう。
$ocamlopt SnakeGame.ml
$./a.out
 
+-----+
|     |
|     |
| ****|
|  O *|
|   **|
+-----+
 
上記はへびが長さ7まで成長できたところです。w,a,s,dを入力してEnterを押すという操作にやっぱり違和感がありますが、慣れてしまえばゲーム自体はすごく簡単です^^/
 
[おわりに]
OCamlはいかがでしたか?関数型言語では扱いが面倒な入出力や逐次実行を素直に書くことができるので、Haskellよりも取っ付きやすいと思います。逆に気軽に副作用を導入できるということは、関数型プログラミングを意識して書かないと手続き中心になってしまう可能性もあるかな〜と思います。このあたりは同じハイブリッド言語であるScalaでも起きるようです。Scalaを使って関数型プログラミングで書くためには勉強・教育が必要なんだとか。その点Haskellは意識しなくても関数型プログラミングを強制される感じがします^^; それと今回は扱いませんでしたが、OCamlの多相型システムについてはもう少し勉強し、機会があったら書いてみたいと思います。
 
それではPart5までお付き合い頂き有り難うございました。ちょっと長いですが今回のコード全体を載せて終わりにいたします。
 
 
第22回目:OCamlでへびゲームを作る(Part4)
2014-07-13
筆者:村田
 
こんにちは。
 
今回はへびを動かすところから始めましょう。ちょっと復習です。へびは位置のリストで表していますので、例えば・・・
 
+----+
|*   |
|*   |
|**  |
|    |
|    |
+----+
 
という上記盤面のへびは[(2,3); (1,3); (1,2); (1,1)]と表されます。頭は(2,3)です。さて、このへびが下に移動すると・・・
 
+----+
|    |
|*   |
|**  |
| *  |
|    |
+----+
 
上記のようになります。おお!どちらもテトリミノの形してますが、テトリスじゃなくへびゲーですよ。んで、移動後のへびをリストで表すと[(2,4); (2,3); (1,3); (1,2)]となります。移動前のへびの頭は(2,3)でしたので、一つ下に移動するとY座標が+1されて(2,4)になります。次に胴体部分(1,3)は一つ右に移動するのでX座標が+1されて(2,3)になります。このように残りの胴体部分を順番に移動して新しいリストを作ればへび全体を移動することができます。
 
各胴体は頭に引っ張られて移動します。そのため上下左右どちらに移動するかを計算するためには、一つ前の胴体がどこにあったかを覚えて・・・って面倒ですね。先程の移動前のへびリストと移動後のへびリストをずらして並べてみましょう。
 
移動前:      (2,3) (1,3) (1,2) (1,1)
移動後:(2,4) (2,3) (1,3) (1,2)
 
アルゴリズムが見えてきますね。頭を(2,3)から(2,4)に移動して元のリストの先頭に加えて、尻尾を一つ取り除けば移動したことになります。まずは移動したい方向に頭を追加する関数を作りましょう。関数名はadd_headで引数は移動方向とへび、戻り値は新しい頭を付けたへびを返します。移動方向は前回定義したmove型(Up,Down,Right,Left)を使います。というわけで書き出しはこんな感じ。
 
let add_head move snake = ...
 
次に移動対象となるへびの頭を取り出しましょう。へびはリストですのでList.hd関数を使ってList.hd snakeとすれば頭のタプルを取り出せます。ですが、前回扱ったパターンマッチを使って取り出してみることにしましょう。
 
let add_head move snake =
  match snake with
    [] -> []
  | (head :: body) -> ...
 
パターンマッチの注意点その1を覚えていますか?パターンに漏れが無いように!です。まあ漏れていたらコンパイラが教えてくれます。というわけでリストをパターン分けましょう。3行目は空リストの場合です。なおゲーム開始時に必ず頭を1つ持つのでへびが空リストになることはあり得ません。しかし、add_head関数はそんなこと分かりませんので空リストのパターンも正しく書いてあげる必要があります。そして、4行目の(head::body)パターンで頭と胴体を変数に束縛しています。おさらいになりますが、間のコロン2つはコンスを表し、headには先頭要素が束縛され、bodyには残りのリストが束縛されるのでした。ゲーム初期状態の頭だけのへび[(1,1)]を渡すとheadには(1,1)が束縛されます。ではbodyには?・・はい、空リスト[]が束縛されます。
 
それと、へびの頭のx,y座標を計算に使う必要がありそうなので、headをさらに細かくパターンマッチしておきましょう。あとbody部分は使わないので、変数の代わりにワイルドカードパターン_(アンダースコア)にしましょう。ワイルドカードパターンにマッチした部分は後から参照できません。パターンのごみ捨て場ですね。というわけで以下のようになります。
 
let add_head move snake =
  match snake with
    [] -> []
  | ((x,y) :: _) -> ...
 
次に、移動方向moveを見て新しい頭の座標を作ります。Upならy座標を-1、Downならy座標を+1ですね。moveによる場合分けはif式でできます。なお恒等検査は=でできます。
 
if move = Up then
...
else if move = Down then
...
 
しかし、ここもパターンマッチを使った方がすっきり書けます。こんな感じですね。
 
let add_head move snake =
  match snake with
    [] -> []
  | ((x,y) :: _) -> match move with
                      Up -> (x,y-1) ...
                    | Down -> (x,y+1) ...
 
それでは新しい頭を既存のへびリストの先頭にくっつけて完成です。
 
let add_head move snake =
  match snake with
    [] -> []
  | ((x, y) :: _) ->
    match move with
      Up    -> (x,y-1) :: snake
    | Down  -> (x,y+1) :: snake
    | Right -> (x+1,y) :: snake
    | Left  -> (x-1,y) :: snake;;
 
そして尻尾を一つ切り落とす関数も必要ですね。リストの尻尾を一つ切り落とす標準関数はないかな〜とListモジュールを探してみましたが見つかりませんでした。自作することにします。再帰ループやforループ式で書くこともできますが、さっきListモジュールを眺めていたら便利な関数があったので、関数の組み合わせで書けそうです。それに関数の組み合わせで書いた方が関数型プログラミングっぽくなりそう:) 使えそうな関数はList.tl関数とList.rev関数です。List.tl関数はリストの「頭を除いた残り部分」を取り出します。(”tl”の読み方はトルじゃなく、テイル(tail)です)
 
# List.tl [1; 2; 3];;
- : int list = [2; 3]
 
もう一つの関数、List.rev関数はリストを逆順にします。
 
# List.rev [1; 2; 3];;
- : int list = [3; 2; 1]
 
さて、関数の組み合わせが浮かんできませんか?
 
こんな風にしたらどうでしょう。まずリストを逆順にしてから頭を除くことで尻尾を切ったことになりますね。それからもう一度逆順にして向きを戻します。試してみましょう。
 
# List.rev (List.tl (List.rev [1; 2; 3]));;
- : int list = [1; 2]
 
尻尾切り落とせましたね^^b さてこれも関数にしておきましょうか。
 
let drop_tail snake =
  List.rev (List.tl (List.rev snake));;
 
試してみましょう。
 
# drop_tail [1;2;3];;
- : int list = [1; 2]
 
うまくいきましたが、空リストを渡すと・・
 
# drop_tail [];;
Exception: Failure "tl".
 
『全域関数と部分関数』
List.tl関数でエラーです。空リストの頭を除くなんてできないからですね。どうしましょう。前述の通り、へびの仕様として空リストはあり得ないので、この問題をほっておいてもいいのですが、パターンマッチで空リストかどうかチェックすることで堅牢な関数にできます。なお、どんな引数でもちゃんと値を返してくれる関数を全域関数(total function)と言います。逆に今回の空リストのように引数によっては値を返してくれず、例外が発生したり、処理が終わらなくなってしまう関数のことを部分関数(partial function)と言います。ではdrop_tailを全域関数にしてみましょう。
 
let drop_tail = function
    [] -> [];
  | snake -> List.rev (List.tl (List.rev snake));;
 
前回登場させたfunction式を使ってみました。空リストのときは空リストを返すだけです。試してみましょう。
 
# drop_tail [];;
- : 'a list = []
 
うまくいきましたね。次はadd_head関数とdrop_tail関数を組み合わせて、へびを移動させるmove_snake関数を作ります。
 
let move_snake move snake =
  drop_tail (add_head move snake);;
 
では試してみましょう。まずは適当にへびを定義して・・・
 
# let snake = [(2,3); (1,3); (1,2); (1,1)];;
val snake : (int * int) list = [(2, 3); (1, 3); (1, 2); (1, 1)]
 
move_snake関数を使ってみましょう。
 
# move_snake Down snake;;
- : (int * int) list = [(2, 4); (2, 3); (1, 3); (1, 2)]
 
OKOKうまく移動してくれました。次はどの部分に取りかかろうかな。へびはリンゴを食べると尻尾が伸びるのでした。これも難しくありません。へびがリンゴを食べるのは移動したときです。何もしないでいきなりリンゴを口にすることはできないのです。そんなにこの世界は甘くないのです。んで、先程作ったへびの移動関数は最後に尻尾を切っていますね。この関数をちょっと直して、移動した先にリンゴがあった場合は、尻尾を切らないようにすれば以下のように尻尾が伸びたように見えます。
 
移動前:      (2,3) (1,3) (1,2) (1,1)
移動後:(2,4) (2,3) (1,3) (1,2) (1,1)
 
では、へびの頭がリンゴを口にしたかどうかをtrue/false判定する関数を作りましょう。is_eated関数はリンゴの位置とへびリストを引数にします。snakeの頭(head)とリンゴの位置が一致するかどうかをtrue/falseで返します。それと空リストにも対応して全域関数にしています。
 
let is_eated apple snake =
  match snake with
    [] -> false
  | (head :: body) -> head = apple;;
 
# is_eated (1,1) [(1,1); (1,2); (1,3)];;
- : bool = true
# is_eated (2,2) [(1,1); (1,2); (1,3)];;
- : bool = false
 
テストもOKです。ではこれをmove_snake関数に組み込みましょう。move_snake関数の引数を増やしてリンゴの位置も教えてあげます。う〜ん、move_snakeの引数にappleか〜、あんまりいい設計じゃない気もしますが、いっちゃえ。
 
let move_snake move snake apple = ...
 
まずは、普通に頭を伸ばしましょう。頭を伸ばした新しいへびはsnake’にローカル束縛します。
 
let move_snake move snake apple =
  let snake’ = add_head move snake in
  ...
 
頭を伸ばしたへびがリンゴを食べたかどうかで、尻尾を切り落とすかを決めます。
 
let move_snake move snake apple =
  let snake' = add_head move snake in
  if is_eated apple snake' then
    snake'
  else
    drop_tail snake';;
 
REPLで試してみましょう。snake変数はさっきの定義が残っているものとします。へびの移動先(2,4)にリンゴを置いておきます。
 
# move_snake Down snake (2,4);;
- : (int * int) list = [(2, 4); (2, 3); (1, 3); (1, 2); (1, 1)]
 
OK。ちゃんとへびがリンゴを食べて成長しています。リンゴを食べられない位置に変えると・・・
 
# move_snake Down snake (2,5);;
- : (int * int) list = [(2, 4); (2, 3); (1, 3); (1, 2)]
 
こちらもOK。ちゃんとへびが移動しています。次はどうしましょうかね。リンゴを食べちゃったら新しいリンゴをランダムに出現させることにしよう。ランダムな値を作る便利な関数ないかな〜とライブラリを探すと、ありました。Random.int関数です。Random.intに5を渡すと0〜4をランダムに返してくれます。引数が同じでも結果が違うので副作用関数でありますな。Haskellではちょっと面倒なRandom処理ですが、OCamlでは副作用が書きやすくて簡単に使えます。
 
# Random.int 5;;
- : int = 4
# Random.int 5;;
- : int = 0
# Random.int 5;;
- : int = 2
 
これで新しいリンゴを生成するnew_apple関数が書けそうです。ただし、既にへびの頭や胴体がある部分にリンゴは出現できません。引数にsnakeを渡してもらってへびの位置をチェックしましょう。もし新しいリンゴがへびの位置が被ったら、ループしてもう一度リンゴを作り直しましょう。再帰ループを書くのでlet recから始めますよ。
 
let rec new_apple snake =
  let x = (Random.int 5) + 1 and
      y = (Random.int 5) + 1 in
  if List.mem (x,y) snake then
    new_apple snake
  else
    (x,y);;
 
一気に書きました。2,3行目で新しいx,y座標を作り出しています。+1しているのは0〜4のランダム値を1〜5に変えるためですね。あ、List.mem関数(member)を説明していませんでした。第1引数の要素が第2引数のリストに含まれていればtrueを返す関数です。ちょっと試してみますか。
 
# List.mem 2 [1; 2; 3];;
- : bool = true
# List.mem 0 [1; 2; 3];;
- : bool = false
 
そんなわけで(x,y)というリンゴがsnakeリストに含まれる場合はnew_appleを再帰呼び出しして作り直します。それではnew_apple関数を試してみます。snake変数は前に定義したものを使い回しています。
 
# snake;;
- : (int * int) list = [(2, 3); (1, 3); (1, 2); (1, 1)]
# new_apple snake;;
- : int * int = (2, 5)
# new_apple snake;;
- : int * int = (1, 5)
 
リンゴを作れました。ではリンゴを食べちゃったら新しいリンゴを作りましょう。リンゴを食べたかどうか分かるポイントってmove_snake関数の中か・・・う〜ん、move_snakeの中でリンゴを作る関数を呼び出すのか〜・・ますます関数名と処理が乖離してきたな。こいつはちょっとリファクタリングした方が良さそうですね。へびの位置状態やリンゴの位置状態が変わる関数・・要はゲームの状態が変わる関数なんですよね。うん、関数名を変えるだけでいいかな:P move_snake関数改めupdate_state関数とします。リンゴを食べるかどうか判定するところまでは同じです。
 
let update_state move snake apple =
  let snake' = add_head move snake in
  if is_eated apple snake' then
    snake'
  ...
 
リンゴを食べちゃったなら、新しいリンゴを作って返しましょう。新しいへびと新しいリンゴをタプルにしてセットで返します。
 
let update_state move snake apple =
  let snake' = add_head move snake in
  if is_eated apple snake' then
    (snake', new_apple snake')
 else
  ...
 
elseの方はどうしましょう。リンゴを食べれなかったので、へびの余分な尻尾を切り、りんごの位置は変化しません。
 
let update_state move snake apple =
  let snake' = add_head move snake in
  if is_eated apple snake' then
    (snake', new_apple snake')
  else
    (drop_tail snake', apple);;
 
試してみましょう。snakeは定義済みですね。へびの頭の下(2,4)にリンゴを置いておき、へびを移動させて食べさせます。
 
# snake;;
- : (int * int) list = [(2, 3); (1, 3); (1, 2); (1, 1)]
# update_state Down snake (2,4);;
- : (int * int) list * (int * int) =
([(2, 4); (2, 3); (1, 3); (1, 2); (1, 1)], (1, 5))
 
へびは伸び、新しいリンゴ(1,5)も返ってきていますので成功です。
 
『ゲームの状態の扱い』
それにしてもこの関数は引数が多いですね。他の言語ではmove,snake,appleといったゲームの中心となる状態はグローバル変数に置くか、変更可能オブジェクト(構造体やクラスのインスタンス)にまとめて、それらを関数から参照したいところですよね。しかし、関数型プログラミングではグローバル変数も変更可能オブジェクトも使えません。状態はすべからく引数で渡す必要があり、また新しい状態は戻り値で返すしかないのです。したがって、複雑なゲームを関数型プログラミングで作る場合は、状態引数が増加し手に負えなくなってきます。ではゲームは関数型プログラミングに向いていないのでしょうか?そんなことはありま温泉。いやオブジェクト指向言語の方が楽でしょうけど、なんとかなるさ!HaskellではStateモナドという仕組みを使って、今回のように状態を引数に羅列しなくても、まるで「グローバル領域がある」かのように、状態の取り出しや格納が行えます。いつか紹介できればいいなと思います。OCamlではオブジェクトにしてもいいし、グローバル変数も使えますのでゲーム状態を関数引数からひっぺがすことは雑作もないことです。ただまあ、今回はゲームの状態を変える関数がupdate_stateだけですし、関数型っぽくいきたいのでこのままにしておこうと思います。
 
さてと、ゲームの中心ロジックもできました。表示関数や入力関数も前回作ったし、あとは「入力〜ロジック〜表示」のゲームループを作るだけですが、今週はこの辺で。
 
以上
 
第21回目:OCamlでへびゲームを作る(Part3)
2014-07-06
筆者:村田
 
こんにちは。
 
突然ですがC Magazineって雑誌をご存知ですか?1989〜2006年まで刊行されていたプログラミング雑誌です。2005年頃、書店で眺めたり何度か試しに買ってみたりもしたのですが、何だかカッコいい!けど難しくてよく分からん!って感じで残念ながら継続購読には至らなかった記憶があります。あれから年月が経ち、先日Amazonで面白い本ないかな〜と探していたら、なんと当時のCマガがKindleにて1冊100円で販売されているようでKindleごと欲しくなってしまいました。1989年10月創刊号はデニス・リッチー氏のインタビューが載ってるようで興味アリです。今では読めない雑誌といえば、こちらも同時期2007年に休刊したJava Worldっていう雑誌も。あの頃Javaの雑誌が複数ありましたが、Java Worldはカラーページが多く、デザインや紙質も奇麗だったイメージがあります。当時CとPerlしか触ったことがなかったのですが、Javaは人気もあってキラキラしていたのを覚えています。眺めるばっかりで中身はさっぱり理解していなかったのですが・・。今でも刊行していたらAndroid、Java8など面白そうですけど、やはりネットが便利であまり売れないのかも。
 
さて、へびゲーム作りですね。前回はfor式でsnakeの頭から尻尾までぶんぶん回して表示しました。ループカウンタとして変数iを上書きしながら処理が進みます。今回はより関数型プログラミングちっくにしてみましょう。まず変数の書き換えを行わずにループを書いてみます。ということで「でたっ!ゥワォ!」・・・再帰ループです。OCamlでは再帰呼び出しを行う関数はlet宣言の後にrecというキーワードが必要になります。recが無いと自己参照できずエラーになってしまいます。書き出しはこうです。
 
let rec show_snake snake = ...
 
へびの頭を取り出すには、前回のようにList.nth関数を使い、

let rec show_snake snake =
  let (x,y) = List.nth snake 0 in ...
 
と書いてもよいですが、後で胴体部分に対して再帰呼び出しすることになるので、ここは頭と胴体をパターンマッチで一度に束縛しましょう。パターンマッチは言語によってバリエーションがあるため一口には説明しづらいですが、主にデータの構造(リストやタプル、型コンストラクタ)に着目し、データ構造を分解して各部分に変数を束縛する機能です。ただし、構造だけでなく値によってマッチングを変える機能もあったりします。まあとても便利な機能なんです。OCamlのパターンマッチはmatch式で書きます。
 
match 式 with
   パターン1 -> 式
 | パターン2 -> 式
 | ...
 
とこんな感じです。REPLで試してみましょう。まずはREPLセッションで使う疑似スネークを用意します。
 
# let snake = [(1,1); (2,2); (3,3)];;
val snake : (int * int) list = [(1, 1); (2, 2); (3, 3)]
 
さてmatch式で頭と胴体を取り出してみましょう。
 
# match snake with (head :: body) -> head;;
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
[]
- : int * int = (1, 1)
 
1行目のwithの後を見て下さい。(head :: body)と書くことでsnakeをheadとbodyのコンスに分解してマッチさせています。コンスは::演算子のことですね。コンスは「要素 :: リスト」と書いて、リストの先頭に要素を加える演算だったことを思い出しましょう。つまり、リストの先頭をheadに束縛し、「残り」をbodyに束縛するようなパターンマッチです。snakeはこのパターンにマッチすることができますので、->記号の右側の式が実行されます。->の右側はheadを式全体の値として返却しているだけです。5行目のREPLの出力をみると(1,1)が返ってきていることが分かりますね。今度はbody側を返却してみましょう。
 
# match snake with (head :: body) -> body;;
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
[]
- : (int * int) list = [(2, 2); (3, 3)]
 
1行目の->の右側でbodyを返しています。5行目を見るとbodyは[(2,2); (3,3)]という胴体部分にマッチしていることがわかります。さて、目障りな2〜4行目のWarningですが、パターンマッチに漏れがあると言っていますね。[]という空リストにはマッチしないと教えてくれています。空リストは(head :: body)にマッチしないのでしょうか?イエス。headにマッチするということは少なくとも1つは要素が無いといけません。試してみましょ。
 
# match [] with (head :: body) -> head;;
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
[]
Exception: Match_failure ("//toplevel//", 2, -17).
 
1行目でsnakeの代わりに[]を使ってmatch式を評価すると5行目でMatch_failureが発生しました。そんなわけで、要素が無い空リストの場合は別のパターンとして記述しましょう。
 
# match [] with 
    [] -> (0,0)
  | (head :: body) -> head;;
- : int * int = (0, 0)
 
最初のパターンにマッチし(0,0)を返しています。次の例では胴体と尻尾をさらに分けています。
 
# match snake with
    [] -> (0,0)
  | (head :: body :: tail) -> body;;
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a value that is not matched:
_::[]
- : int * int = (2, 2)
 
3行目で頭・胴体・尻尾にマッチさせ、胴体を返しました。7行目で胴体(2,2)が返ってきていますね。えっとまたWarningが・・・今度は_::[]にマッチしないと。うーんと、「要素1個と空リストのコンス」ってことは[x]のような要素1個のリストにはマッチしないって教えてくれています。確かに[x]ではheadはあってもbodyとなる要素が無いです。ではこれなら?
 
# match snake with
    [] -> (0,0)
  | (head :: body) -> head  
  | (head :: body :: tail) -> body;;
Warning 11: this match case is unused.
- : int * int = (1, 1)
 
パターン2で(head::body)、パターン3で(head::body::tail)にマッチさせてみると、最後のパターンにはマッチしないと警告しています。順番を入れ替えましょう。
 
# match snake with
    [] -> (0,0)
  | (head :: body :: tail) -> body 
  | (head :: tail) -> head;;
- : int * int = (2, 2)
 
うまくパターン2にマッチしてbodyが取り出せました。パターンマッチの注意点は2つ。
その1. パターンは漏れが無いように。漏れた場所に引っかかると例外だ。
その2. パターンは上から評価される。網羅的パターンより限定的パターンを先に持ってこよう。
 
ではパターンマッチを覚えたのでshow_snakeを再帰ループで書いてみましょう。
 
let rec show_snake snake =
  match snake with
    [] -> ()
  | ((x,y) :: body) ->
      set_pos(x+1, y+1); print_char '*';
      show_snake body;;
 
4行目のパターンマッチはhead部分をさらにタプル構造でパターンマッチし、(x,y)に分配束縛しています。他言語ではifやパース関数を使って書くところですが、パターンマッチは一度に色々やってくれるのでとっても便利な記法です。HaskellやErlangにもあります。次に再帰ループの部分を確認しましょう。パターンマッチで取り出した胴体部分に対し、6行目でshow_snakeを再帰呼び出ししています。呼び出す度に胴体が短くなり、最後はパターン1にマッチして再帰が停止します。
 
せっかくなのでもうちょっと。OCamlでは無名関数とパターンマッチを組み合わせるシンタックスシュガーがあります。まずは単純に無名関数とパターンマッチを組み合わせてみます。
 
fun x -> match x with
  パターン1 -> 式1
| パターン2 -> 式2
 
この式は全体としては無名関数の定義です。引数xをもらってmatch式を評価しています。これを以下のようにfunction式で書くことができます。
 
function
  パターン1 -> 式1
| パターン2 -> 式2
 
やはり式の全体は無名関数です。引数はパターンマッチに使われ、一致したパターンの式が評価されます。function式を使ってshow_snakeを書き換えてみましょう。
 
let rec show_snake = function
    [] -> ()
  | ((x,y) :: body) ->
      set_pos(x+1, y+1); print_char '*';
      show_snake body;;
 
snakeという引数名はどこいった?って感じですがパターンマッチに使われます。引数snakeに束縛してからmatch snakeするのではなく、引数をいきなり((x,y)::body)にマッチさせてしまうのです。なお、複数の引数を取る関数の場合、最後の引数がfunction式のパターンに使われます。
 
さて、少し関数プログラミングちっくになりました。「ちっく」と言っているのはset_posとprint_charが副作用関数だからですが、まあいいでしょう。HaskellやClojureの時もお話したとおり、リストをなめて何か処理するというのは定番ですので便利関数があります。mapやfilterやfoldなどですね。もちろんOCamlにもmapがあります。これを使えば自分で再帰ループを書くよりもきっと良いコードになるでしょう。では早速。
 
let show_snake snake =
  List.map (fun (x,y) -> set_pos(x+1, y+1); print_char '*') snake;;
 
ちょっと長いですが、全体の構造は「List.map (無名関数の処理) snake」です。snakeから一つずつ要素を取り出して無名関数の引数(x,y)に束縛し、set_posとprint_charを実行しています。この関数の型をREPLで見てみましょう。
 
# show_snake;;
- : (int * int) list -> unit list = <fun>
 
(int * int)というタプルのリストを引数に、unitのリストを返すという型です。「unitのリスト」とunitは違うので注意です。実はOCamlでは「戻り値がunit以外の関数」を逐次実行式で書くと警告してきます。前回お話したとおり、逐次実行式では途中の式の戻り値は捨てられます。そのため「戻り値がunit以外の関数」を途中の式として呼び出す場合、意味のある値を捨てているとコンパイラが判断し怒るのです。今回のshow_snakeも*記号を表示するという副作用だけが目的であり、逐次実行式で使うことを想定しています。なので戻り値がunit listだと逐次実行式(うが〜言いにくい、要は文ってことです)で使えないので、戻り値をunitに整えましょう。そんなとき便利なのがignore関数です。型は'a -> unitです。’aは「どんな型でも」って意味です。「どんな型の値」でもunitにしちゃう関数ですね。これをshow_snakeに適応しましょう。
 
let show_snake snake =
  ignore(List.map (fun (x,y) -> set_pos(x+1, y+1); print_char '*') snake);;
 
# show_snake;;
- : (int * int) list -> unit = <fun>
 
ignore関数を使って戻り値をunit listからunitに変えました。さてshow_snakeをいじくるのはこの辺にして、次は何しましょう。盤面表示、へび表示、と作ってきたので次はリンゴ表示を作りましょう。リンゴの座標は(x,y)とタプルで表すことにします。
 
let show_apple (x,y) = set_pos(x+1, y+1); print_char 'O';; (* オー *)
 
ところで、へびゲームの全体の流れとして「キーボード入力->へび移動など状態変更->画面表示」をループで繰り返すことになります。画面更新とは、これまで作成した盤面、へび、リンゴを表示することです。それではこれらの関数をまとめて呼び出せるようにしましょう。
 
let show_game apple snake =
  cls ();
  show_board();
  show_apple apple;
  show_snake snake;
  set_pos(0,8);;
 
画面クリアして盤面、リンゴ、へびを表示した後、カーソルを左下(0,8)に設定しておきます。引数に適当な値を入れて表示を確認してみましょう。メイン関数を書き換えてコンパイルして実行します。
 
let () =
  show_game (3,3) [(1,1); (1,2); (1,3)];;
 
$ ocamlopt SnakeGame.ml
$ ./a.out
+----+
|*   |
|*   |
|* O |
|    |
|    |
+----+
 
うまくいっていますね。そろそろへびの移動とか作り込みたいところですが、先にキーボード入力処理を作ってしまいましょう。入力関数で良さそうなのはないかな?とPervasivesを眺めてみるとread_line (型: unit -> string)というのをめっけました。この関数はキーボードでw,a,s,dを入力した後Enterを叩かないと文字列を返してこないので、ゲームの操作性としてはイケてないですがまあいいでしょう。本当はキーボード入力後、内部でバッファリングせずに即座に1文字返す関数が欲しいのですが、Unix関数使ったりとちょっと面倒なので今回はこれでいきます。
 
w,a,s,dという文字のままだとプログラムで扱いにくいので、上下左右の移動を表す定義に置き換えましょう。数値でも文字列でもない新しい値を作るので、まずはその値の型を作ります。OCamlではtype宣言を使って新しい型を定義することができます。
 
type 型名 = コンストラクタ1 | コンストラクタ2 | .. | コンストラクタn
 
と定義します。以下のように上下左右の移動を新しい型として定義します。
 
type move = Up | Down | Right | Left;;
 
型名はmoveです。Upはコンストラクタです。引数はありません。Upはmove型の値として振る舞います。Down、Right、Leftも同様です。なおこのあたりの説明を飛ばします。型宣言はじっくり取り組むべきネタですが、今回はEnumみたいな単純なものなのでバッサリいきます。ではキーボード入力関数を書いてみます。関数名はget_moveです。
 
let get_move () =
  let s = read_line() in
  match s.[0] with
    'w' -> Up | 's' -> Down | 'a' -> Left | 'd' -> Right
  | _ -> ???
 
引数はありません。read_line関数の副作用でsに文字列が束縛されます。文字列は文字の配列であり、.[n]でアクセスすることができます。上の例のようにパターンマッチは構造だけでなく値(w,s,a,d)にマッチさせることもできます。さて5行目の???ですが、w,s,a,d以外が入力されたらどうしましょう。例えばqが入力されたらゲームを終了させるなど複雑な処理も考えられますが、今回はw,s,a,dのいずれかが入力されるまで無限ループさせましょう。それと何も入力せずEnterを押されると1文字も入力されていないので、s.[0]のアクセスでエラーになります。その点も考慮してループを再帰で書いたバージョンが以下になります。
 
let rec get_move () =
  let s = read_line() in
  if String.length s = 0 then
    get_move()
  else
    match s.[0] with
      'w' -> Up | 's' -> Down | 'a' -> Left | 'd' -> Right
    | _ -> get_move();;
 
まず再帰呼び出しできるようにrecキーワードを付けます。3行目でStringモジュールのlength関数で長さを調べて、0ならば再帰ループです。8行目でw,s,a,d以外の文字ならば再帰ループです。したがってこの関数は一度呼び出すとw,s,a,dのいずれかが入力されるまでループして、move型の値(Up/Down/Left/Right)に変換して返却します。
 
最後にまた雑誌の話です。Cマガではなくベーマガです。私にとってベーマガっていうとBass Magazineっていう音楽雑誌の方になじみがあるのですが、ココで言うベーマガと言えばもちろんBASICマガジンのことです。Wikipediaを見ると1982〜2003年に刊行していたとのこと。プログラマの登竜門的な雑誌として有名ですが、私は残念ながら一度も中身を見たことがありません。ゲームプログラムのソースリストが完全な形で掲載されており、読者はそれを手入力して遊んだり、自らプログラムを投稿したりしたそうです。タイトル通りBASICで書かれていたのでしょう。私はVB、VBAは扱ったことがありますがBASICは未体験なのでいつか触ってみたい言語です。プログラマがベーマガについて語るとき、郷愁とそして楽しそうに語る姿をよく見ます。裏山鹿。当時のプログラミング環境はどんなんだったのでしょう。今は各種リソースが安価で入手でき、ブログだけでなくStackOverflow/Qiitaなどネットで有用な情報を入手でき、最新の開発環境が無料でDLでき、ソースを共有して開発するなんてこともできますが、当時はどれも無かったでしょう。それでも、いやだからこそ?限られた少ないソースから多くのことを学ぼうとじっくり研究し、足りないパーツはライブラリを探すのではなく自分で作ったのでしょう。パソコンやコンパイラを持っていなくてもK&Rのプログラミング言語Cを穴が開く程読み、エアギターならぬエアプログラミングをしていたというエピソードを聞いたこともあります。うーん(妄想中)、どんなんだったんでしょうね、渇望?熱狂?牧歌的?それとも今と根本的には別に変わらなかった?そんな当時の空気も休刊になった雑誌と同じく今となっては手に入らないものなのかもしれません。
 
以上
 
第20回目:OCamlでへびゲームを作る(Part2)
2014-06-29
 筆者:村田
 
こんにちは。
 
前回はOCamlの基本文法をいくつか見たところでした。今回から早速へびゲーを作っていきましょう。まずはどこから手をつけましょうか。データ構造とアルゴリズムといったMVCパターンのModelから作って最後にViewを・・と進めていけたら説明がしやすいし、関数プログラミングっぽい(?)気がします。が、実際にはやはり目に見えるところからが作りやすく、モチベーションを上げるためにも見た目(View)から始めたいと思います。
 
それから、しばらくはファイル(SnakeGame.ml)に関数を書き、ocamloptでコンパイルして実行するというサイクルでコーディングを進めます。後ほどアルゴリズムなどModel部分を書く時はREPLを使いたいと思います。
 
ではまずゲームの盤面を表示してみましょう。コンソールに文字列を表示する関数はprint_endline関数を使います。なおどんな関数があるかを調べる場合は、以下の公式ページのライブラリマニュアルが参考になります。OCamlではmoduleという機能で関数やデータ型がまとめられており、特にPervasivesというモジュールは重要です。プログラムの最初で必ず読み込まれており、比較関数、ブール演算、数値演算、基本的な入出力関数が収められています。
 
http://caml.inria.fr/pub/docs/manual-ocaml-4.01/libref/index.html
 
それではSnakeGame.mlのメイン関数にprint_endlineを書いてゲームの盤面(5x5マス)を表示させてみましょう。
 
let () =
  print_endline "+----+";                                                       
  print_endline "|    |";                                                       
  print_endline "|    |";                                                       
  print_endline "|    |";                                                       
  print_endline "|    |";                                                       
  print_endline "|    |";                                                       
  print_endline "+----+";;  
 
print_endline関数は引数の文字列を画面に出力した後、自動的に改行します。なおprint_stringという関数もあり、こちらは改行せず文字列を出力するだけです。セミコロン1個は複数の式を順番に実行する時のセパレータです。最後のセミコロン2個は式全体のターミネータとして必要になります。コンパイルと実行方法をもう一度書いておきます。
 
$ ocamlopt SnakeGame.ml
$ ./a.out
+----+
|    |
|    |
|    |
|    |
|    |
+----+
 
盤面を表示することができました。まあ簡単ですね。先程のライブラリマニュアルでprint_endlineの型を見るとstring -> unitとあります。stringを引数に取りunitを返す関数です。REPLで実験してみましょう。
 
# print_endline "Hello";;
Hello
- : unit = ()
 
最初の「Hello」という出力はprint_endlineの副作用(画面出力)です。次の「- : unit = ()」はREPLが出力したprint_endlineの戻り値(評価結果)です。unit型の()という値を返したと言っています。()はunit型の唯一の定数として定義されており、「意味の無い値」を意味します。哲学的ですね。unitは副作用を起こす関数の引数や戻り値として使われます。逆に純粋な関数は何らかの意味のある引数を取り、意味のある結果を返すので、引数や戻り値にunitは登場しません。というわけで、関数の実装を見なくても型にunitが出てきたら副作用があるんだろうなと想像できます。
 
ところで上記の方法では画面がクリアされないので、へびが動く度に盤面を再描画するとしたら、どんどんコンソール画面が下にスクロールして各ターンが全部表示されてしまいます。これでも遊べなくはないですがゲームっぽく見えません。そこでVT100コマンドを使ってコンソール画面のクリアをプログラムから実行しましょう。VT100をwikipediaで調べてみると、1978年に作られたDEC社のビデオ表示端末であるとのこと。VT100の画像を検索してみると、あらカッコいい。実物に触ってみたいですね。VT100のコマンドはエスケープ文字(ESC)から始まるキャラクタシーケンスで表されます。詳しい仕様や規格名称を知らないので、ここで原典を紹介できないのですが、”VT100 エスケープシーケンス”で検索すると色々なサイトで解説されているので参考にして下さい。注意として、VT100のコマンドを正しく解釈して画面制御が行えるコンソールでないとうまく動きません。macの標準ターミナルソフトは動きましたが、うまく動かない場合はVT100コマンドをサポートしているターミナルソフトを使うようにして下さい。
 
では画面クリアの機能を追加しましょう。画面全体をクリアするコマンドはESC[2Jです。ESCは表示可能文字ではないので、16進数で入力しましょう。ESCの16進数はman asciiで調べると0x1bですね。OCamlの文字列中に表示不可文字を入力するためにはバックスラッシュでエスケープします。0x1bを入力するには”\x1b”となります。バックスラッシュの後の’x’は16進数で入力することを意味します。それではメイン関数を書き換えてみます。
 
let () =
  print_string "\x1b[2J";
  print_endline "+----+";
  ..省略
 
実行してみると、あれ?おかしい。確かに画面はクリアされたけど、盤面をコンソールの一番上から描画してくれません。ところで今更ですがUIの説明をするには文章だけだと苦しいですね。画面のキャプチャでも載せようかとも思いましたが、途中経過のキャプチャを見てもね〜ということでパスします。それはさておき解決方法は、画面クリアに加えて描画開始位置をコンソールの一番上にリセットすることです。描画位置をxy座標で(0,0)にセットするVT100コマンドはESC[0;0Hです。数値部分は最初が行指定、次が列指定です。一般的なxy座標は縦(行)にy軸を取るため、例えばXY座標(1,3)にセットしたければ、3行目1列目ですので、ESC[3;1Hとなります。順番がひっくり返るので注意しましょう。ではメイン関数を書き換えて位置リセットも加えましょう。
 
let () =
  print_string "\x1b[2J";
  print_string "\x1b[0;0H";
  print_endline "+----+";
  ..省略
 
Yey!うまくいきました。画面の一番上から盤面が描画されるようになりました。いったんメイン関数でやっていることを整理して関数に分けていきましょう。まずは画面クリア(clear screen)を関数にします。関数の定義は「let 関数名 引数の並び = 式;;」です。関数名はclsとし、引数は要らないのでunit型の()をもらうようにします。呼び出す時はcls ();;のようにunitを渡せば良いです。
 
let cls () = print_string "\x1b[2J";;
 
次は位置リセット関数ですが、(0,0)だけでなく、任意のxy座標に移動できるようにしましょう。へびやリンゴの描画にもきっと使いますしね。関数名はset_posとし、引数はxとyをもらうので、let set_pos x y = ...と2つの引数でもらってもいいのですが、let set_pos (x,y) = ...とx,yをタプル(組み)にして、1引数でもらうようにしたいと思います。set_pos関数は以下のとおり。
 
let set_pos (x,y) =
  print_string ("\x1b[" ^ string_of_int y ^ ";" ^ string_of_int x ^ "H");;
 
まず^記号ですが、これも関数です。中置演算子のように書け、型はstring -> string -> stringです。左右の文字列を結合します(concat)。string_of_int関数の型はint -> stringです。数値を文字列表現に変換します。変換できなければ例外が発生します。今回のプログラムでは例外処理は無視して扱いませんが、OCamlには例外をパターンマッチングでハンドリングするtry式という構文があります。あと、print_string (...)の両端の括弧は必須です。この括弧は2 * (3 + 4)の括弧のように結合性を高めるものです。括弧を無くすとprint_string "\x1b[" を先に評価してしまい、その結果、() ^ string_of_int y ...というヘンテコ式になってしまい、コンパイルエラーになってしまいます。
 
次は盤面表示を関数にまとめておきましょう。最初に(0,0)にリセットして盤面を描画します。画面クリアはshow_board関数内では行いませんので、呼び出し側が必要に応じてcls関数を呼ぶようにします。
 
let show_board () =
  set_pos (0,0);
  print_endline "+----+";
  print_endline "|    |";
  print_endline "|    |";
  print_endline "|    |";
  print_endline "|    |";
  print_endline "|    |";
  print_endline "+----+";;
 
メイン関数も修正しておきましょう。
 
let () =
  cls();
  show_board();;              
 
そろそろアルゴリズムとデータ構造も考え始めます。へびのデータ構造を考えてみましょう。へびの頭から尻尾までを盤面上に*で表すので、「位置」のリストにするのが良さそうです。リストにしておけば、へびの胴体が伸びるという部分も自然に表現できそうです。「位置」はxy座標のタプルで表します。というわけで例えば(1,1)に頭、(1,2)に胴体、(1,3)に尻尾があるとすると、[(1,1); (1,2); (1,3)]という感じでタプルのリストとして表すことにします。clojureで8クイーンを書いた時は盤面上のクイーンを「リストのリスト」で表現しました。今回のプログラムでもxy位置をリストとして表現し、「リストのリスト」でへびを表すこともできますが、幸いにしてOCamlやHaskellにはタプルがあるので、「組み合わせ」を表現するならばタプルを使った方が自然です。
 
ふー。ではちょい余談で。複数のデータをまとめる言語機能として、構造体(C言語など)や連想配列(JavaScriptなど)がありますが、タプルはメンバに名前やキーを付ける必要がなく、とても簡単にグループ化できて便利なデータ構造です。もちろんメンバに名前をつけたり、まとめたものに新しい型を与えたい場合には、構造体などで定義するわけで、OCamlにもレコードやヴァリアントという型を作り出す機能があります。AppleのSwift言語もObjCにはなかったタプルがサポートされ、手軽にデータをまとめて戻り値にするなど便利に使えるようになりました。
 
さあさあ、へびデータ構造が決まったのでへびを表示できるようにView優先でもりもり作っていきましょう。リストで表されたそれぞれの位置に*記号を書くわけですから、ループ処理になりますね。色々な書き方があるので、せっかくだから色々試してみようかな。まずはfor式を使ってみます。for式は 
 
for 変数 = 初期化式 to 終端値 do
  ...変数を使った何らかの式
done;;
 
って書きます。REPLで実験してみましょう。
 
# for i = 0 to 9 do
    print_int i
  done;;
 
0123456789- : unit = ()
 
って感じです。iという変数が0〜9まで変化します。iを参照できるのはdo〜doneまでのスコープです。OCamlでは代入などの副作用が簡単に書けるので、iという「いわゆる変数」を使ってC言語のようにループを書けますね。これを使ってshow_snake関数を書いてみます。
 
let show_snake snake =
  for i = 0 to List.length snake - 1 do
    let (x,y) = List.nth snake i in
    set_pos(x+1, y+1); 
    print_char '*'
  done;;
 
1行目はいいですかね。snakeが引数です。2行目のi=0もいいでしょう。終端値は引数snakeから計算しています。リストの長さを計算する関数は標準モジュールPervasivesには無く、Listモジュールのlength関数を使います。外部モジュールのインポート宣言は不要ですが、モジュール名.関数名という形で参照する必要があります。毎回モジュール名を関数名の前に書くのが煩わしい場合は、ファイルの先頭でopen List;;と書いてopen宣言することでモジュール名解決が自動的に行われます。ですが今回のプログラムでは律儀にモジュール名.関数名で書くことにします。List.length snake - 1と書いているので、例えばsnake引数が[(1,1); (1,2); (1,3)]の場合、リストの長さは3ですから、iは0,1,2と変化します。3行目は盛りだくさんです。まずlet式で局所変数の束縛を行っています。clojureでもletで局所束縛をやりましたが同じものです。let式は
 
let 束縛名 = 式 in 束縛名を使った式;;
 
と書きます。変数ではなく束縛と言っているのは「書き換えられない」というニュアンスを込めています。List.nth snake iはsnakeリストのi番目のデータを取り出すという式です。「List.nth snake 0」ならばへびの頭の位置(1,1)が返ってきます。さらに=の左辺は(x,y)で束縛しています。これはclojureの分配束縛と同じで、(x,y) = (1,2)と書いたとき、xは1にyは2に分配束縛されます。4行目はコンソール画面の出力位置をセットしています。xとyをそれぞれ+1しているのは、盤面の枠部分を考慮しているからです。例えばへびの先頭(1,1)を表示したい場合、実際の画面上の位置は(2,2)になります。次にprint_char関数で*文字を出力しています。4と5行目の間のセミコロンはセパレータとして必要です。
 
副作用と逐次実行
4と5行目の間のセパレータについてもう少し詳しく説明しましょう。副作用のある式というのは実行順がとても重要になります。実行順の制御構造の一つ、逐次実行を表す式をOCamlでは以下のように書きます。
 
式1; 式2; 式3
 
と書き、式3の値が逐次実行式の全体の値となります。式1、式2の戻り値は捨てられます。当たり前のように見える逐次実行ですが、関数型言語では逐次実行が特別扱いされています。OCamlでは逐次実行式、HaskellではIOモナド、Clojureではdoフォームとそれぞれ専用の構文が用意されています。もう耳タコ状態かもしれませんが、副作用の無い関数とは「引数にしか依存しない」関数です。引数が同じならばいつ実行しても同じ結果が返ってくるので、実行順は重要ではなく、任意順でも良いのです。遅延評価のように必要になったら式を評価するというスタンスもアリです。式A、式Bと書いたとき式Aが式Bより先に実行(評価)されることを保証しないのが関数型言語のスタンスです。しかし、副作用を持つ式はこれでは困ります。例えば、式Aでファイルを開いて式Bでデータを書き込むことを想像してください。順番が逆だとエラーになってしまいますね。そこで、副作用のある式の実行順を制御するためにOCamlでは逐次実行式があり、セミコロンで区切った式は順番に実行されるのです。HaskellのIOモナドやClojureのdoフォームも逐次実行を行うために導入されていると言ってもいいでしょう。(それだけではありませんが)。
 
副作用とfor式
ついでにもう一つ。OCamlのfor式のように、forループは内部で式を繰り返し実行しますが、ループを抜ける直前に評価した結果しか返せません。途中の値はポイです。再帰ループのようにループする度に戻り値として値が返るのとは対照的です。このことをよく考えると、forループで実行する式は基本的に副作用を目的にしたものであることが分かります。変数を+1ずつ書き換えたり、データベースにデータを繰り返し入れたり、エトセトラです。何しろ最後の評価結果しか返せないのですから、純粋関数をforループ内で繰り返し呼んだところで、結果は捨てられてしまって、何も世界を変えられないのです。というわけで副作用とforループは密接に関連します。forループあるところに副作用ありです。
 
えーっとウンチクばっかりで肝心のコードはもりもりどころか全然書けていませんが、show_snake関数forループ版が書けたところでそろそろ終わりましょう。次回はせっかく作ったshow_snake関数を書き直すところから:=)
 
以上
 

連載記事のソースコード

連載記事のソースコード
 
・Haskellで問題を解く(Part1〜Part4) ソースコード
・Clojureで8クイーン問題にチャレンジ(Part1〜Part5) ソースコード
・OCamlでへびゲームを作る(Part1〜Part5) ソースコード
・Swiftでオセロを作る(Part1〜Part5) ソースコード
・Processingでシューティング(Part1〜Part4) ソースコード 
Haskellでテトリス(Part1〜Part9) ソースコード
・プチコン3号(BASIC)でさめがめ(Part1〜Part3) ソースコード
・Prologでさめがめを解く(Part1〜Part6) ソースコード