Haskellを少しやってみる

何となくHaskellを少しやってみようかと思った。「ふつうのHaskellプログラミング」(青木峰郎著)を頭から3分の1ほど読んだ。ビックリするぐらいRubyと似てる。モナドとか言わなければ、あまり違和感がないかも。リストを切り刻む関数のhead、tailはLispのcar、cdrみたい。Haskellでは、car、cdrの長短関係を逆にしたinit、lastというのもあるらしい。

アクションと関数の違いが、ちょっとよく分からないけど、アクションは関数呼び出しみたいなもんで、関数と呼んでいるものは関数ポインタなのかしら。

EmacsHaskellモードを入れたけど、どうもインデントがおかしい。

main = do cs <- getContents
          putStr cs

のように書きたいのに、

main = do cs <- getContents
                putStr cs

となってしまって、コンパイルが通らない……。

do構文は複数の式をまとめるもので、複数の式を書くときには、ブレースと「;」でCっぽく書く書式のほか、インデントで式の先頭をそろえるオフサイドルールというのがあるそうだ。それが使えない。どこかに設定があるのかな? というより、Haskellはコーディングスタイルがそれほど確立してないそうで、そういう話を聞くと、Pythonのようなスパルタンな強制も意味あるのかなぁと思ってしまう。

とりあず、

main = do {cs <- getContents;
                putStr cs}

と書くことにした。

cat -nコマンドを作るという例題を打ち込んでみた。Hakellは静的型付け言語だけど、型推論をしてくれるので、明示的に書かなくてもコンパイルが通れば型チェックはオッケー。

main = do {cs <- getContents; 
                 putStr $ numbering cs}

numbering cs = unlines $ map format
               $ zipLineNumber $lines cs

zipLineNumber xs = zip [1..] xs

format (n, line) = rjust 3 (show n) ++ " " ++ line

rjust width s = replicate (width - length s) ' ' ++ s

関数の型宣言をちゃんと書くと、

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 3 (show n) ++ " " ++ line

rjust :: Int -> String -> String
rjust width s = replicate (width - length s) ' ' ++ s

となる。確かに型が書いてあるほうが読みやすいし、安心感もある。

関数の型といっても、数値や型を明示するやり方、「6 :: Int」と似ている。こういう対称性とかシンプルさって好きかも。引数と返り値が矢印でつらつらつながっているのは、関数の合成とかarityとかcurryとかあたりが関係してくる話なのかしら。何やらすごくおもしろそうな予感。

文字列のStringが実は文字のリスト([Char])でしかないというのなんかも、話がシンプルでいいなと思う。

で、ghcコンパイルする代わりに、runghcでインタプリタっぽい実行(実際はコンパイルしてる?)した結果はこんな感じ。

% runghc catn.hs <catn.hs
  1 main = do {cs <- getContents; 
  2                  putStr $ numbering cs}
  3 
  4 numbering :: String -> String
  5 numbering cs = unlines $ map format
  6                $ zipLineNumber $lines cs
  7 
  8 zipLineNumber :: [String] -> [(Int, String)]
  9 zipLineNumber xs = zip [1..] xs
 10 
 11 format :: (Int, String) -> String
 12 format (n, line) = rjust 3 (show n) ++ " " ++ line
 13 
 14 rjust :: Int -> String -> String
 15 rjust width s = replicate (width - length s) ' ' ++ s

ちょっといじって、あちこちにreverseを入れると、数字の数え上げを逆順にしたり、各行の中身を反転させたりといったことが簡単にできる。

9行目の「zip [1..] xs」とかが、えぐいなと思った。Rubyなら、

(1..3).zip(["a", "b", "c"])
=>  [[1, "a"], [2, "b"], [3, "c"]]

と書くんだろうけど、遅延評価がないとzip対象の2つが同じ長さであることをプログラマが何らかの仕組みで保証しないとダメという制限がある。

>> (1..5).zip(["a","b","c"])
=> [[1, "a"], [2, "b"], [3, "c"], [4, nil], [5, nil]]

だし。

もしかしてオイラー・プロジェクトみたいな算数っぽい問題は、RubyよりもHaskellのほうが書きやすいのじゃないかという気がしてきた。リスト内包表記とか、すごく便利そう。RubyでもArray#find_allでやれば似たようなものかもしれないけど。リスト内包表記ってPythonにもあった気がするけど、Rubyはなぜ取り入れないんだろうか。