ラベル Ruby の投稿を表示しています。 すべての投稿を表示
ラベル Ruby の投稿を表示しています。 すべての投稿を表示

2015年4月2日木曜日

[Ruby][Hipchat]Hipchat用のBotをLitaで作る(2)

前回に引き続き、LitaでBotを作る。
今回は簡単な応答をするようにしてみよう。

後でgemで公開するつもりなら、
$ lita handler lita-ハンドラ名称
とすれば、雛形を作ってくれるようだ。

今回は単純に、「@Bot名 hello」と書きこみがあったら、「〜さん、こんにちは。」と応答するだけのHandlerを作る。
ソースは以下の通り。
module Lita
  module Handlers
    class HelloHandler < Handler
      route(/^hello/, :hello, command: true)

      def hello(response)
        response.reply("#{response.user.name}さん、こんにちは。")
      end
    end
  end

  Lita.register_handler(Handlers::HelloHandler)
end
routeで応答するメッセージの正規表現と呼び出すメソッド(:hello)を指定し、
呼び出されるメソッドhelloでは response.user.nameでメッセージを書いたユーザ名を参照し、response#replyで、応答メッセージを返している。
routeメソッドで command: trueを渡しているのは、@Bot名で、Botを宛先にしたメッセージだけに応答するためである。
commandの詳細については、こちらを参照。

デバッグ中は lita_config.rb を以下のように設定すると、コマンドラインで動作確認できる。
config.robot.adapter = :shell
config.adapters.shell.private_chat = true 
ただし、この場合は、routeメソッドでcommand: false にし、 テスト時には、@Bot名を付けずに、helloだけにしないと応答しない。
      route(/^hello/, :hello, command: false)
コマンドラインでの実行例
$ lita
Type "exit" or "quit" to end the session.
Lita Bot > hello
Shell Userさん、こんにちは。
Lita Bot > 

2015年3月29日日曜日

[Ruby][Hipchat]Hipchat用のBotをLitaで作る

仕事場でHipchatを使い始めた。
Botを作って、いろいろと便利な機能を提供したい。
関連する情報を検索すると、Hubot + Hipchat用のアダプタという例が多数あった。
しかし、Debian Wheezy で試してみると、Hipchatのアダプタ(hubot-hipchat)のインストールに失敗してしまう。
最終的には、node.js のバージョン落として、v0.10.38でインストールできたのだけど。

気に入らなかったので、RubyでHipchatと連携できるものが無いのか調べたら、
Lita - A robot companion for your company's chat room
というのがあった。
こちらの解説記事がまとまっており、このページに書かれている手順で、動かすことができた。
以下、Botでinfo表示、whois、Googleのイメージ検索ができるようにする設定を、まとめておく

環境

環境は以下の通り。
  • Debian Wheezy
  • rbenvでruby-2.2.0をインストール済み
  • apt-getでredis-server をインストール済み

準備

Hipchatで利用するBotアカウントを作成しておく。
アカウント作成にはメールアドレスが必要。
gmailのアカウントがあれば、メールアドレスに+を使って、複数アカウトを作る方法がお手軽だ。 (ここらへんを参考に。)

Litaのインストール

Litaはgemでインストールできる。
$ gem install lita

Botの作成

Bot用のプロジェクトを作る。
$ lita new bot
botディレクトリに、新しいLitaのプロジェクトが作成される。

Gemfileを編集する。
HipChatアダプタを使う。
gem "lita-hipchat"
の行をコメントアウトする。
$ bundle install
で反映させる。

lita_config.rbを編集する。
# 名前を合わせておかないと起動時にエラーになるので注意。
config.robot.name = "Lita Bot"
# Hipchatを利用する。
config.robot.adapter = :hipchat
# ポート番号はデフォルトは1234になっていたが、6379にしないと起動しなかった。
config.redis.host = "127.0.0.1"
config.redis.port = 6379
# Hipchatの管理画面からXMPP/Jabber Account Informationに表示されるJabber IDを設定する。
config.adapters.hipchat.jid = "省略@chat.hipchat.com"
# Hipchatのアカウントのパスワード
config.adapters.hipchat.password = "省略"
# trueにしておくと、デバッグログが表示される。接続できない場合に便利。
config.adapters.hipchat.debug = true
# 利用するChat roomの名前か :all
config.adapters.hipchat.rooms = :all

Litaの起動

$ lita
設定が間違っているとエラーログが表示されるので確認する。
問題がなければ、BotはChat roomに参加する。

Hipchatで動作確認

@lita info
LitaとRedisの情報が表示される。

whoisとGoogleのイメージ検索の利用

Gemfileに次の設定を追加する。
gem "lita-google-images"
gem "lita-whois"
lisaを再起動する。
whoisは
@lita whois ホスト名
イメージ検索は
@lita image cat
で利用できる。


機能の拡張

Botが応答するwhoisやimageの処理は、Handlerとして実装できるようなので、まずは、単純に、ユーザの入力に応答をするものを作ってみよう。

参考

2014年4月6日日曜日

[OCaml][Common Lisp][Ruby][SICP]両替の組み合わせを数えるプログラムの実行速度

SICPにあった、両替の組み合わせを数えるプログラムを、Common Lisp、OCaml、Rubyで書いて、実行速度を比べてみた。

各言語のコード

Common Lisp

(defun change-count (amount)
  (change-count-aux amount 5))

(defun change-count-aux (amount kind)
  (cond
    ((= amount 0) 1)
    ((< amount 0) 0)
    ((= kind 0) 0)
    (t (+ (change-count-aux amount (1- kind))
      (change-count-aux (- amount (first-denomination kind)) kind)))))

(defvar *money* #(1 5 10 25 50))

(defun first-denomination (kind-of-coins)
  (elt *money* (1- kind-of-coins)))


OCaml

open Core.Std

let money = List.to_array [1; 5; 10; 25; 50]

let change_count amount =
  let first_denomination kind = money.(kind - 1) in
  let rec count amount kind =
    match (amount, kind) with
    | (0, _) -> 1
    | (amount', _) when amount' < 0 -> 0
    | (_, 0) -> 0
    | (_, _) -> (count amount (kind - 1))
                + (count (amount - (first_denomination kind)) kind)
  in
  count amount (Array.length money)
   
let () =
  print_endline "*start";
  flush stdout;
  printf "Change count 1000=%d\n" (change_count 1000)


Ruby

#!/usr/bin/env ruby

Money = [1,5,10,25,50]

def first_denomination(kind)
  Money[kind - 1]
end

def count(amount, kind)
  if amount == 0
    1
  elsif amount < 0
    0
  elsif kind == 0
    0
  else
    count(amount, kind - 1) + count((amount - first_denomination(kind)), kind)
  end
end

def change_count(amount)
  count(amount, 5)
end

print "*start\n"
printf("Change count 1000=%d\n", change_count(1000))


実行時間の比較

10ドルを1,5,10,25,50セントで両替する場合の組み合わせ数を求める時間を測定した。

Common Lispは Clozure CL 1.9を使用し、REPL上でtimeマクロで測定した。
OCaml は 4.01を使用。ocamlbuildでnative指定でコンパイルし、timeコマンドで測定した。
Rubyは2.0.0-p353を使用。timeコマンドで測定した。

環境はWindow8のVirtualBox上のDebian Wheezy。
CPUは Intel Core-i5 4200U 1.6GHz。
VirtualBoxには2CPUを割り当て。

Common Lisp

CL-USER> (time (change-count 1000))
(CHANGE-COUNT 1000)
took 4,475 milliseconds (4.475 seconds) to run.
During that period, and with 2 available CPU cores,
     4,752 milliseconds (4.752 seconds) were spent in user mode
         0 milliseconds (0.000 seconds) were spent in system mode
801451

OCaml

satoshi@debian:~/workspace/sicp/ch1$ time ./Ch1.native
*start
Change count 1000=801451
./Ch1.native  2.54s user 0.01s system 99% cpu 2.553 total

Ruby

satoshi@debian:~/workspace/sicp/ch1$ time ruby ch1.rb
*start
Change count 1000=801451
ruby ch1.rb  42.54s user 0.06s system 99% cpu 42.630 total


結果

Common Lisp(CCL1.9): 4.752sec
OCaml4.01: 2.54sec
Ruby: 42.54sec

何の根拠もなく、Common Lispの方がOCamlより速いのだろうと思っていたが、結果は逆で、OCamlの方が1.8倍ほど速かった。
圧倒的にRubyは遅かった。VM上でコードを実行するので、もう少し速いと思っていた。Ruby2.1だともう少し速いのかな?

その他

この規模のプログラムでは、各言語での読み易さには、あまり違いがない。OCamlのmatch 〜 with構文は、少し読み易いかなという程度。
Common LispやRubyと異なり、厳格な型チェックをするOCamlのコンパイルが通った後の、プログラム実行時の安心感は格別だ。

2013年12月18日水曜日

[Clojure][Ruby]RougeでSelenium WebDriverを使う

Ruby + Clojure = Rouge
Selenium WebDriverを使ってGoogle検索してみた。

Rubyは最新版を使用した。
$ ruby -v
ruby 2.0.0p353 (2013-11-22 revision 43784) [i686-linux]

Rougeのインストールと起動

gemでインストールできる。簡単だ。
$ gem install rouge-lang
REPLを起動するには rougeコマンドを使う。
$ rouge
Rouge 0.0.15
user=>

ついついprintlnとやってしまいそうになるが、putsでhello。

user=> (puts "hello, Rouge!")
hello, Rouge!
nil
user=> ^Dで終了

Selenium WebDriverを使ってみる

gemでselenium-webdriverをインストールしておく。
$ gem install selenium-webdriver

Rubyのコード

require "selenium-webdriver"

driver = Selenium::WebDriver.for :firefox
driver.navigate.to "http://google.com"

element = driver.find_element(:name, 'q')
element.send_keys "Clojure Ruby Rouge"
element.submit

puts driver.title

driver.quit


Rougeのコード

(require "selenium-webdriver")

(def driver (Selenium.WebDriver/for :firefox))

(-> driver
  (.navigate)
  (.to "http://www.google.com/"))

(def element (.find_element driver :name "q"))

(.send_keys element "Clojure Ruby Rouge")
(.submit element)

(puts (.title driver))

;; (.quit driver) ;; ブラウザを終了させないようにコメントアウト


Rougeで実行

拡張子はrgのようだ。
上記のコードはgoogle.rgとした。

rougeコマンドに渡せば良い。
最初分からずに(load-file "google.rg")とやってみたが、
load-fileは定義されていなった。
https://github.com/rouge-lang/rouge/blob/master/lib/boot.rg
https://github.com/rouge-lang/rouge/blob/master/lib/rouge.rb
を見ても、load〜は定義されていないみたい。

$ rouge google.rg
Google

Firefoxが立ち上がり、「Clojure Ruby Rouge」を検索する。

感想

Rougeは起動が早くて良い。
Emacs ciderで接続できない。これはかなり残念。
https://github.com/clojure/tools.nrepl をrougeに移植すれば良いのかな?
でも大変そう。
Rubyの豊富なライブラリが利用できるのは便利だ。
エラーが起きても、該当行が表示されないので、デバッグが大変だ。

参考


2013年7月26日金曜日

[Ruby][Solr][PDF] PDF解析ライブラリを作成

PDF文書のテキスト抽出

Solrを使ってPDF文書の全文検索システムを作ろうかといろいろと調査中である。
Solr Cellを使うと、PDF文書やWord文書を簡単に取り込めるようだけど、ワードが含まれる場所(ページ番号)は関連付けされないようだ。
ページ番号が不明だと、対象となるPDF文書が分かっても、その文書を開いてから、そのワードを再検索しなければならない。それは面倒なので、ページ番号も一緒に取得できるようにして、コマンドラインから、ページ番号を指定して、PDF文書を開きたい。
まずは、PDF文書からページ単位でテキストを抽出する必要があるので、Rubyのpdf-reader を使い、手持ちの日本語のPDF文書を処理してみたが、最初のページは正しくテキストを抽出できたが、以降のページは文字化けし、正しくテキストを抽出できなかった。
Ubuntu12.04のXPDF(pdfinfo, pdftotextコマンド)では文字化けすることなくテキストを抽出できたので、これをラップしたRubyのライブラリを作成した。

ソースはこちらから。
https://github.com/takeisa/xpdf-ruby

サンプルコード

require './xpdf'

doc = XPDF::Reader.read('sample.pdf')

puts "Title: #{doc.title}"
puts "Author: #{doc.author}"
puts "PDF Version: #{doc.pdf_version}"

doc.pages.each do |page|
  puts page
end


何も難しいところはない。
XPDF::Document#pagesにページ毎に抽出したテキストを格納している。

参考

pdf-reader https://github.com/yob/pdf-reader

2013年6月15日土曜日

[Ruby][Scheme] Micro Schemeの実装(17) S式の表示

評価結果をRubyオブジェクトではなく、S式として表示できるようにした。
https://github.com/takeisa/uschemer2/tree/v0.03

リストの表示処理

通常のリスト表記とドット表記に対応させた。
SConsクラスのto_sメソッドを呼び出すと、リストを文字列として取得できる。
対応するSConsクラスのto_sメソッドは以下の通り。

  def to_s(need_paren = true)
    s = ""
    s << "(" if need_paren
    s << "#{@car.to_s}"
    if @cdr == SNil then
      # nop
    elsif @cdr.list? then
      s << " #{@cdr.to_s(false)}"
    else
      s << " . #{@cdr.to_s}"
    end
    s << ")" if need_paren
    s
  end


cdrがリスト※か、そうでないかによって、ドット表記を切り換える。
リストの場合は、括弧をつけずに続けて、次の要素を表示したいため、to_sの引数には、括弧が必要かどうかのフラグを持たせている。
リスト中の要素が自分のリストに含まれる要素を参照している場合は、無限ループとなってしまうが、これは後で修正しよう。

※SConsクラスの時にリストと判定している。cons?メソッドとした方が良かったかも。

実行例

$ ruby uschemer.rb
Micro Scheme
> (define (add a b) (+ a b))
#<Instruction::Closure:0x98295fc>
> (add 10 20)
30
>


 いままでは評価結果は内部で使用しているRubyオブジェクトをそのまま表示していたが、やっと、Scheme処理系らしくなってきた。

なお、リストの表示方法を書いたけど、まだリストは扱えない。
次はリスト処理関数をいくつか追加しよう。

2013年6月8日土曜日

[Ruby][Scheme] Micro Schemeの実装(16) defineを実装

defineを実装した。
https://github.com/takeisa/uschemer2/tree/v0.02

define命令の追加


Virutal machineにdefine命令を追加した。

命令説明
(define var)aレジスタのオブジェクトをシンボルvarで束縛する。

define式のコンパイル

define式は
(define var val)
(define (func arg0...) body)
の2種類の形式に対応する。

対応するRubyコードは以下の通り。

elsif symbol == :define then
  if exp.cdr.car.is_a?(SSymbol) then
    # (define var val)
    var = exp.cdr.car
    val = exp.cdr.cdr.car
    compile(val, Define.new(var, next_op))
  elsif list?(exp.cdr.car) then
    # (define (func arg0...) body)
    var = exp.cdr.car.car
    args = exp.cdr.car.cdr
    body = exp.cdr.cdr.car
    val = SCons.new(SSymbol.new(:lambda), SCons.new(args, SCons.new(body)))
    compile(val, Define.new(var, next_op))
  else
    raise "define syntax error"
  end

2番目の形式の場合は、
(define (func arg0...) body)

(define func (lambda (arg0...) body))
に変換し、再度コンパイルする。


実行例

$ ruby uschemer.rb                 
> (define (add a b) (+ a b)) ←◆ここでadd関数を定義
#<Instruction::Closure:0x96d8c98
 @body=
  #<Instruction::Frame:0x96d8cc0
   @ret=#<Instruction::Return:0x96d8d88>,
   @x=
    #<Instruction::Refer:0x96d8cd4
     @var=#<SSymbol:0x96d8e3c @value=:a>,
     @x=
      #<Instruction::Argument:0x96d8ce8
       @x=
        #<Instruction::Refer:0x96d8cfc
         @var=#<SSymbol:0x96d8e14 @value=:b>,
         @x=
          #<Instruction::Argument:0x96d8d10
           @x=
            #<Instruction::Refer:0x96d8d60
             @var=#<SSymbol:0x96d8e64 @value=:+>,
             @x=#<Instruction::Apply:0x96d8d74>>>>>>>,
 @env=
  #<Env:0x96d96e8
   @binds=
    [#<VarBind:0x96d96d4
      @bind=
       {:+=>#<Proc:0x96d9648@uschemer.rb:53 (lambda)>,
        :-=>#<Proc:0x96d95f8@uschemer.rb:54 (lambda)>,
        :*=>#<Proc:0x96d9594@uschemer.rb:55 (lambda)>,
        :/=>#<Proc:0x96d9530@uschemer.rb:56 (lambda)>,
        :add=>#<Instruction::Closure:0x96d8c98 ...>}>]>, ←◆define時の環境を持っている
 @vars=
  #<SCons:0x96d8ef0
   @car=#<SSymbol:0x96d8edc @value=:a>,
   @cdr=
    #<SCons:0x96d8ec8
     @car=#<SSymbol:0x96d8ea0 @value=:b>,
     @cdr=#<SNilClass:0x927bba0>>>>
> (add 10 20) ←◆ここでadd関数呼び出し
#<SNumber:0x96c2df8 @value=30> ← ◆30が返ってきた。

define式の評価後は、Virtual machineのaレジスタにクロージャのコンパイル結果が格納されているため、
上記のような表示となる。
式をVirtual machineの命令に変換し、関数呼び出しで、それが実行される。
自分で実装して、実際に動かしてみると、なかなか感動的で、かなり楽しい。

2013年6月2日日曜日

[Ruby][Scheme] Micro Schemeの実装(14) Virtual machine方式に変更

いままで実装してきたインタプリタ方式で、末尾最適化や継続を実現するには、どのようにすれば良いのか、Webを検索したり、いろいろ考えていたが、これといった良い方法が思い付かなかった。
そこで、方針を変えて、Virtual Machine を使う方式に変更し、ごそごそやってできたのがこれ。

https://github.com/takeisa/uschemer2/tree/v0.00

Virtual machineとコンパイラを実装した。実装中にいろいろバグが出て、コンパイラのバグなのか、Virtual machineのバグなのか、分かり難かったりしたが、四苦八苦するのも、なかなか楽しい。ただ、適当に作ってしまったのでソースが非常に汚い。orz

Three Implementation Models for Scheme


Three Implementation Models for Scheme(3imp.pdfとして有名)というScheme処理系の実装方式の解説を読んだ結果、コンパイルしたSchemeコードをScheme用に最適化したVitrual machine上で実行する方式のほうが、インタプリタ方式よりも、末尾最適化や継続の実装が簡単そうだった。

VMを使う方式では、末尾最適化と継続は次のように実装できる。

末尾最適化
コンパイル時に、呼び出す関数が処理の末尾にあるか判定し、末尾の場合はコールフレームを生成せずに、関数を呼び出すコードを生成する。
継続
継続時点のスタックフレームを保存しておき、継続手続の呼び出しで、スタックフレームを復元後、値を渡す。

Virtual machine


いまのところ 3imp.pdf とほぼ同じ仕様だ。
備忘録をかねて書いておこう。

レジスタ


5つのレジスタを持つ。

レジスタ機能
aアキュムレータ
x次に実行する式
e現在の環境
r引数のリスト
s現在のスタックフレーム

命令セット


Virtual machineでサポートしている命令は以下の通り。

命令の中には、次に実行する命令を引数に取るものもあるが、以下では省略している。

命令説明
(halt)Virtual machineを停止する。aレジスタの値が評価結果となる。
(refer var)環境を参照し、変数varの値を取得し、aレジスタに設定する。
(constant obj)オブジェクトをaレジスタに設定する。
(test then else)aレジスタが非nilの場合はthenを実行し、nilの場合はelseを実行する。
(assign var)環境を参照し、変数varの値にaレジスタの値を設定する。
(conti)継続オブジェクトを生成する。
(nuate s var)スタックフレームsをsレジスタに設定し、変数varをaレジスタに設定する。
(frame ret)コールフレーム(戻り先retは次に実行する式として設定)を生成し、スタックフレームへ追加する。
(argument)aレジスタの値を、rレジスタに追加する。
(apply)クロージャまたはプリミティブ関数を評価する。
(return)スタックフレームのコールフレームより、フレーム生成前の状態にレジスタを戻す。

コンパイラ


式に応じてVirutal machineの命令を生成する。

コンパイル例


いくつかコンパイル例を示す。

変数aの参照 (refer a)
数値100 (constant 100)
(if pred "true" "false") (refer pred)
(test (constant "true") (constant "false"))
(+ 1 2) (frame)
(constant 1)
(argument)
(constant 2)
(argument)
(refer +)
(apply)
(return)

実行例

今はまだreplは無いので、rspec実行時のデバッグログ出力結果のみ。

クロージャ

context do
  it {
    op = compile('
(((lambda (a)
    (lambda (b) (+ a b))) 10) 20)
')
    res = @vm.eval(op)
    res.should eq 30
  }
end

(((lambda (a)
    (lambda (b) (+ a b))) 10) 20)
 -> (frame) ...
↓ここから下はVMが実行した命令
(frame)
(constant 20)
(argument)
(frame)
(constant 10)
(argument)
(close #<SCons:0x86e7d98> (close #<SCons:0x86e6754> (frame)))
(apply)
(close #<SCons:0x86e6754> (frame))
(return)
(apply)
(frame)
(refer a)
(argument)
(refer b)
(argument)
(refer +)
(apply)
(return)
(return)
(halt)

継続

context do
  it {
    op = compile('(+ 1 (call/cc (lambda (k) (k 3))))')
    res = @vm.eval(op)
    res.should eq 4
  }
end


(+ 1 (call/cc (lambda (k) (k 3)))) -> (frame) ...
↓ここから下はVMが実行した命令
(frame)
(constant 1)
(argument)
(frame)
(conti)
(argument)
(close #<SCons:0x9d1a96c> (frame))
(apply)
(frame)
(constant 3)
(argument)
(refer k)
(apply)
(nuate #<CallFrame:0x9d1a4d0> v)
(return)
(argument)
(refer +)
(apply)
(return)
(halt)



不足しているもの

末尾最適化
まだ実装していない。
define special form
VMの命令追加が必要だ。
REPL
REPLが無いと不便だ。

リファクタリング

メソッド内の最大インデントは一段まで、elseは使わない等の強い制約の元で実装することで、オブジェクト指向プログラミングがうまくなるらしい。
このルールに従ってリファクタリングしてみるのも、いろいろと得るものがありそうだ。

2013年5月25日土曜日

[Ruby][Scheme] Micro Schemeの実装(13) スペシャルフォーム評価処理のクラス化

コードの見通しが悪くなってきたので、リファクタリングした。
https://github.com/takeisa/uschemer/tree/v0.12

スペシャルフォーム評価処理のクラス化


スペシャルフォーム毎に式を評価するクラスを作成した。
処理の概要は以下の通り。

スペシャルフォームの識別子と、それに対応する処理オブジェクトを定義する。

  SP_FORM_EVAL = {
    :lambda => LambdaEval.new,
    :let => LetEval.new,
    :letrec => LetrecEval.new,
    :define => DefineEval.new,
    :if => IfEval.new,
    :cond => CondEval.new,
    :and => AndEval.new,
    :or => OrEval.new,
    :not => NotEval.new
  }

スペシャルフォームを評価する時は、識別子に対応する処理オブジェクトを、このハッシュオブジェクトより取得し、式を評価するevalメソッドを呼び出す。
参考として、if式を評価するクラスを示す。

  class IfEval < BaseEval
    def eval(exp, env, exp_eval)
      test_form, then_form, else_form = if_to_test_then_else(exp)
      if exp_eval.eval(test_form, env) then
        exp_eval.eval(then_form, env)
      else
        exp_eval.eval(else_form, env)
      end
    end
   
    def if_to_test_then_else(exp)
      [exp[1], exp[2], exp[3]]
    end
  end

もっと良い構成にできそうだが、いくらか見通しが良くなったので、ここまでにしておこう。

S式のクラス化


プログラム中ではRubyのリストオブジェクトのまま扱っているので、
今のままでは、Consセルを実現できない。
こちらはConsセルが必要になるまで、後回しにしよう。

2013年5月22日水曜日

[Ruby][Scheme] Micro Schemeの実装(12) and,or,not式の評価

and,or,not式を評価できるようにした。
https://github.com/takeisa/uschemer/tree/v0.11

各式の仕様

and式

(and e1 e2 ... en)
式e1からenまで順に評価し、すべてが真の場合は、最後の式enの値を返す。
いずれかが偽の場合は falseを返す。

or式

(or e1 e2 ... en)
式e1からenまで順に評価し、真となった値を返す。
すべてが偽の場合は falseを返す。

not式

(not e)
式eが真の場合は falseを返す。
式eが偽の場合は trueを返す。

評価部分のコード

実装は仕様そのままで簡単だ。

and式の評価

    def eval_and(exp, env)
      exp_list = and_to_exp_list(exp)
      last_exp = nil
      exp_list.each do |exp|
        last_exp = eval(exp, env)
        return false unless last_exp
      end
      last_exp
    end

    def and_to_exp_list(exp)
      exp[1..-1]
    end


or式の評価

    def eval_or(exp, env)
      exp_list = or_to_exp_list(exp)
      exp_list.each do |exp|
        last_exp = eval(exp, env)
        return last_exp if last_exp
      end
      false
    end

    def or_to_exp_list(exp)
      exp[1..-1]
    end


not式の評価

    def eval_not(exp, env)
      exp = not_to_exp(exp)
      not eval(exp, env)
    end

    def not_to_exp(exp)
      exp[1]
    end



リファクタリング

式の種別に応じて、対応する評価メソッドを呼び出す部分は以下の通り。

    def eval_special_form(exp, env)
      if lambda?(exp) then
        eval_lambda(exp, env)
      elsif let?(exp) then
        eval_let(exp, env)
      elsif if?(exp) then
        eval_if(exp, env)
      elsif letrec?(exp) then
        eval_letrec(exp, env)
      elsif define?(exp) then
        eval_define(exp, env)
      elsif cond?(exp) then
        eval_cond(exp, env)
      elsif and?(exp) then
        eval_and(exp, env)
      elsif or?(exp) then
        eval_or(exp, env)
      elsif not?(exp) then
        eval_not(exp, env)
      end
    end


うーむ。汚ないコードだ。リファクタリングしたい。
Strategyパターンを適用して、それぞれのスペシャルフォームをクラス化すると、すっきりするかな?

2013年5月21日火曜日

[Ruby][Scheme] Micro Schemeの実装(11) 四則演算で3個以上の引数に対応

前回 「-」関数を3個以上に引数に対応させたが、
その他の「+」、「*」、「/」関数も同様に対応させた。
https://github.com/takeisa/uschemer/tree/v0.10

四則演算部分のコード

  FUNCS = {
    :+ => [:primitive, lambda {|*xs| xs.reduce(0) {|a, x| a + x}}],
    :- => [:primitive, lambda {|*xs| if xs.size == 1 then -xs[0] else xs.reduce {|a, x| a - x} end}],
    :* => [:primitive, lambda {|*xs| xs.reduce(1) {|a, x| a * x}}],
    :'/' => [:primitive, lambda {|*xs| if xs.size == 1 then 1 / xs[0] else xs.reduce {|a, x| a / x} end}],
    ..snip..
  }


RSpecでテストファースト

コードが増えてくると、機能の追加や修正で、デグレしやすくなる。
今回からRubyのテスティングフレームワークであるRSpecを使い、テストファーストで実装した。

以下は「+」関数のテストケース。
結構、分かりやすいコードになる。

describe '+ operator' do
  before do
    @env = [USchemeR::KEYWORDS, USchemeR::FUNCS]
  end

  context 'one parameter' do
    it { eval("(+ 1)", @env).should eq 1 }
  end

  context 'two parameters' do
    it { eval("(+ 1 2)", @env).should eq 3 }
  end

  context 'many parameters' do
    it { eval("(+ 1 2 3 4 5)", @env).should eq 15 }
  end
end


テストファーストで実装すると、NGだったテストがOKになるときに、気持ち良さがあり、ちょっとしたコードの実装でも、なかなか楽しい。
本当は、このような上位レベルのテストコードにするのではなく、NGになったときに、原因が明確になるように、機能の対象となる部分のテストコードを書くべきなんだろうけど、まずはこれで良しとしよう。

参考

Rubyist Magazine 改めて学ぶ RSpec
とても分かり易くまとまっているのでおすすめ。

2013年5月19日日曜日

[Ruby][Scheme] Micro Schemeの実装(10) cond式の評価

cond式を使えるようにした。
https://github.com/takeisa/uschemer/tree/v0.09

cond式の評価

(cond ([p1] [e2])
      ([p2] [e2])
      ...
      ([pn] [en])
      (else [e-else])


条件式をp1→pnの順に評価する。
条件式pnが真の場合、対応する式enを結果とする。
いずれの条件式も成立しなかった場合、elseに対応するen-elseを結果とする。
elseは省略可。
else省略時に条件が一つも成立しなかった場合は例外を出すようにしておこう。

対応するコードは以下の通り。

def eval_cond(exp, env)
  pred_exp_list = cond_to_pre_exp_list(exp)
  pred_exp_list.each do |pred_exp|
    pred, exp = pred_exp
    if pred == :else || eval(pred, env) then
      return eval(exp, env)
    end
  end
  raise "cond: not match conditions"
end

def cond_to_pre_exp_list(exp)
  exp[1..-1]
end




abs関数

condの使用例として、数値の絶対値を求めるabs関数を定義する。

(define (abs x)
  (cond ((< x 0) (- x))
        ((= x 0) 0)
        (else x)))


(- x)に対応していなかったので、引数が一つの場合に、符号を反転できるように、「-」関数を以下のように修正した。

引数が一つの場合
  (- 10) ; => -10
引数が複数の場合
  (- 1 2 3) ;=> -4

「-」関数のコードは以下のとおり。
  :- => [:built_in, lambda {|*x| if x.size == 1 then -x[0] else x.reduce {|a, x| a - x}}]

reduceの初期値を0にすれば、引数の個数の判定処理は不要なので、
  :- => [:built_in, lambda {|*x| x.reduce(0) {|a, x| a - x}}]
でOK。
2013/5/20 全然OKじゃないことに気が付いた。後の方法では、(- 2 1) は -3 になってしまう。


実行結果

(define (abs x)
  (cond ((< x 0) (- x))
        ((= x 0) 0)
        (else x)))
 ;=> [:abs,
 [:closure,
  [:x],
  [:cond, [[:<, :x, 0], [:-, :x]], [[:"=", :x, 0], 0], [:else, :x]],
  [{:abs=>[...]},
   {:true=>true, :false=>false},
   {:+=>
     [:built_in,
      #<Proc:0x85e051c@/home/satoshi/workspace/uschemer/uschemer.rb:10 (lambda)>],
    ..snip..
    :>==>
     [:built_in,
      #<Proc:0x85e03dc@/home/satoshi/workspace/uschemer/uschemer.rb:18 (lambda)>]}]]]

(abs -10)
 ;=> 10

(abs 0)
 ;=> 0

(abs 10)
 ;=> 10


if は condを使ったマクロで実装したい。
その前に、リスト処理をする関数がいくつか必要だ。

2013年5月18日土曜日

[Ruby][Scheme] Micro Schemeの実装(9) Parserの改良 - 文字列に対応

へなちょこパーサを廃止し、文字列を扱えるようにパーサを実装した。
https://github.com/takeisa/uschemer/tree/v0.08

Lexerの実装

前回作成したTokenizerクラスで処理するトークンを定義する。
Lexerでは、一行毎にトークン分割をして、結果を配列に格納しておく。
get_tokenメソッドで、読み込んだトークンを一つを返す。
返すトークンが無い場合は、read_tokensメソッドを呼び出し、一行分のトークンを取得する。
unget_tokenメソッドで読み出したトークンを元の配列へ戻す。
これは、トークンを取得して、リストの最後の「)」トークンか判定し、違った場合に元に戻して、
リストのパースを継続するためである。

class Lexer
  def initialize(stream)
    @stream = stream
    @tokens = []

    @tokenizer = Tokenizer.new do |t|
      t.match(/\s+/) do |val|
        # skip
      end

      t.match(/(?:(?:-?[1-9][0-9]*)|0)(?:\.[0-9]+)?/) do |val|
        Token.new(:numeric, eval(val))
      end

      t.match(/"([^"]*)"/) do |val|
        Token.new(:string, val[1..-2])
      end

      t.match(/\(|\)/) do |val|
        Token.new(val.to_sym)
      end

      t.match(/[^\s\(\)]+/) do |val|
        Token.new(:ident, :"#{val}")
      end
    end
  end

  def get_token
    if @tokens.empty? then
      read_tokens
    end
    @tokens.shift
  end

  def read_tokens
    while @tokens.empty?
      line = @stream.readline.chomp
      next if line.empty?
      @tokens = @tokenizer.scan(line)
    end
  end

  def unget_token(token)
    @tokens.unshift(token)
  end
end


Parserの実装

S式のパースなので、非常に簡単だ。
以下のようにS式、リスト、アトムを定義し、再帰下降パーサとして実装した。
S式 := リスト || アトム
リスト := ( S式* )
アトム := 数値 || 文字列 || 識別子

class Parser
  def initialize(lexer)
    @lexer = lexer
  end

  def parse
    parse_sexp
  end

  def parse_sexp
    parse_list || parse_atom
  end

  def parse_list
    token = @lexer.get_token
    unless token.type == :'('
      @lexer.unget_token(token)
      return nil
    end
    list = []
    while TRUE
      token = @lexer.get_token
      if token.type == :')' then
        return list
      end
      @lexer.unget_token(token)
      list.push(parse_sexp)
    end
    list
  end

  def parse_atom
    token = @lexer.get_token
    token.value
  end
end



Parserのテスト

次のコードを実行する。
print Parser.new(Lexer.new(STDIN)).parse
(define (hello) ←入力
"hello") ←入力
[:define, [:hello], "hello"] ← 結果

うまく動いた。

Micro Scheme本体の修正

今回実装したパーサに差し替え、文字列を評価できるようにした。
実行例は以下の通り。

(define (hello) "hello, world!")
 ;=> [:hello,
 [:closure,
  [],
  "hello, world!",
  [{:hello=>[...]},
   {:true=>true, :false=>false},
   {:+=>
     [:built_in,
      #<Proc:0x879dcb0@/home/satoshi/workspace/uschemer/uschemer.rb:10 (lambda)>],
    ...snip...
    :>==>
     [:built_in,
      #<Proc:0x879db70@/home/satoshi/workspace/uschemer/uschemer.rb:18 (lambda)>]}]]]


(hello)
 ;=> "hello, world!"



そろそろテストコードが必要になってきた。
やっぱりRSpecあたりで書くのが順当かな。

2013年5月16日木曜日

[Ruby][Scheme] Micro Schemeの実装(8) ちょっと脱線 Y Combinator

Y Combinatorについて理解した(つもり)。まとめ。

Y Combinatorとは

以下のブログが非常に分かりやすかった。感謝!

関数を引数にとり、新しい関数を返す関数Fとする。
FをY Combinatorに適用すると、再帰関数fができる。
この再帰関数fは最初の関数Fの不動点である。
不動点であるため、この再帰関数fを関数Fに適用すると再帰関数fができる。
これで関数で再帰できることになる。
関数の引数として受け取った関数で再帰できるので、関数の名前が不要である。
無名再帰ともいうようだ。

と書いていて、本当に理解したのか怪しくなってくるなぁ...

Y Combinatorの求め方

Y Combinatorの求め方は次のブログが参考になった。
こちらにも感謝!
Y Combinator
http://www.loveruby.net/ja/misc/ycombinator.html

自分なりにまとめ直すと、以下の通り。

階乗を求める関数を元に、Y Combinatorを使って名前の定義を不要にするまで。

まずは階乗を求める関数。

(define (fact n)
  (if (= n 0)
      1
      (* n (fact (- n 1)))))

(fact 10) ; => 3628800

階乗を求める関数を作る関数。

(define fact-maker
  (lambda ()
    (lambda (n)
      (if (= n 0)
          1
          (* n ((fact-maker) (- n 1)))))))

((fact-maker) 10) ; => 3628800

(fact-maker)を一番外側のlambdaの仮引数fで受け取るようにする。
再帰時には自分自身を渡さなくてはならないので (f f)となる。

(define fact-maker
  (lambda (f)
    (lambda (n)
      (if (= n 0)
          1
          (* n ((f f) (- n 1)))))))

((fact-maker fact-maker) 10) ; => 3628800

(f f) を f と書きたい。
((f f) arg) は ((lambda (arg) ((f f) arg)) arg) と同じ。

置換すると

(define fact-maker
  (lambda (f)
    (lambda (n)
      (if (= n 0)
          1
          (* n ((lambda (arg) ((f f) arg)) (- n 1)))))))

fact0を以下のようにおき、(lambda (arg) ((f f) arg)) を 渡すようにすれば

(define fact0
  (lambda (f)
    (lambda (n)
      (if (= n 0)
          1
          (* n (f (- n 1)))))))

(define fact-maker
  (lambda (f)
    (fact0 (lambda (arg) ((f f) arg)))))

となる。

ここで f を g に置き換えると

(define fact-maker
  (lambda (g)
    (fact0 (lambda (arg) ((g g) arg)))))

となる。

よって、

(fact-maker fact-maker)

は、以下の式となる。

((lambda (g)
   (fact0 (lambda (arg) ((g g) arg))))
 (lambda (g)
   (fact0 (lambda (arg) ((g g) arg)))))

fact0を外に出すと

(lambda (f)
 ((lambda (g)
    (f (lambda (arg) ((g g) arg))))
  (lambda (g)
    (f (lambda (arg) ((g g) arg))))))

となる。
これが Yコンビネータ。

(define Y
  (lambda (f)
    ((lambda (g)
       (f (lambda (arg) ((g g) arg))))
     (lambda (g)
       (f (lambda (arg) ((g g) arg)))))))

(define fact
  (lambda (f)
    (lambda (n)
      (if (= n 0)
          1
          (* n (f (- n 1)))))))

((Y fact) 10) ; => 3628800

Micro Schemeで実行

現在、作成中の Micro Scheme で実行してみる。

(define Y
  (lambda (f)
    ((lambda (g)
       (f (lambda (arg) ((g g) arg))))
     (lambda (g)
       (f (lambda (arg) ((g g) arg)))))))
 ;=> [:Y,
 [:closure,
  [:f],
  [[:lambda, [:g], [:f, [:lambda, [:arg], [[:g, :g], :arg]]]],
   [:lambda, [:g], [:f, [:lambda, [:arg], [[:g, :g], :arg]]]]],
  [{:Y=>[...]},
   {:true=>true, :false=>false},
   {:+=>[:built_in, #<Proc:0x8d520c0@uschemer.rb:8 (lambda)>],
    :-=>[:built_in, #<Proc:0x8d52098@uschemer.rb:9 (lambda)>],
    :*=>[:built_in, #<Proc:0x8d52070@uschemer.rb:10 (lambda)>],
    :/=>[:built_in, #<Proc:0x8d52048@uschemer.rb:11 (lambda)>],
    :"="=>[:built_in, #<Proc:0x8d52020@uschemer.rb:12 (lambda)>],
    :<=>[:built_in, #<Proc:0x8d51ff8@uschemer.rb:13 (lambda)>],
    :>=>[:built_in, #<Proc:0x8d51fd0@uschemer.rb:14 (lambda)>],
    :<==>[:built_in, #<Proc:0x8d51f6c@uschemer.rb:15 (lambda)>],
    :>==>[:built_in, #<Proc:0x8d51f44@uschemer.rb:16 (lambda)>]}]]]


(define fact
  (lambda (f)
    (lambda (n)
      (if (= n 0)
          1
          (* n (f (- n 1)))))))
 ;=> [:fact,
 [:closure,
  [:f],
  [:lambda, [:n], [:if, [:"=", :n, 0], 1, [:*, :n, [:f, [:-, :n, 1]]]]],
  [{:fact=>[...]},
   {:Y=>
     [:closure,
      [:f],
      [[:lambda, [:g], [:f, [:lambda, [:arg], [[:g, :g], :arg]]]],
       [:lambda, [:g], [:f, [:lambda, [:arg], [[:g, :g], :arg]]]]],
      [...]]},
   {:true=>true, :false=>false},
   {:+=>[:built_in, #<Proc:0x8d520c0@uschemer.rb:8 (lambda)>],
    :-=>[:built_in, #<Proc:0x8d52098@uschemer.rb:9 (lambda)>],
    :*=>[:built_in, #<Proc:0x8d52070@uschemer.rb:10 (lambda)>],
    :/=>[:built_in, #<Proc:0x8d52048@uschemer.rb:11 (lambda)>],
    :"="=>[:built_in, #<Proc:0x8d52020@uschemer.rb:12 (lambda)>],
    :<=>[:built_in, #<Proc:0x8d51ff8@uschemer.rb:13 (lambda)>],
    :>=>[:built_in, #<Proc:0x8d51fd0@uschemer.rb:14 (lambda)>],
    :<==>[:built_in, #<Proc:0x8d51f6c@uschemer.rb:15 (lambda)>],
    :>==>[:built_in, #<Proc:0x8d51f44@uschemer.rb:16 (lambda)>]}]]]


((Y fact) 10)
 ;=> 3628800

おーーー。動いた。
ちょっと感動...

2013年5月14日火曜日

[Ruby][Scheme] Micro Schemeの実装(7) defineの評価

defineを使って、変数や関数を定義できるようにした。
https://github.com/takeisa/uschemer/tree/v0.07

defineの構文

以下の二つの構文に対応する。
(1)第一引数に変数、第二引数に値を指定する形式
(define id (lambda (x) ...))
(2)第一引数に関数名と仮引数のリスト、第二引数に関数本体を指定する形式
(define (id x) (...))

変数と値の束縛

変数をdefineで定義する際に、変数が束縛されているかどうかによって、以下のように処理する。
(1)束縛されている場合
束縛されている変数の値をdefineで指定した値に設定する。
(2)束縛されていない場合
defineで指定した変数と値の束縛を環境に作る。

(1)と(2)のどちらにも対応するというのが嫌な仕様だな。
例えば、
(define fuga ...) ...(a)
(define hoge ...
  (define fuga ...)) ...(b)
この場合、(b)のdefineは、(a)でfugaが定義されているため、上書きされる。
(a)がなければ、hoge内のローカルとなる。
ということは、ローカルのつもりで変数を定義していても、間違って外側でdefineしてしまうと、そちらを参照してしまい、良く分からないバグの原因になりそうだ。
そもそも、defineはこの仕様で正しいのかな?

実行例

フィボナッチ数を求める関数を定義してみた。

(define (fib n)
  (if (<= n 2)
      1
      (+ (fib (- n 2)) (fib (- n 1)))))
 #=> [:fib,
 [:closure,
  [:n],
  [:if, [:<=, :n, 2], 1, [:+, [:fib, [:-, :n, 2]], [:fib, [:-, :n, 1]]]],
  [{:fib=>[...]},
   {:fact=>
     [:closure,
      [:n],
      [:if, [:"=", :n, 0], 1, [:*, :n, [:fact, [:-, :n, 1]]]],
      [...]]},
   {:one=>1},
   {:true=>true, :false=>false},
   {:+=>[:built_in, #<Proc:0x931b574@uschemer.rb:8 (lambda)>],
    :-=>[:built_in, #<Proc:0x931b54c@uschemer.rb:9 (lambda)>],
    :*=>[:built_in, #<Proc:0x931b4fc@uschemer.rb:10 (lambda)>],
    :/=>[:built_in, #<Proc:0x931b4d4@uschemer.rb:11 (lambda)>],
    :"="=>[:built_in, #<Proc:0x931b4ac@uschemer.rb:12 (lambda)>],
    :<=>[:built_in, #<Proc:0x931b484@uschemer.rb:13 (lambda)>],
    :>=>[:built_in, #<Proc:0x931b45c@uschemer.rb:14 (lambda)>],
    :<==>[:built_in, #<Proc:0x931b434@uschemer.rb:15 (lambda)>],
    :>==>[:built_in, #<Proc:0x931b40c@uschemer.rb:16 (lambda)>]}]]]


(fib 15)
 #=> 610

正しく計算できた。これは楽しい!

2013年5月11日土曜日

[Ruby][Scheme] Micro Schemeの実装(6) letrec式の評価

letrec式を評価できるようにし、再帰呼び出しが可能になった。
https://github.com/takeisa/uschemer/tree/v0.06

let式での再帰呼び出し

階乗を求めるコードの例
(let ((fact
      (lambda (x) (if (= x 0)
                      1
                      (* x (fact (- x 1))))))) ← ◆このfactは参照できない
  (fact 5))           

外側のletで自分自身を変数に束縛しても、lambda式を評価してクロージャを生成した時の環境と、クロージャの仮引数と引数の束縛を使って、クロージャ本体を評価するため、クロージャ本体からは、その変数で自分自身を参照できない。

letrec式での再帰呼び出し

(letrec ((fact
         (lambda (x) (if (= x 0)
                         1
                         (* x (fact (- x 1))))))) ← ◆このfactを参照可にする
  (fact 5))           

factを参照できるようにするために、letrecは次のように評価する。
(1)letrecの束縛定義に従い、変数と、lambda式を評価した結果のクロージャの束縛を作成する。
(2)環境にその束縛を追加する。
(3)(1)のクロージャそれぞれの環境に(1)で作成した束縛を追加する。
(4)letrec本体を評価する。

書籍(http://tatsu-zine.com/books/scheme-in-ruby)の実装とは違うが、
この方式だと何か問題があるのかな?

実行結果

10の階乗

(letrec ((fact
         (lambda (x)
                 (if (= x 0)
                     1
                     (* x (fact (- x 1)))))))
  (fact 10))
 #=> 3628800

100の階乗

(letrec ((fact
         (lambda (x)
                 (if (= x 0)
                     1
                     (* x (fact (- x 1)))))))
  (fact 100))
 #=> 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

再帰呼び出しで、正しく計算できた。
少しずつ出来ることが増えてきて、なかなか楽しい。

[Ruby][Scheme] Micro Schemeの実装(5) if式の評価

if式を評価できるようにし、比較関数を追加した。
https://github.com/takeisa/uschemer/tree/v0.05

if式

if式の構文は以下の通り。
(if test-form then-form else-form)

test-formを評価し、その結果に応じて以下の処理を行う。
真の場合 → then-formの評価結果を返す
偽の場合 → else-formの評価結果を返す

この処理のRubyコードの一部示す。
    def eval_if(exp, env)
      test_form, then_form, else_form = if_to_test_then_else(exp)
      if eval(test_form, env) then
        eval(then_form, env)
      else
        eval(else_form, env)
      end
    end

if式の動作を、そのまま実装したコードで、とても簡単だ。

trueとfalse

trueとfalseが使えるように、
  KEYWORDS = {
    :true => true,
    :false => false
  }
を定義し、環境に追加した。

比較関数の定義

ifが使えるようになったので、比較関数 =, <, >, <=, >=, /= を定義した。
  FUNCS = {
    〜略〜
    :< => [:built_in, lambda {|x, y| x < y}],
    :> => [:built_in, lambda {|x, y| x > y}],
    〜略〜
  }

実行例

eval_print("(if true 1 2)")
(if true 1 2) #=> 1

eval_print("(if false 1 3)")
(if false 1 3) #=> 3

eval_print("(if (< 1 2) 1 2)")
(if (< 1 2) 1 2) #=> 1

eval_print("(if (> 1 2) 1 3)")
(if (> 1 2) 1 3) #=> 3

2013年5月9日木曜日

[Ruby][Scheme] Micro Schemeの実装(4) S式パーサの実装

Rubyのリスト形式でSchemeプログラムを書くのは大変なので、S式のパーサを作成した。
https://github.com/takeisa/uschemer/tree/v0.04

パーサの実装

作成中のMicro Schemeは、Rubyの配列でS式を表現している。
S式をRubyの配列形式になるように文字列置換し、Kernel.evalすればS式の配列表現を得ることができる。

    def parse(sexp)
      sexp.gsub!(/[_a-zA-Z\+\*\-\/][_a-zA-Z0-9\+\*\-\/]*/, ':\\0')
      sexp.gsub!(/\s+/, ',')
      sexp.gsub!(/\(/, '[')
      sexp.gsub!(/\)/, ']')
      Kernel.eval(sexp)
    end

まだ文字列は扱わないので、すごく単純だ。


実行例

eval_print("(let ((a 1) (b 1)) (+ a b))", env)
(let ((a 1) (b 1)) (+ a b)) #=> 2

eval_print("(let ((a 1)) (lambda (x) (+ a x)))", env)
(let ((a 1)) (lambda (x) (+ a x))) #=> [:closure,
 [:x],
 [:+, :a, :x],
 [{:a=>1},
  {:+=>[:built_in, #<Proc:0x9e7fec4@uschemer.rb:6 (lambda)>],
   :-=>[:built_in, #<Proc:0x9e7fe9c@uschemer.rb:7 (lambda)>],
   :*=>[:built_in, #<Proc:0x9e7fe4c@uschemer.rb:8 (lambda)>],
   :/=>[:built_in, #<Proc:0x9e7fe10@uschemer.rb:9 (lambda)>]}]]

eval_print("((let ((a 1)) (lambda (x) (+ a x))) 2)", env)
((let ((a 1)) (lambda (x) (+ a x))) 2) #=> 3

やっとLispらしくなってきた。

2013年5月8日水曜日

[Ruby][Scheme] Micro Schemeの実装(3) let式の評価

let式を扱えるようにした。
https://github.com/takeisa/uschemer/tree/v0.03

lambda式への変換

let式の束縛リストに定義した環境で本体を評価すれば良いので、以下のようにlambda式に変換する。

(let 束縛リスト 本体)

((lambda 仮引数リスト 本体) 引数)

let式を使って作成したクロージャをlambda式に変換する例

(let ((a 1) (b 2))
     (lambda (x) (+ a b x)))

((lambda (a b)
     (lambda (x) (+ a b x))
) 1 2)

処理手順

  1. let式か判定する。
  2. let式の場合は、上記のルールでlambda式に変換する。
  3. 変換後の式を再評価する。

実行例

eval_print([:let, [[:a, 1], [:b, 2]], [:+, :a, :b]], env)
[:let, [[:a, 1], [:b, 2]], [:+, :a, :b]] #=> 3

eval_print([:let, [[:a, 1]], [:lambda, [:x], [:+, :a, :x]]], env)
[:let, [[:a, 1]], [:lambda, [:x], [:+, :a, :x]]] #=> [:closure,
 [:x],
 [:+, :a, :x],
 [{:a=>1},
  {:+=>[:built_in, #<Proc:0x9d4bbe8@uschemer.rb:6 (lambda)>],
   :-=>[:built_in, #<Proc:0x9d4bbc0@uschemer.rb:7 (lambda)>],
   :*=>[:built_in, #<Proc:0x9d4bb98@uschemer.rb:8 (lambda)>],
   :/=>[:built_in, #<Proc:0x9d4bb70@uschemer.rb:9 (lambda)>]}]]

eval_print([[:let, [[:a, 1]], [:lambda, [:x], [:+, :a, :x]]], 2], env)
[[:let, [[:a, 1]], [:lambda, [:x], [:+, :a, :x]]], 2] #=> 3

2013年5月6日月曜日

[Ruby][Scheme] Micro Schemeの実装(2) lambda式とクロージャの評価

lambda式とクロージャを評価できるようにした。
https://github.com/takeisa/uschemer/tree/v0.02

束縛と環境

lambda式評価時の仮引数と引数の対応を束縛とし、その束縛のリストを環境とする。
束縛はハッシュで管理する。
環境の先頭から束縛のハッシュを順に検索して、束縛した値を取得する。

lambda式の評価方法

評価時の環境をひもづけてクロージャを作成する。

クロージャの評価方法

クロージャの仮引数と引数より束縛を作成し、環境の先頭に追加する。
この環境を利用してクロージャの本体を評価する。

実行例

eval_print([:+, :x, :y], [{:x => 123, :y => 456}] + env)
[:+, :x, :y] #=> 579

eval_print([:lambda, [:x, :y], [:+, :x, :y]], env)
[:lambda, [:x, :y], [:+, :x, :y]] #=> [:closure,
 [:x, :y],
 [:+, :x, :y],
 [{:+=>[:built_in, #<Proc:0xb747bca8@uschemer.rb:6>],
   :/=>[:built_in, #<Proc:0xb747b780@uschemer.rb:9>],
   :-=>[:built_in, #<Proc:0xb747baf0@uschemer.rb:7>],
   :*=>[:built_in, #<Proc:0xb747b938@uschemer.rb:8>]}]]

eval_print([[:lambda, [:x, :y], [:+, :x, :y]], 1, 2], env)
[[:lambda, [:x, :y], [:+, :x, :y]], 1, 2] #=> 3

eval_print(
  [[[:lambda, [:y], [:lambda, [:x], [:+, :x, :y]]], 5], 10],
  env
)
[[[:lambda, [:y], [:lambda, [:x], [:+, :x, :y]]], 5], 10] #=> 15