読者です 読者をやめる 読者になる 読者になる

CoffeeScriptで遅延評価(もどき)を実装しよう

遅延評価をまねる

厳密に言えば、遅延評価は式の評価戦略 evaluation strategy のひとつだ。JavaScriptとかLuaは正格な評価を行い、Haskellは非正格な評価を行う。

正格な言語で非正格評価を真似る方法のひとつに、引数をすべてクロージャーでくるみ、使うときにcallする方法がある。これが一番簡単な方法だと思う。

CoffeeScriptやMoonScriptはアロー記法でクロージャーが書けるので、すごく簡単に実現できる。

# CoffeeScript
# 正格
square = (x) -> x * x
add = (x, y) -> x + y
console.log square 42
console.log square add 12, 34

# 遅延
squareDelay = (x) -> x() * x()
addDelay = (x, y) -> x() + y()
console.log squareDelay -> 42
console.log squareDelay -> addDelay (-> 12), (-> 34)

引数の評価を遅延する関数は普通の関数と呼出し方が違うので、混同しないように関数名末尾にDelayを付け、遅延関数と呼ぶことにする。遅延関数の呼出し規約を次のように決めておこう。

  • 引数をクロージャーで包み、引数無しで関数を呼び出すと値が取り出せるようにする。
  • 返り値は、特にクロージャーなどで包まずに、そのまま返す。

普通の関数を遅延関数に変換する便利な関数も用意しておこう。

delayfunc = (obj, f) ->
  (args...) -> f.apply obj, (arg() for arg in args)

console.log (delayfunc this, square)(-> addDelay (-> 12), (-> 34))

メモ化

遅延評価は先程の工夫で真似できるようになったが、このままでは引数が複数回評価されてしまう。解決の方法のひとつに、引数の評価回数が1回以下になるように遅延関数を実装する方法がある。

squareDelay = (x) ->
  x = x()
  x * x

しかし、遅延関数同士を組み合わせた時、評価される回数が1回以下になることを保証するのは難しい。

そこで、複数回評価されると困るような場合には、メモ化を行うことにする。メモ化は一度評価した値を記憶しておき、後で再利用する。

メモ化するための関数 lazy を用意する。

lazy = (f) ->
  x = undefined
  return ->
    if x is undefined
      x = f()
    return x

console.log squareDelay lazy -> addDelay (-> 12), (-> 34)

遅延データ構造

コンストラクターが遅延関数になっている遅延データ構造を作ってみよう。

class List
  constructor: (@head, @tail) ->
  isNil: -> @head is undefined
  forEach: (callback, thisObject) ->
    if not @isNil()
      if thisObject is undefined
        callback @head()
      else
        callback.call(thisObject, @head())
      @tail().forEach(callback, thisObject)
  map: (f) ->
    if @isNil()
      new List()
    else
      new List (lazy => f @head()), (lazy => @tail().map(f))
  take: (n) ->
    if n <= 0 or @isNil()
      new List()
    else
      new List (lazy => @head()), (lazy => @tail().take(n - 1))

list = (elems...) ->
  return new List() if elems.length is 0
  head = elems[0]
  new List (-> head), (lazy -> list.apply this, elems[1..])

forEachが呼ばれた段階になってからsquareMessageが呼び出されることを確認しよう。

squareMessage = (x) ->
  console.log 'squared ' + x
  x * x

l = list(1, 2, 3, 4, 5).map(squareMessage).take(3)
console.log 'l.forEach'
l.forEach console.log