SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

L2Lisp in Ruby

RubyによるモダンなLispの小さな実装


データの表現方法とLispの基本関数

 Lisp処理系の設計の大半はデータ構造の設計です。ある意味、Lispは一定の抽象的な仕様を満たすデータ操作ルーチン集にすぎませんから、具体的なデータ構造が決まると、どのような実装になるかが、おおよそ決まります。

 ここでは、データの表現方法として、consセル以外はすべてRubyのデータをそのまま使うことにします。LispのnilはRubyのnilで表現します。LispのシンボルはRubyのシンボルで表現します。シンボルの大域変数値はInterpクラスのインスタンス変数@symbolに格納します。こうすることで実装が簡単になります。とりわけ、Lispの組込み関数をRubyで自然に実現できます。

 例えばLispの加算と乗算は、加算や乗算を行うProcオブジェクトをシンボル:'+':'*'の大域変数値としてセットすることで実現できます。

加算と乗算の実現
@symbol = {}

@symbol[:'+'] = proc {|*x| x.inject(0) {|a, b| a + b}}
@symbol[:'*'] = proc {|*x| x.inject(1) {|a, b| a * b}}

 下記のようにL2Lispで加算や乗算を行うとき、内部でこのProcオブジェクトが呼び出されています。

加算と乗算の実行例
> (+ 1 2 3 4)
10
> (* 1 2 3 4)
24
>

 このことは+*の値から直接確認できます。RubyのProcオブジェクトの文字列表現が見られます。

> +
#<Proc:0x0001cac0@L2Lisp.rb:346>
> *
#<Proc:0x0001c818@L2Lisp.rb:347>

consセルを表すCellクラス

 Lispは、可変長の列であるリスト(list)を、consセルをノードとする二分木で表現します。各consセルはcarおよびcdrと名付けられた枝または葉を持ちます。

 car(カー)とcdr(クダー)の名称は歴史上のLisp実装に由来します。現在は言葉そのものに意味はありません。carとcdrの値から二分木のノードを構築(construct)する関数はcons(コンス)と言います。さらにノード自体も構築関数の名にちなんでconsセルまたは単にコンスと言います。consセルの図解については「JavaScriptでつくるSchemeインタプリタの基礎の基礎」などを参照してください。
 

 Rubyにはconsセルに直接該当するものがありませんから、クラスとして定義します。最低限carcdrの属性だけあれば十分ですが、便宜のため、文字列化メソッドを用意します。

 さらに、Rubyのデータ構造として自然に扱えるようにeachイテレータを用意し、Enumerableモジュールをインクルードします。これによりlengthメソッドはinject(0) {|x, y| x + 1}と簡潔に定義できます。各要素への関数の適用結果をArrayでなくconsセルとして得るために、Enumerable#collectに似たmapcarも用意します。

 eachイテレータはLispのmapc関数に相当します。mapcarイテレータはLispのmapcar関数に相当します。
 
Cellクラス
LL = L2Lisp                   # 便宜上の短縮名

MAX_EXPANSIONS = 5            # 再帰的に印字する深さ

class Cell
  include Enumerable
  attr :car, true
  attr :cdr, true

  def initialize(car, cdr)
    @car = car
    @cdr = cdr
  end

  def inspect
    return LL.str(self)
  end

  def each                    # Lispのmapcに相当
    j = self
    begin
      yield j.car
      j = j.cdr
    end while Cell === j
    j.nil? or raise ProperListExpected, j
  end

  def mapcar                  # Lispのmapcarに相当
    c = yield @car
    x = y = Cell.new(c, nil)
    j = @cdr
    while Cell === j
      c = yield j.car
      y = y.cdr = Cell.new(c, nil)
      j = j.cdr
    end
    j.nil? or raise ProperListExpected, j
    return x
  end

  def length
    return inject(0) {|x, y| x + 1}
  end

  def _repr(print_quote, reclevel, printed)
    if printed.has_key?(self)
      reclevel -= 1
      return ['...'] if reclevel == 0
    else
      printed[self] = true
    end
    case @cdr
    when nil
      s = LL.str(@car, print_quote, reclevel, printed)
      return [s]
    when Cell
      s = LL.str(@car, print_quote, reclevel, printed)
      t = @cdr._repr(print_quote, reclevel, printed)
      return t.unshift(s)
    else
      s = LL.str(@car, print_quote, reclevel, printed)
      t = LL.str(@cdr, print_quote, reclevel, printed)
      return [s, '.', t]
    end
  end
