読者です 読者をやめる 読者になる 読者になる

めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

Haskell ユーザがおもむろに OCaml を使うとどうなるか実験してみました

・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 の入門書を読んだら、その後で私の書くコードがどう変わるか報告したいと思います。