LL Planets の「メタプログラミングの光と闇」で Haskell について話してきました。Perl、Python、Ruby が概ね内部 DSL を作る話だったのに対し、Haskell では外部DSLを内部に埋め込むという話をしました。短い時間で説明不足になった感があるので、この記事で二点ほど補足します。
- Haskell では文法がうまく設計されており、コードを書けば自然とDSLっぽくなるので、わざわざ内部DSLなんて言わない。それよりもコンビネータという考え方を学ぶ方が新しい視野がひらけてよい。
- Haskell ではパーサーを作るのが簡単。だから自分で言語を作るのも簡単。その言語を外部ファイルから読み込んでもいいし、HERE DOCUMENT のように内部に貼付けることもできる。
関数を二項演算子として扱う
Haskell では関数をバッククォートで囲むと二項演算子になります。
isPrefixOf "Java" "JavaScript"
は、以下のようにも書けます。
"Java" `isPrefixOf` "JavaScript"
英語のように読めますね。
二項演算子は自由に定義できる
二項演算子は自由に定義できるので、ライブラリがいろんな二項演算子を定義しています。たとえば、パスを連結する二項演算子は以下のような感じです。
"/foo/" </> "bar"
かなり読みやすいでしょ?
Parsec で、選択するパーサーを列挙するには以下のように書きます。
value = jsObject <|> jsArray <|> jsNumber <|> jsString ...
ちなみに、自分で定義する二項演算子は、結合規則と結合順位も定義できます。
引数には関数を取れる
関数型言語なので、関数はファーストクラスです。ですから、関数を引数として渡せます。
例として、以下のように replicateM_ に times という別名を付けてみましょう。
times = replicateM_
そうすると Ruby っぽく書けます。
10 `times` putStrLn "Hello!"
「だから何だ」という例ですが。。。
data
data を直和型(C でいう union)として使う場合、それぞれにタグが付きます。たとえば、数式を表す data は以下のように表現できます。
data Expr = C Int | Add Expr Expr | Sub Expr Expr deriving Show
Haskell では、data のリテラルがスマートなので、リテラルはこのまま書けます(僕はこれをスマートリテラルと名付けたい)。たとえば、1 + 2 を表現するリテラルは、以下のようになります。
Add (C 1) (C 2)
data の定義を見ると、タグは引数への関数適用の形をしていますね。Haskell では、タグも関数のように扱えます。なので、バッククォートで囲むと二項演算子として使えます。
C 1 `Add` C 2
かなり読みやすくなりました。ところで、Haskell では二項演算子を自由に定義できるのでした。当然、data のタグも二項演算子として定義できます。
data Expr = C Int | Expr :+ Expr | Expr :- Expr deriving Show
リテラルは、さらに数式に近づきます。
C 1 :+ C 2
この data の使い道がよく分からない人がいるかもしれません。たとえば、自分でプログラミング言語を設計することを想像してみて下さい。数式のパーサーが必要になります。そのパーサーが、この data を返すようにするのです。
括弧
Haskell 初心者は、以下のような括弧だらけのコードを書きます。
func x = foo (bar (baz x))
関数適用の演算子 $ を知ると、以下のように括弧がなくなります。
func x = foo $ bar $ baz x
もう少し Haskell に慣れると、引数を消して関数合成を使うようになります。
func = foo . bar . baz
オブジェクト指向に染まった人に対する注意ですが、これはメソッドチェインじゃないですよ。関数合成です。データは右から左へ流れます。
コンビネータ
Haskeller なら、ある問題を解く際、まずその問題を表現する data を定義します。そして、そのデータを生成したり処理したいする小さな関数を書きます。そして、それらの関数を組み合わせる演算子(あるいは関数)を定義します。この演算子をコンビネータと呼びます。
たとえば、パーサーを作るための Parser コンビネータを作るなら、まず Parser という型を定義します。
data Parser a = Parser (String -> (Maybe a, String))
そして、小さなパーサーを定義します。たとえば、ある一文字にマッチし、その文字を自体を返す char というパーサーを定義します。
char :: Char -> Parser Char char = ...
さらに、パーサーを連結する演算子 >>= と選択する演算子 <|> を定義します。
(>>=) :: Parser a -> (a -> Parser b) -> Parser b p >>= f = ... (<|>) :: Parser a -> Parser a -> Parser a p1 <|> p2 = ...
実際に大きなパーサーを書くときは、あらかじめ定義されている小さなパーサーをコンビネータ >>= と <|> で組み上げていきます。このボトムアップな方法がとても大切です。元になるパーサーは小さく、バグがなかなか入り込みません。それを組み上げるという、バグが入り込みにくい方法で、大きなパーサーを作るのです。
多くの言語では、パーサーを書くとなると、正規表現を使ってなんとかしようとすると思います。正規表現が大きくなると、メンテナンスするのが困難になるのは、みなさんも経験しているでしょう。
あれほど面倒だったパーサーも、Haskell では一番簡単なプログラミングの一つになります。少し訓練は必要ですけどね。
たとえば、Yacc でパーサーを定義した場合、C のコードを生成されますが、Yacc 自体は C ではありません。Yacc で書いたコードは、C コンパイラーの型検査を受けることはできません。一方で、Haskell で書いたパーサーは、Haskell そのものです。つまり、型検査のご加護の下、コンパイラーがたくさんのバグを発見してくれます。
準クォート
Haskell でパーサーを書くのは簡単なので、外部 DSL として言語を作成するのも簡単です。その言語を納めたファイルが外部に存在しても構いません。たとえば、サーバーの設定ファイルがそれにあたるでしょう。
また、準クォートを使って Haskell のコードの内部に埋め込むこともできます。以下に Yesod という Web Application Framework でページを2つ作る例を示します。
mkYesod "Demo" [$parseRoutes| / HomeR GET /page1 Page1R GET |] getHomeR = defaultLayout [$hamlet| <a href="@{Page1R}">Go to page 1. |] getPage1R = defaultLayout [$hamlet| <a href="@{HomeR}">Go home. |]
[$foo| ... |] が準クォート記号で、foo が囲まれた部分を処理するパーサーになります。クォートではなく、準クォートと呼ばれるのは、内部の変数を展開できるからです。どこを展開するのかを表す記号は自由に決められます。だって、自分でパーサーを書くのですから。
Template Haskell
構文木のタネから構文木を育てる Template Haskell の例として、以下のようなコードを紹介しました。
mkFrom :: Name -> Q [Dec] mkFrom t = do -- 構文木の種を得る TyConI (DataD [] name [] [RecC _ gs] []) <- reify t -- 構文木を育てる x <- newName "x" let fnm = mkName $ "from" ++ nameBase name tosql = mkName "toSql" trans (get,_,_) = VarE tosql `AppE` (VarE get `AppE` VarE x) ds = map trans gs bdy = NormalB (ListE ds) cls = [Clause [VarP x] bdy []] return [FunD fnm cls]
「このコードが読める Haskeller は、30 人に 1人もいない」と言いましたが、読めないのは構文木のデータ構造であって、ロジックではありませんので悪しからず。