ふつける 2,3 章

ふつけるの p.44 head コマンドから。

main = do cs <- getContents
          putStr $ firstNLines 10 cs

firstNLines n cs = unlines $ take n $ lines cs

lines 関数で文字列を改行で区切ってリストを作って、take 関数で最初の10要素を取る。そして、unlines 関数でそれを改行文字で連結する、と。
lines は SML では次のように書ける。

fun lines s = String.fields (fn c => c = #"\n") s

String.fields に似た関数として String.tokens があるが、ここでは String.fields を使うのがよい。これらの違いは、区切り文字が連続した場合に String.fields は空文字列を含むのに対して String.tokens は含まないという点にある。
なお、lines の定義として

fun lines s = String.fields (op= #"\n") s

とすることはできない。SML では二項演算子は二要素のタプルを取る。つまり、op= の型は

''a * ''a -> bool

なっているからである。''a というのは等値型、すなわち = で比較できる型を表す型変数だ。Haskell には型クラスがあるが、それをもっと限定したものとも考えられる。
OCaml では = は多相型の関数になっていて、型

'a -> 'a -> bool

を持つ。ゆえに、引数に関数を渡したりしてもコンパイル時にエラーにはならず、実行時に例外が発生する。

だいぶ話がずれた。take 関数は最初の n 要素を取り出す関数。わりと使いそうなものなのに OCaml にはないらしい。SML には List.take があるのに。

次に tail コマンドを作っているが読み飛ばして練習問題に挑戦。

標準入力から読んだバイト数を数えるコマンド、countbyte を書きなさい。

えーと

open TextIO
val _ = print(Int.toString (String.size (inputAll stdIn)))

こうかな? 解答例とはだいぶ違うが気にしない。
ちなみに解答例は

main = do cs <- getContents
          print $ length cs

となっている。Haskell では型クラスがあるから明示的に Int を String に直したりしなくていいみたいだ。
バイト数ではなく単語数を数える2番もあるが、 ML には words 関数がないのでパス。(String.tokens を使えばすぐ定義できそうだが)

さて、3 章に突入。型と高階関数について。
Haskell には型推論がある。わーい。でも Haskell ではわりと型を書いてることが多いと思う。OCaml ではあまり型は書いていない。書いたほうが読むときは嬉しいと思うが、自分もあまり書いていない。書きにくいんだよな。例えば、

let plus (x:int) (y:int) :int  = x + y

このように汚らしいことになってしまう。こんなことをするくらいなら mli ファイルやシグネチャの中にだけ書いたほうが美しい。

Haskell の型の名前は大文字か。あと、Int のリストの型が [ Int ] なのは微妙。これに関しては int list のほうが統一感があって好きだ。まあそんなに重要なことでもない。
length 関数の型は

[a] -> Int

とのこと。a は型変数で、OCaml でいう 'a だな。型の名前は大文字で始まって型変数の名前は小文字で始まるということか。'a はアルファと読んでるので、Haskell のはちょっと混乱しそう。慣れたら一緒か。ふつう型変数の名前は 'a とか 'b 程度しか使わないが、 'c になるとなんて読んでいいかわからなくなる。ガンマでいいのか? 'c とガンマは前二つに比べてあまり結び付かない。
Haskell では明示的に型を記述しなければならない場合があるらしい。OCaml ではどうなんだろ。必ず型推論できる、という話を聞いたことがあるような気もするが。

p.62 の高階関数。タブ文字をスペースに変換する expand コマンドを作る。

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]

Haskell では値が等しさを調べるのに (==) 関数を使う。これは OCaml でいう (=) だ。OCaml の (==) は Haskell にはない。
concat は SML の List.concat と同じ。ただし、Haskell では String は Char のリストなので String.concat とも同じように使える。

次に expand コマンドの改良バージョンが出てくる。(型宣言は省略)

tabStop = 8

main = do cs <- getContents
          putStr $ expand cs

expand cs = concatMap expandTab cs

expandTab '\t' = replicate tabStop ' '
expandTab c = [c]

concatMap は map して concat する関数。そのまま。replicate n x は x を n 個含むリストを作る関数。この場合は String.make と等しい。あと新しいのはパターンマッチ。SML ではほぼ同じように書けて

fun expandTab #"\t" = "        "
  | expandTab c = str c

となる。OCaml では function を使って、

let expand_tab = function
  '\t' -> String.make 8 ' '
| c -> String.make 1 c

というようにする。
SML では String.translate 関数が定義されていて、これを使うと expand は次のようにできる。

(* String.translate : (char -> string) -> string -> string *)
fun expand s = String.translate expandTab s

ほとんど concatMap を String.translate に置き換えただけだ。

3 章の練習問題に挑戦。

標準入力の 'a' と 'A' を入れ換えるコマンド、swapa を書きなさい。

open TextIO

fun swapa #"a" = #"A"
  | swapa #"A" = #"a"
  | swapa c = c

val _ = print(String.map swapa (inputAll stdIn))

よし。解答例は

main = do cs <- getContents
          putStr $ map swapa cs

swapa :: Char -> Char
swapa 'a' = 'A'
swapa 'A' = 'a'
swapa c = c

上と下の大きな違いとして、swapa と main の場所の違いがある。SML(や OCaml ) では前に出てきた変数しか参照できないのに対し、Haskell では後に出てくる変数も参照できる。OCaml で相互再帰的な関数を書くには and を使う。例として自然数の偶奇を判定する関数 even と odd を定義してみる。

let rec even = function
  0 -> true
| n -> odd (n-1)

and odd = function
  0 -> false
| n -> even (n-1)

のように使う。let の後の rec を忘れてはいけない。

3 章終わり。今日も ML 分が多すぎだ。