モナドを実装する(Vim script編)


2011年 05月 21日

発端

モナドの正体が分かると、
次はモナドを実装してみたくなるものです。

前回は試しに 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

returnm 型が持つメソッドとして定義することにしましょう。

  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 では不可能なので諦めます。

MaybeJustNothing

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

NothingJust と同様に実装をさぼることにします。

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)

しかしよくよく考えてみると上記の修正で改善されたのは Maybebind だけです。
個々のモナドでこれと同様の処理を書きたくはありません。
という訳でモナドの 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 編です。