2014年3月29日土曜日

[OCaml]リバーシゲームを作ってみた

OCamlの勉強がてら、リバーシ(オセロ)を作ってみた。


コンピュータの思考アルゴリズムは minimaxアルゴリズムを使用した4手先読み。
盤の評価関数は、四隅と縦横の列にディスクを置けると高評価という単純なもの。
でも、適当に手を指していたら、コンピュータに一回負けた...

いろいろ試行錯誤しながら書いた汚ないコードだけど、公開しておこう。

https://github.com/takeisa/ocaml-reversi

モジュールは以下の通り。

reversi.mlリバーシ本体
board.ml
cell.ml盤の升目の定義
computer.mlコンピュータの思考ロジック
disk.mlディスクの定義
pos.ml座標

2014年3月16日日曜日

[OCaml]FFIでhello.world!

OCamlからFFI(Foreign Function Interface)を使い、Cで書いた hello, world! を出力する関数を呼び出す。

準備

ctypesパッケージが必要

opamでインストールできる。
$ opam install ctypes

libffiが必要

$ dpkg --list | grep libffi
ii  libffi-dev:i38 3.0.10-3     i386         Foreign Function Interface library
ii  libffi5:i386   3.0.10-3     i386         Foreign Function Interface library

私のシステム(Debian Wheezy)にはもうインストールされていた。

ソース

hello.h

void print_hello(void);

hello.c

#include <stdio.h>
#include "hello.h"

void print_hello(void) {
  printf("hello, world!\n");
}


hello.ml

open Ctypes
open Foreign

let print_hello =
  foreign "print_hello" (void @-> returning void)


hello.mli

val print_hello : unit -> unit

main.ml

open Hello

let () =
  print_hello ();


コンパイル

共有ライブラリの作成

$ gcc -c -fPIC -Wall hello.c
$ gcc -shared -o libhello.so hello.o


OCamlソースのコンパイル

$ ocamlfind ocamlopt -linkpkg hello.mli
$ ocamlfind ocamlopt -linkpkg -package ctypes,ctypes.foreign hello.ml
$ ocamlfind ocamlopt -cclib libhello.so -linkpkg -g -thread -package ctypes.foreign -package core -o main hello.cmx


実行

$ LD_LIBRARY_PATH=. ./main
hello, world!

動いた!

まとめ

簡単そうだったけど、試行錯誤したので、メモを残しておこう。

静的ライブラリは使えない(使えなかった)

最初は静的ライブラリが使えると思っていた。
いろいろ試していたのだけど、
Fatal error: exception Dl.DL_error("〜/main.native: undefined symbol: print_hello")
というエラーメッセージを見て、動的ライブラリでないと駄目だということに気が付いた。

リンカへのライブラリ指定

hello.cを置いたディレクトリの下に、ocamlソース用のディレクトリを作成して、corebuildコマンドでビルドしていた。
$ corebuild -pkg ctypes.foreign -lflags -cclib,-L../..,-cclib,libhello.so main.native
実行すると、
$ ./main.native
main.native: error while loading shared libraries: ../../libhello.so: cannot open shared object file: No such file or directory
となってしまい、../../libhello.soを参照してしまう。
カレントディレクトリにlibhello.soを置いて、corebuildを実行してみたが、余計なファイルがあると、以下のようなエラーが発生し、ビルドできない。
$ corebuild -pkg ctypes.foreign -lflags -cclib,libhello.so main.native
SANITIZE: a total of 1 file that should probably not be in your source tree
  has been found. A script shell file
  "/home/satoshi/workspace/real_world_ocaml/ffi/ocaml/_build/sanitize.sh" is
  being created. Check this script and run it to remove unwanted files or use
  other options (such as defining hygiene exceptions or using the -no-hygiene
  option).
IMPORTANT: I cannot work with leftover compiled files.
ERROR: Leftover object files:
  File libhello.so in . has suffix .so
Exiting due to hygiene violations.
あきらめて、ビルドにはocamlfindコマンドを使うことにした。

シグネチャの作成

corebuildを使って、以下のようなコマンドを実行すると、mlファイルからシグネチャを作ることができる。
$ corebuild -pkg ctypes.foreign hello.inferred.mli
モジュールの公開メソッドが多いとき、自分で一から書くよりは楽ちん。

参考

Real World OCamlがオンラインで公開されている。

2014年3月8日土曜日

[OCaml]omake -P で FAM not enabled

環境はDebian wheezy。
omakeの自動ビルドを試そうと、omake -Pしてみた。

~/workspace/ocaml/reversi$ omake -P  
*** omake: reading OMakefiles
*** omake: finished reading OMakefiles (0.00 sec)
Fatal error: exception Invalid_argument("FAM not enabled")

エラーとなった。
omakeをインストールする際に、libfam-devをインストールしていなかったためかと思い、
sudo apt-get install libfam-dev してから、
再度、opam reinstall omake とした。

