あどけない話

インターネットに関する技術的な話など

HaskellとDSL

LL Planets の「メタプログラミングの光と闇」で Haskell について話してきました。PerlPythonRuby が概ね内部 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人もいない」と言いましたが、読めないのは構文木のデータ構造であって、ロジックではありませんので悪しからず。