モナドの正体が分かると、
次はモナドを実装してみたくなるものです。
前回は試しに Python でモナドを実装してみましたが、他の言語でも実装できないことはありません。
ただクロージャや部分適用が簡単に使えない言語では本質的でないところで苦労する羽目になるので、前回は Python を使いました。
という訳で今回は “エディター界のPHP” でお馴染みの Vim script でモナドを実装することにしましょう。
なお、今回作成した Vim script によるモナドの実装は GitHub で公開中です。
最初に Maybe
のようなモナドの具体例を実装するためのフレームワークを作っておいて、次に Maybe
の実装例を示すことにしましょう。
Vim script はプログラミング言語として見る分には貧弱ですが、 dictionary (他の言語で言うところのマップ/ハッシュテーブル/連想配列に相当するもの)はありますし、関数への参照も値として扱えますから、プロトタイプベースのプログラミングは、一応、可能です。ここでは Vim 7.3 を使うことにします(Vim 6.x までの Vim script には dictionary すらないため実装する気が起きません)。
という訳で Maybe
のようなモナドの具体例に共通のプロトタイプを実装していきましょう。
モナドの構成要素は以下の3つです:
m
m
でラップするための関数 return
>>=
これらを順に実装していきましょう。
m
まず m
を作るための関数を定義しましょう(本質的に名前(name
)は不要なのですが、後で値を表示するときなどに便利なように付加しています)。
function! monad#create_type_constructor(name)
return extend({'type': {'name': a:name}}, s:prototype, 'keep')
endfunction
let s:prototype = {}
s:prototype
はモナドの振舞いを dictionary で表現したものです。
具体的な定義は後述します。
return
return
は m
型が持つメソッドとして定義することにしましょう。
function! s:prototype.return(a)
return extend({'value': a:a}, self, 'keep')
endfunction
>>=
Haskell では >>=
のようにオレオレ演算子を自由に定義できますが、
大多数の言語ではそのような芸当はできませんので、
ここでは m
型のメソッド bind
として定義することにしましょう。
function! s:prototype.bind(a_to_m_b)
throw 'bind operator is not defined for type: ' . self.type.name
endfunction
bind
の実装は m
によって異なるため、プロトタイプでは「bind
が未実装である」ことを示す例外を投げるだけに留めておきます。
なお、Python で実装した場合は演算子オーバーロードを利用して見た目をかわいくできましたが、 Vim script では不可能なので諦めます。
Maybe
と Just
と Nothing
Haskell では
data Maybe a = Just a | Nothing
という1文で
Maybe
Maybe
型の値を作る関数 Just
Maybe
型の値(を作る関数(のようなもの)) Nothing
を定義することができます。
先程のプロトタイプを用いて順に実装していきましょう。
まず Maybe
型は以下のようにして定義できます:
let g:Maybe = monad#create_type_constructor('Maybe')
Maybe
の >>=
は以下のように定義できます:
function! g:Maybe.bind(a_to_m_b)
if self is g:Nothing
return g:Nothing
else
return a:a_to_m_b(self.value)
endif
endfunction
Just
は、本来なら Just
を表す型を作っておくところですが、面倒なので Maybe.return
のラッパーに留めておきます:
function! Just(a)
return g:Maybe.return(a:a)
endfunction
Nothing
も Just
と同様に実装をさぼることにします。
let g:Nothing = g:Maybe.return({'identity': 'Nothing'})
function! Nothing()
return g:Nothing
endfunction
Maybe
試運転実装した Maybe
のテストとして、
入れ子になった dictionary の値を指定したキーで次々と参照するコードを書いてみましょう。
テストデータとしては以下のものを使うことにします:
let d = Just({‘kana’: {‘arpeggio’: ‘https://github.com/kana/vim-arpeggio’}})
Vim script では残念ながら Lisp で言うところの lambda
がないため、>>=
に渡す処理は、名前付きの関数を定義しておいて、それを指定するしかありません。
取り敢えず以下の関数を定義しておきましょう:
function! LookupByKana(a)
return has_key(a:a, 'kana') ? Just(a:a.kana) : Nothing()
endfunction
function! LookupByUjihisa(a)
return has_key(a:a, 'ujihisa') ? Just(a:a.ujihisa) : Nothing()
endfunction
function! LookupByArpeggio(a)
return has_key(a:a, 'arpeggio') ? Just(a:a.arpeggio) : Nothing()
endfunction
実行例としては以下のような結果になります:
echo d.value
" ==> {'kana': {'arpeggio': 'https://github.com/kana/vim-arpeggio'}}
echo d.bind(function('LookupByKana')).value
" ==> {'arpeggio': 'https://github.com/kana/vim-arpeggio'}
echo d.bind(function('LookupByKana')).bind(function('LookupByArpeggio')).value
" ==> https://github.com/kana/vim-arpeggio
echo d.bind(function('LookupByUjihisa')).value
" ==> {'identity': 'Nothing'}
echo d.bind(function('LookupByUjihisa')).bind(function('LookupByArpeggio')).value
" ==> {'identity': 'Nothing'}
>>=
のインターフェースの改善(1)上記のテストを見て明らかなように、このモナドの実装は実用的ではありません。
これはひとえに Vim script の制約によります。
まず lambda
の不在です。これがないために >>=
へ渡す処理をその都度関数として定義しなければなりません。
Haskell であれば以下のように記述できるので、これと比較すると悲惨としか言いようがありません。
go = Just d >>= lookupBy "kana" >>= lookupBy "arpeggio"
where
lookupBy = flip lookup
lookup :: Dictionary -> Key -> Maybe a
d :: Dictionary
という訳で >>=
のインターフェースに少々手を加えて実用性を改善しましょう。
これには bind
の実装を工夫して「部分適用もどき」が実現できれば大分改善されます。
例えば以下のような感じです:
function! g:Maybe.bind(a_to_m_b, ...)
if self is g:Nothing
return g:Nothing
else
return call(a:a_to_m_b, [self.value] + a:000)
endif
endfunction
こうすれば以下のように使用することができます。
function! Lookup(a, key)
return has_key(a:a, a:key) ? Just(a:a[a:key]) : Nothing()
endfunction
echo d.bind(function('Lookup'), 'kana').bind(function('Lookup'), 'arpeggio').value
echo d.bind(function('Lookup'), 'ujihisa').bind(function('Lookup'), 'arpeggio').value
また、既にお気付きの通り、 Vim script では関数への参照は function('FunctionName')
で取得します。
いちいち呼び出し側で function('FunctionName')
と書くのは冗長ですので、
これを省略できるようにしておきましょう。
例えば以下のような感じです:
function! g:Maybe.bind(a_to_m_b, ...)
if self is g:Nothing
return g:Nothing
else
if type(a:a_to_m_b) == type(function('function'))
let F = a:a_to_m_b
else " type(a:a_to_m_b) == type('')
let F = function(a:a_to_m_b)
endif
return call(F, [self.value] + a:000)
endif
endfunction
こうすればさらに簡潔な記述で使用することができます。
echo d.bind('Lookup', 'kana').bind('Lookup', 'arpeggio').value
echo d.bind('Lookup', 'ujihisa').bind('Lookup', 'arpeggio').value
>>=
のインターフェースの改善(2)しかしよくよく考えてみると上記の修正で改善されたのは Maybe
の bind
だけです。
個々のモナドでこれと同様の処理を書きたくはありません。
という訳でモナドの s:prototype
の方を修正して、
全てのモナドで先程の「部分適用もどき」が使えるようにしましょう。
まず、個々のモナドでの >>=
の実装は __bind__
で定義することにしましょう:
function! s:prototype.__bind__(cont)
throw 'bind operator is not defined for type: ' . self.type.name
endfunction
次に、ユーザーが直接呼ぶことになる bind
については s:prototype
側で「部分適用もどき」の面倒を見ることにします:
function! s:prototype.bind(a_to_m_b, ...)
let cont = {}
if type(a:a_to_m_b) == type(function('function'))
let cont.a_to_m_b = a:a_to_m_b
else " type(a:a_to_m_b) == type('')
let cont.a_to_m_b = function(a:a_to_m_b)
endif
let cont.partial_arguments = a:000
let cont.inue = function('monad#_bind')
return self.__bind__(cont)
endfunction
function! monad#_bind(a) dict
return call(self.a_to_m_b, self.partial_arguments + [a:a])
endfunction
最後に、 __bind__
の具体的な実装例は以下のようになります:
function! g:Maybe.__bind__(cont)
if self is g:Nothing
return g:Nothing
else
return a:cont.inue(self.value)
endif
endfunction
以前は a -> m b
に相当する関数を直接 bind
で受け取って呼び出していました。
これを「部分適用もどき」に対応したもの cont
にラップし、__bind__
は cont
を受け取ってそれを呼ぶ形に変えました。
cont
そのものは関数ではないので cont(value)
のように呼び出すことはできず、cont.foo(value)
のような形で呼び出さなければなりません。
各モナドで __bind__
を実装する際に cont.foo(value)
の形で呼び出すとなると、foo
をどのような名前にしても今一でしたので、上記のような名前にしてあります。
これで Vim script でもオレオレモナドが実装し放題です。やりましたね。
次回は Emacs Lisp 編です。