再度、試してみる。
~/workspace/ocaml/reversi$ omake -P
*** omake: reading OMakefiles
*** omake: finished reading OMakefiles (0.01 sec)
*** omake: 0/1 targets are up to date
*** omake: failed (0.10 sec, 0/0 scans, 0/0 rules, 0/13 digests)
*** omake error:
   Failure: om_notify_open: (null)     

また、エラーになった。  
famデーモンを起動していないと出るらしい。
famなんかインストールしたっけ?

/etc/init.d$ ls *fam*
zsh: no matches found: *fam*

あれ?? famがない...
famを入れれば良いのかな?

~/workspace/ocaml/reversi$ sudo apt-get install fam
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています               
状態情報を読み取っています... 完了
以下のパッケージが新たにインストールされます:
  fam
アップグレード: 0 個、新規インストール: 1 個、削除: 0 個、保留: 1 個。
74.8 kB のアーカイブを取得する必要があります。
この操作後に追加で 270 kB のディスク容量が消費されます。
取得:1 http://ftp.jp.debian.org/debian/ wheezy/main fam i386 2.7.0-17 [74.8 kB]
74.8 kB を 0秒 で取得しました (124 kB/s)
以前に未選択のパッケージ fam を選択しています。
(データベースを読み込んでいます ... 現在 110180 個のファイルとディレクトリがインストールされています。)
(.../archives/fam_2.7.0-17_i386.deb から) fam を展開しています...
man-db のトリガを処理しています ...
fam (2.7.0-17) を設定しています ...
Starting file alteration monitor: FAM.

どうやらFAMが有効になったらしい。
omake -P を試してみる。

~/workspace/ocaml/reversi$ omake -P
*** omake: reading OMakefiles
*** omake: finished reading OMakefiles (0.00 sec)
*** omake: done (0.02 sec, 0/2 scans, 0/7 rules, 0/46 digests)
*** omake: polling for filesystem changes

お。動いた!
試しに、ファイルを編集してみる。

*** omake: file reversi_client.ml changed ←◆これを編集
*** omake: rebuilding
*** omake: done (0.25 sec, 1/3 scans, 2/11 rules, 6/93 digests)
*** omake: polling for filesystem changes

リビルドされた。これは便利。

参考

2014年3月5日水曜日

[OCaml]cursesライブラリの wide character サポートを有効にする

OCaml にも、Cursesライブラリがある(OCaml Curses)。
このライブラリは opam でインストールできるが、現バージョンのcurses-1.0.3では、wide characterサポートが有効になっておらず、日本語は文字化けして正しく表示できなかった。
opamでのインストール時に実行するconfigureに、--enable-widec を付けてビルドできれば良いのだけど、具体的にどのようにすればできるのか不明だった。
何とかできないかなーと、tweetしていたところ、
星のキャミバ様さんに、ローカルリポジトリを作る方法で、簡単に自分用パッケージを作成できることを教えていただいた。

想像していたよりも、かなり簡単にパッケージを作成することができ、wide characterサポートを有効にしたOCaml cursesライブラリをインストールすることできた。
星のキャミバ様さん、ありがとうございます!

以下、手順。

ローカルリポジトリにするディレクトリ(~/opam_myrepo とした)を作成する。
~$ mkdir ~/opam_myrepo

そのリポジトリを登録する。
~$ opam repo add myrepo ~/opam_myrepo
myrepo     Synchronizing with /home/satoshi/opam_myrepo
[WARNING] "/home/satoshi/opam_myrepo" doesn't contain a "packages" nor a "compilers" directory.
Is it really the directory of your repo ? ("opam remote remove myrepo" to revert)
Updating ~/.opam/repo/compiler-index ...
Updating ~/.opam/compilers/ ...
Updating ~/.opam/repo/package-index ...
Updating ~/.opam/packages/ ...
Updating the cache of metadata (~/.opam/state.cache) ...

登録できたか確認する。
~$ opam repo
  10 [local]     myrepo     /home/satoshi/opam_myrepo
   0 [http]     default     https://opam.ocaml.org

opam_myrepoディレクトリ配下に、packages/curses.1.0.3/を作り、その中に、descr, opam, url ファイルを作る。
curses.1.0.3をインストール済みだったので、~/.opam/packages/curses/curses.1.0.3 からコピーした。