end # Cell


# 引数の文字列表現を得る
def str(x, print_quote=true, reclevel=MAX_EXPANSIONS, printed={})
  case x
  when Cell
    return '(' + x._repr(print_quote, reclevel, printed).join(' ') + ')'
  when Symbol then return x.to_s
  when String, Exception then return (print_quote) ? x.inspect : x
  else return x.inspect
  end
end
module_function :str

 実際にconsセルで作られるデータ構造は、本来の意味での二分木に限りません。閉路、つまりループ構造を作ることがあります。これは決してまれなことではなく、ある程度以上のLispプログラムによく現れます。ですから文字列化メソッドには、ループ構造をもつ循環リストに出会っても無限ループや無限再帰呼出しに陥らないことが求められます。

 そこで、木構造のconsセルから再帰的に文字列表現を得るとき、文字列化済みのconsセルを「ハッシュ表」に登録することで無限再帰呼出しを防止することにします。上記コードのprinted引数がそのハッシュ表です。下記の実験では循環リストを作っていますが、一定レベル以上は...で代用されることが確認できます。

JRubyでの循環リストの実験
$ jirb
irb(main):001:0> require "L2Lisp"
=> true
irb(main):002:0> def cons(x, y) L2Lisp::Cell.new(x, y) end
=> nil
irb(main):003:0> a = cons(1, cons(2, nil))
=> (1 2)
irb(main):004:0> a.car = a
=> ((((((...) 2) 2) 2) 2) 2)
irb(main):005:0> 

 Lispのリストは、長さ1以上の場合はconsセルで表現されますが、長さ0の場合はnilで表現されます。nilにはeachメソッドがないため、リスト一般に対する繰返しを記述するときは場合分けが必要です。場合分けを省くため、特異メソッドとしてnil.eachを定義する方法は、L2Lispを他のプログラムに組み込むときに不具合が出るおそれがあります。下記のようなモジュール関数を定義します。

def mapc(x, &block)
  case x
  when Cell then x.each(&block)
  when nil then ;
  else raise ProperListExpected, x
  end
end
module_function :mapc

基本5関数の実現

 Lispの基本5関数car、cdr、cons、atom、eqは下記のように実現できます。

RubyによるLispの基本5関数
@symbol[:car] = proc {|x| (x.nil?) ? nil : x.car}
@symbol[:cdr] = proc {|x| (x.nil?) ? nil : x.cdr}
@symbol[:cons] = proc {|x, y| Cell.new(x, y)}
@symbol[:atom] = proc {|x| (Cell === x) ? nil : :t}
@symbol[:eq] = proc {|x, y| (x == y) ? :t : nil}

 Rubyの==演算子は、文字列などいくつかのクラスに対し表現が等価かどうかを判別します。つまり、一般的なLispのeqよりも広い範囲で真を返します。しかし、nilとconsセルについてはeqと同じように振舞いますから、Lispとしては問題ありません。

 ==演算子をequal?メソッドに替えると一般的なeqに近くなりますが、他の変更も必要になります。実験してみてください。
実際のソース・ファイルを読んだ方は、atomの定義がこれより少し複雑になっていることに気付いたことと思います。これはマクロ引数の捕捉問題の対策のためです。詳しくは「続々L2Lisp:マクロの健全化」をみてください。
 

次のページ
Readerクラス

修正履歴

この記事は参考になりましたか?

特集記事連載記事一覧

もっと読む

この記事の著者

(鈴)(リン)

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/1492 2011/03/18 15:56

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー