・Haskell レベル:「ふつうの Haskell プログラミング」とか「Real World Haskell」を読んで Hine Sweeper を書いてみたレベル。
・OCaml レベル:1週間前に OCaml プログラミング入門 を読んだだけ。
という人間(というか私のことです)が適当に Web 上の文書をあさりながら Haskell のサンプルプログラムを OCaml で書き直したらどうなるか実験した結果です。サンプルプログラムは「ふつうの Haskell プログラミング」のものを利用します。(cat/head/tail などの Unix コマンドの簡易版です。)
※ 実際にはもうちょっと紆余曲折していますが、以下はそれなりに動いたコードを記載しています。
cat
Haskell のサンプルはこれ。標準入力をそのまま標準出力に書き出す。
main = do cs <- getContents putStr cs
普通に行単位の再帰で書いてみる。普通に動いたっぽい。
(* cat0.ml *) let cat () = let rec cat_sub () = print_string( read_line () ); print_newline (); cat_sub () in try cat_sub() with End_of_file -> () let _ = cat ()
ここで、getContents 風に標準入力を一気に list にまとめることをやってみる。
(* cat1.ml *) let read_all () = let rec read_all_sub result = try read_all_sub ( read_line () :: result ) with End_of_file -> ( List.rev result ) in read_all_sub [] let cat () = let print_line line = print_string line; print_newline (); in List.map print_line ( read_all () ) let _ = cat ()
ただし、cat0 は行単位で入力ごとに処理が行われるのに対して、cat1 は全部の行を入力し終わらないと処理が行われない。
# ./cat0 hoge ← 入力 hoge ← 出力 hoga ← 入力 hoga ← 出力 ponyo ← 入力 ponyo ← 出力 (Ctrl-D) # ./cat1 hoge ← 入力 hoga ← 入力 ponyo ← 入力 (Ctrl-D) hoge ← 出力 hoga ← 出力 ponyo ← 出力
同じロジックを Haskell で書いても同じ動きになるのでロジックが悪いわけではないですが、getContents 的な遅延 IO を活用した書き方は OCaml では難しい???
countline
Haskell のサンプルはこれ。標準入力から読んだテキストの行数を表示。
main = do cs <- getContents print $ length $ lines cs
普通に再帰処理で書いたらこうなりました。
(* countline0.ml *) let nop a = () let count = let rec count_sub total = try nop ( read_line () ); count_sub ( total + 1 ) with End_of_file -> total in count_sub 0 let () = print_int count; print_newline ()
read_line () を nop で囲んでいるのは、"Warning S: this expression should have type unit." というコンパイル時の警告を避けるためだけです。; で次の文に継続する文は、返り値が捨てられるので () を返さないとこのような警告がでるらしい。
せっかくなので、先程つくった read_all を使ってみるとこんな感じ。
(* countline1.ml *) let read_all () = let rec read_all_sub result = try read_all_sub ( read_line () :: result ) with End_of_file -> ( List.rev result ) in read_all_sub [] let countline () = print_int ( List.length ( read_all () ) ); print_newline () let _ = countline ()
head
Haskell のサンプルはこれ。標準入力の先頭 10 行を表示。
main = do cs <- getContents putStr $ firstNLines 10 cs firstNLines n cs = unlines $ take n $ lines cs
cat0.ml の処理を 10 行目で打ち切ればいいわけですが、End_of_file を raise して強制終了してみました。
(* head0.ml *) let head () = let rec head_sub count = if count = 0 then raise End_of_file else (); print_string( read_line () ); print_newline (); head_sub ( count - 1 ) in try head_sub 10 with End_of_file -> () let () = head ()
もうちょっと上品に終われないかなぁ。。。ということで。
(* head1.ml *) let head () = let rec head_sub = function | 0 -> () | count -> ( print_string( read_line () ); print_newline (); head_sub ( count - 1 ) ) in try head_sub 10 with End_of_file -> () let () = head ()
expand
まずは、'\t' を '@' に置換するだけの処理。
main = do cs <- getContents putStr $ expand cs expand :: String -> String expand cs = map translate cs translate :: Char -> Char translate c = if c == '\t' then '@' else c
Haskell 版と同様に map で一発!と思いきや OCaml では、String が Char の配列ではない事に気づいて、さぁ大変。結局、こんな感じになりました。OCaml の文字列操作って、普通はどうやるんでしょうね???
(* expand0.ml *) let rec expand_line line = try line.[ String.index line '\t' ] <- '@'; expand_line line with Not_found -> line let rec expand () = let rec expand_sub () = print_string ( expand_line( read_line() ) ); print_newline (); expand_sub () in try expand_sub() with End_of_file -> () let _ = expand ()
次は、'\t' をスペース 8 文字に置換するやつ。
main = do cs <- getContents putStr $ expand cs expand :: String -> String expand cs = concat $ map expandTab cs expandTab :: Char -> String expandTab c = if c == '\t' then " " else [c]
String.sub でこんな感じ。うーん。関数プログラミングっぽくない。。。
(* expand1.ml *) let rec expand_line line = try let i = String.index line '\t' in expand_line (String.sub line 0 i ^ String.make 8 ' ' ^ String.sub line (i + 1) ( String.length line - i - 1 ) ) with Not_found -> line let rec expand () = let rec expand_sub () = print_string ( expand_line( read_line() ) ); print_newline (); expand_sub () in try expand_sub() with End_of_file -> () let _ = expand ()
catn
Haskell のサンプルはこれ。行番号を付与して表示します。
main = do cs <- getContents putStr $ numbering cs numbering :: String -> String numbering cs = unlines $ map format $ zipLineNumber $ lines cs zipLineNumber :: [String] -> [(Int, String)] zipLineNumber xs = zip [1..] xs format :: (Int, String) -> String format (n, line) = rjust 6 (show n) ++ " " ++ line rjust :: Int -> String -> String rjust width s = replicate (width - length s) ' ' ++ s
実行例はこんな感じ
# cat catn.hs | ./catn 1 main = do cs <- getContents 2 putStr $ numbering cs 3 4 numbering :: String -> String 5 numbering cs = unlines $ map format $ zipLineNumber $ lines cs 6 7 zipLineNumber :: [String] -> [(Int, String)] 8 zipLineNumber xs = zip [1..] xs 9 10 format :: (Int, String) -> String 11 format (n, line) = rjust 6 (show n) ++ " " ++ line 12 13 rjust :: Int -> String -> String 14 rjust width s = replicate (width - length s) ' ' ++ s
やっぱり zip がいいよねぇと思いつつ OCaml での無限リストの扱いが面倒なことに気づいて、さぁ大変。
まずは普通に再帰処理で・・・。行番号部分のフォーマッティングはおとなしく printf しています。
(* catn1.ml *) let catn () = let rec catn_sub count = try Printf.printf "%6d %s\n" count ( read_line () ); catn_sub ( count + 1 ) with End_of_file -> () in catn_sub 1 let _ = catn ()
いいのか悪いのか分かりませんが有限リストを構成して無理矢理 zip を使った例はこちら。OCaml の場合は List.combine ですね。
(* catn2.ml *) let iota n m step = let rec iota_sub n m step result = if m <= 0 then List.rev result else iota_sub (n + step) (m - 1) step ( n :: result ) in iota_sub n m step [] let read_all () = let rec read_all_sub result = try read_all_sub ( read_line () :: result ) with End_of_file -> ( List.rev result ) in read_all_sub [] let print_line = function | ( count, contents ) -> Printf.printf "%6d %s\n" count contents let catn () = let lines = read_all () in let counts = iota 1 (List.length lines) 1 in List.map print_line ( List.combine counts lines ) let _ = catn ()
う〜ん。普通に再帰処理した方が簡単かも。let 〜 in の段々畑もなんとかならないものか。。。
そのうち OCaml の入門書を読んだら、その後で私の書くコードがどう変わるか報告したいと思います。