~$ cd opam_myrepo
~/opam_myrepo$ mkdir -p packages/curses.1.0.3
~/opam_myrepo$ cd packages/curses.1.0.3
~/opam_myrepo/packages/curses.1.0.3$ cp ~/.opam/packages/curses/curses.1.0.3/* ./

この後、cursesは再インストールするので、削除する。(reinstallするなら、削除しなくても良いかも)
$ opam remove curses

コピーしたopamを編集して、
--enable-widecを追加する。
〜略〜
build: [
  ["./configure" "--enable-widec"] ←◆これ
  [make "byte"]
  [make "opt"]
  [make "install"]
]
〜略〜

リポジトリをアップデートする。
~/opam_myrepo/packages/curses.1.0.3$ opam update
myrepo     Synchronizing with /home/satoshi/opam_myrepo
default    Downloading https://opam.ocaml.org/urls.txt
default    Downloading https://opam.ocaml.org/index.tar.gz
Updating ~/.opam/repo/compiler-index ...
Updating ~/.opam/compilers/ ...
Updating ~/.opam/repo/package-index ...
Updating ~/.opam/packages/ ...
Updating the cache of metadata (~/.opam/state.cache) ...
0 to install | 11 to reinstall | 17 to upgrade | 0 to downgrade | 0 to remove
You can now run 'opam upgrade' to upgrade your system.

curses をインストールする。
~/opam_myrepo/packages/curses.1.0.3$ opam install curses
The following actions will be performed:
 - install curses.1.0.3
1 to install | 0 to reinstall | 0 to upgrade | 0 to downgrade | 0 to remove

=-=-= Installing curses.1.0.3 =-=-=
curses.1.0.3 ocaml-curses-1.0.3.ogunden1.tar.gz is in the local cache, using it.
Building curses.1.0.3:
  ./configure --enable-widec  ←◆ここ。できたー!!
  make byte
  make opt
  make install
Installing curses.1.0.3.

wide character サポートが有効になったか調べる。
$ echo -e '#require "curses";;\nlet s = if Curses.Curses_config.wide_ncurses then "wide char ok!" else "no wide char support." in print_endline s;;' | ocaml
〜略〜
/home/satoshi/.opam/system/lib/herelib/pa_herelib.cma: loaded
# /home/satoshi/.opam/system/lib/curses: added to search path
/home/satoshi/.opam/system/lib/curses/curses.cma: loaded
# wide char ok! ←◆wide characterが有効になった
- : unit = ()

サンプルプログラム

open Core.Std

let () =
  let module C = Curses in
  let main_window = C.initscr () in
  let err = C.mvwaddstr main_window 10 2 "hello, world!" in
  let err = C.mvwaddstr main_window 11 2 "こんにちは、世界!" in
  let err = C.refresh () in
  Unix.sleep 5;
  C.endwin ()

文字化けせずに、「こんにちは、世界!」を表示できた。
 

参考


2014年3月2日日曜日

[OCaml]Life gameを作ってみた

ここ数日、プログラミングHaskell を読んでいる。
この本には、対話プログラムの例題としてライフゲームが載っているのだけど、
OCamlでは、どんなになるかなーと思い、書いてみた。

昨年の年末から年始にかけて、プログラミングの基礎 (Computer Science Library) を読んで、OCamlで基本的な処理は書けるようになったのだけど、それから2ヶ月の間、全く使っていなかったら、いろいろと忘れていた。やっぱり、言語は使っていないと忘れてしまって駄目だなぁ。

コードは こちら(https://github.com/takeisa/ocaml-lifegame.git) から。
Coreライブラリを使っている。
opamでcoreをインストールして、
$ corebuild life_game.native
とすれば実行ファイルができる。

実行例

途中で止めた時のスクリーンショット。
「*」が生きているセル。
ライフゲームを知らない人は、何だか良く分からない実行結果ですね。


OCamlでコードを書いて思ったこと

・Haskellにある (.) ($) は便利だな。自分で定義すれば良いのだけど、OCamlにも欲しい。
・Screenモジュールで 座標を表わす pos_t型を以下のように定義し、
type pos_t = {x: int; y: int}
シグネチャでは、
type pos_t
として、中身を非公開にした。
しかし、程度のプログラムでは、int * intのタプルそのままで座標を管理する方がコードは簡単になったかもしれない。
・コードの編集時には、Emacs tuareg-modeはとても便利。
でも、ソースをモジュール分割した場合、そのモジュールを認識させるためには、いちいちコンパイルしなければならず、面倒だった。何か良い方法があるのかな?
・OCamlでは簡単に副作用があるコードをどこでも気軽に書けるので、便利と言えば便利なのだけど、気を付けないと、純粋なコードと副作用があるコードが混在しやすそう。
・OCamlはHaskellみたいに、関数の直前に型定義を書けないのかな?シグネチャを分けて書くの面倒だなぁ。
・HaskellよりOCamlの方がコードは若干は冗長になった。これは書き方が悪いか...
・厳密な型チェックは、型に関連するバグが出ないので、やっぱり便利。
・あたりまえだけど、この程度のプログラムではHaskellもOCamlも、あまり変わらない。
・文字出力後、画面を更新する際は、flush stdout を忘れずに。
・HaskellもOCamlもコードを書いていて楽しいので、他にも書いてみよう。
・ライブラリを把握していないので、実装に時間がかかるし、車輪の再発明をしがち。
Cheat sheetは便利。

参考




2014年3月1日土曜日

[Haskell]echoなし1文字入力関数

GHCで、echoなしの1文字キー入力の関数。
hSetEcho関数を使い、echoを無効にする。

import System.IO (hSetEcho, stdin)

getCh :: IO Char
getCh  = do hSetEcho stdin False
            c <- getChar
            hSetEcho stdin True
            return c


参考