2011年4月25日

Haskell 手習い

「ふつうの Haskell プログラミング」をつらっと読んで Haskell を書いてみたメモです。 Haskell のメリットとこの本を読む目的は書籍の 9 ページにざっくりとまとめられています。

遅延評価とは、引数を必要なときだけ評価するという、ただそれだけのことです。 それだけのことですが、それがどういう意味を持つかは、実際に体験してみないとよくわかりません。 ある意味、本書は遅延評価の嬉しさを書くためにあると言ってもよいでしょう。

注力すべきことだけを記述するため、余計なことは極力排除し、扱う問題をできるかぎり細分化していきます。 分割統治の「分割」を徹底的にこだわるイメージでしょうか。 書籍の後半では、Haskell のつまずきポイントである「モナド」について「床下配線」と表現しています。 関係ないことはまとめてどこかに押し込める、という雰囲気が分かりやすいですね。

書籍を離れてみると、Haskell とは関数型言語で云々かんぬん... という話もぽつぽつありますが、 個人的には次の一文がもっともしっくりくるまとめでした。

"Haskell は簡単なことと難しいことが入れ替わったような言語で..." / Haskell とは http://www.shido.info/hs/haskell1.html

構文や使い方、それぞれの概念は書籍を読むとして、ここでは簡単なプログラムを書いてみます。 まずは実行環境を整えて、日本語を扱えるようにします。

実行環境と日本語入出を準備する

次の2つの記事を参考にして環境を準備しました。 手元の環境は Mac OSX 10.5 です (ちょっと古いので買い替えようかな)。

macports からインストールします。 おそらく他のパッケージ管理システムでも類似のパッケージが存在すると思います。

$ sudo port install ghc
$ sudo port install hs-utf8-string

ghc-pkg でパッケージ一覧を表示させてみます。

$ ghc-pkg list
/opt/local/lib/ghc-6.10.4/./package.conf:
    Cabal-1.6.0.3, HUnit-1.2.0.3, QuickCheck-1.2.0.0, array-0.2.0.0,
    base-3.0.3.1, base-4.1.0.0, bytestring-0.9.1.4, containers-0.2.0.1,
    directory-1.0.0.3, (dph-base-0.3), (dph-par-0.3),
    (dph-prim-interface-0.3), (dph-prim-par-0.3), (dph-prim-seq-0.3),
    (dph-seq-0.3), extensible-exceptions-0.1.1.0, filepath-1.1.0.2,
    (ghc-6.10.4), ghc-prim-0.1.0.0, haddock-2.4.2, haskell-src-1.0.1.3,
    haskell98-1.0.1.0, hpc-0.5.0.3, html-1.0.1.2, integer-0.1.0.1,
    mtl-1.1.0.2, network-2.2.1.2, old-locale-1.0.0.1, old-time-1.0.0.2,
    packedstring-0.1.0.1, parallel-1.1.0.1, parsec-2.1.0.1,
    pretty-1.0.1.0, process-1.0.1.1, random-1.0.0.1,
    regex-base-0.72.0.2, regex-compat-0.71.0.1, regex-posix-0.72.0.3,
    rts-1.0, stm-2.1.1.2, syb-0.1.0.1, template-haskell-2.3.0.1,
    time-1.1.4, unix-2.3.2.0, utf8-string-0.3.5, xhtml-3000.2.0.1

色々ありますが、最後の一行に utf8-string を確認できます。 インタープリタを起動して Prelude を確認できれば良いと思います。

$ ghci
GHCi, version 6.10.4: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer ... linking ... done.
Loading package base ... linking ... done.
Prelude>

CSV っぽいファイルの特定パターンを置換する

実際に Haskell プログラムを書いてみます。 ここでは、カンマで区切られたテキストファイルにおいて、 フィールドが「味スタ」の場合に「味の素スタジアム」に変換するものを作ります。 正確な表現ではありませんが、実現したいことは次のことです。

  1. 標準入力から文字列を読み込む。
  2. 一行ごとに区切って、何か処理して、行ごとに改行して出力する。
  3. 何かの処理とは、カンマで文字列を区切って、特定パターンを置換すること。
  4. 特定パターンとは、「味スタ」を「味の素スタジアム」に置換すること。 同じ要領でパターンを追加できると嬉しい。
  5. 置換が終わったら、カンマで連結して一行の文字列に戻す。
  6. 結果を標準出力に書き出す。

そもそも "CSV" とは何か?という問題には深入りしません。 RFC4180 とエクセルの互換性とか、ロケールがフランスなどの場合には小数点がカンマだから云々かんぬん... という話に立ち入ってしまうと大変なので、上述の仕様に簡略化しました。 ダブルクォーテションでの引用やエスケープ、区切り文字以外の用途でのカンマは存在しないものとします。 フィールドの中に改行も許しません。

なお、Haskell で CSV をきちんと扱う場合にはこちらが参考になるでしょう。 Parsec を使います。

ということで、実装してみました。 たぶんもっと効率的な書き方があると思いますが、一番最初はこんな感じで力尽きました。 組み込み関数 (or Prelude で定義されているもの) をもう少し調べた方が良さそうです。

import qualified System.IO.UTF8 as U

main = do cs <- U.getContents
          U.putStr $ unlines $ parse $ lines cs

parse :: [String] -> [String]
parse ss = map replace ss

replace :: String -> String
replace cs = join $ map rewrite $ split cs

split :: String -> [String]
split cs = words $ map csv2tsv cs
  where
    csv2tsv :: Char -> Char
    csv2tsv ',' = '\t'
    csv2tsv c = c

join :: [String] -> String
join ss = map space2csv $ unwords ss
  where
    space2csv :: Char -> Char
    space2csv ' ' = ','
    space2csv c = c

rewrite :: String -> String
rewrite "味スタ" = "味の素スタジアム"
rewrite cs = cs

終わりに

Haskell の入門書を読みながら簡単な入出力のプログラムを書いてみました。 扱っているデータ型がどのデータ型のリストなのか、ということを理解するまでに時間がかかりました。 しかし、ひとつひとつの関数の型を書き下していけば理解できそうであることも分かりましたので、 日常的に書き続けるかどうかの問題とも言えます。

冒頭の書籍の「ふつうのまえがき」でも述べられているように、今、Haskell (もしくはその他の関数型言語) を学ぶ意義は、 異なる知見を得るためという部分が大きいと思います。

本書が目指すのは、Haskell という未知の言語の血を導入することでみなさんにプログラマとしてレベルアップしてもらうことです。 たとえ同じ言語を使い続けるとしても、Haskell の知見を活かせばプログラムはよりよくなるでしょう。 それこそが新しい言語を学ぶことの利益であり、また本書の狙いでもあるのです。

もちろん業務でゴリゴリと使っている人もたくさんいると思いますが、まだまだ「多数派」ではないのかな、と。

その他

コメントを投稿