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

2016年4月18日月曜日

[SICP][Lisp]遅延評価の解釈系の実装

引き続きSICPを読みながら、Common LispでLispインタープリタを作成している。
「4.2.2 遅延評価の解釈系」を参考に、遅延評価できるように修正してみた。

https://github.com/takeisa/LispInCommonLisp/tree/lazy_evaluation

ifの反対のunlessを作って試してみる。

CL-USER> (repl)
LISP>
(define (unless condition usual exceptional)
    (if condition exceptional usual))
OK
LISP> (unless (= 1 0) 'hoge (/ 1 0))
HOGE

引数の(/ 1 0)は評価しないで、'hoge を返している。


cons,car,cdrを使えるようにして、前章のストリームで作成した無限リストを作ってみようとしたが、今の実装では、Common Lispの関数をそのまま使おうとすると、全ての引数をforceするようになっているので、簡単にできない。

5章のレジスタ計算機まで、早めに進みたいので後回しにしよう。

2016年4月9日土曜日

[SICP][Lisp]Common LispでLispインタープリタを書いてみた

SICP 第4章 超言語的抽象を参考にして、Common LispでLispインタープリタを書いてみた。

ソースはこちら。
https://github.com/takeisa/LispInCommonLisp

350行程度になった。
letはまだ実装していない。
言語処理系を実装するのは楽しいなー。

動作例


※evalで評価する式をデバッグ出力している。

フィボナッチ数を求める関数を定義する。
CL-USER> (repl)

LISP> (define (fibonacci n)
    (if (<= n 1)
 n
 (+ (fibonacci (- n 2)) (fibonacci (- n 1)))))
make-lamba parameters: (N)
make-lamba body: ((IF (<= N 1)
                      N
                      (+ (FIBONACCI (- N 2)) (FIBONACCI (- N 1)))))
lambda: (LAMBDA
            ((N) (IF (<= N 1) N (+ (FIBONACCI (- N 2)) (FIBONACCI (- N 1))))))
lambda parameters: (N)
lambda body: ((IF (<= N 1)
                  N
                  (+ (FIBONACCI (- N 2)) (FIBONACCI (- N 1)))))
OK

20番目のフィボナッチ数を求める。
LISP> (fibonacci 10)
t-eval: (FIBONACCI 10)
t-eval: FIBONACCI
t-eval: 10
t-eval: (IF (<= N 1)
            N
            (+ (FIBONACCI (- N 2)) (FIBONACCI (- N 1))))
t-eval: (<= N 1)
t-eval: <=
t-eval: N
t-eval: 1
..snip..
6765

2016年3月22日火曜日

[SICP][Lisp]ストリーム

SICP 3.5 ストリーム Common Lisp で実装したので、コードを貼っておこう。

遅延ストリーム・無限ストリームを使って、エラトステネスのふるいを作成し、素数を求めた。

(defmacro stream-cons (a b)
  `(cons ,a
  (delay ,b)))

(defun stream-car (s)
  (car s))

(defun stream-cdr (s)
  (force (cdr s)))

(defmacro delay (func)
  `#'(lambda () ,func))

(defun force (delay-obj)
  (funcall delay-obj))

(defvar +stream-empty+ (delay nil))

(defun stream-null? (s)
  (eq s +stream-empty+))

(defun stream-enumerate-interval (a b)
  (format t "[~a]~%" a b)
  (if (= a b)
      +stream-empty+
      (stream-cons a
     (stream-enumerate-interval (1+ a) b))))

(defun stream-each (func s)
  (unless (stream-null? s)
    (funcall func (stream-car s))
    (stream-each func (stream-cdr s))))

(defun stream-cadr (s)
  (stream-car (stream-cdr s)))

(defun stream-caddr (s)
  (stream-car (stream-cdr (stream-cdr s))))

(defun stream-nth (n s)
  (if (= n 0)
      (stream-car s)
      (stream-nth (1- n) (stream-cdr s))))

(defun stream-integers-starting-from (n)
  (stream-cons n
        (stream-integers-starting-from (1+ n))))

(defun stream-map (func &rest ss)
  (if (null (car ss))
      +stream-empty+
      (stream-cons
       (apply func (mapcar #'car ss))
       (apply #'stream-map (cons func (mapcar #'stream-cdr ss))))))

(defun stream-filter (pred s)
  (if (stream-null? s)
      +stream-empty+
      (if (funcall pred (car s))
   (stream-cons (car s)
         (stream-filter pred (stream-cdr s)))
   (stream-filter pred (stream-cdr s)))))

(defun stream-take (s n)
  (labels ((iter (s ts n)
      (if (= n 0)
   (reverse ts)
   (iter (stream-cdr s)
         (cons (stream-car s) ts)
         (1- n)))))
    (iter s '() n)))

(defun divisible? (x y)
  (= (mod x y) 0))

(defun sieve (stream)
  (stream-cons
   (stream-car stream)
   (sieve
    (stream-filter
     #'(lambda (x) (not (divisible? x (stream-car stream))))
     (stream-cdr stream)))))

sieve関数に2から始まる整数の無限ストリームを渡すと、最初の要素の値2と、2で割れる数を除外した無限ストリームを引数とするsieve関数の結果をconsしたものとなり、素数を表す無限ストリームが得られる。


実行例

最初の10個の素数を取得する。

CL-USER> (stream-take (sieve (stream-integers-starting-from 2)) 10) 
(2 3 5 7 11 13 17 19 23 29)
おおっ。素晴しい。

1000個目の素数を取得する。

CL-USER> (stream-nth 999 (sieve (stream-integers-starting-from 2)))
7919

楽しいねー。

 

10000個目の素数を取得する。

CL-USER> (stream-take (sieve (stream-integers-starting-from 2)) 9999) 
帰ってこない...

と思ったら、SBCL(swankサーバー側)でエラーとなっていて、 ヒープを使い尽していた。

fatal error encountered in SBCL pid 26815(tid 140737295218432):
Heap exhausted, game over.

ゲームオーバー...

2016年3月19日土曜日

[SICP][Lisp]デジタル回路のシミュレータ

SCIP 3.3.4 ディジタル回路のシミュレータ をCommon Lisp で書いみたので、貼っておこう。
インバータしか作っていないけど、こういうの作っていて楽しいね。

;; SICP Circuit simulator

;;----------------------------------------
;; queue

(defun make-queue ()
  (cons '() '()))

(defun front-ptr (queue)
  (car queue))

(defun rear-ptr (queue)
  (cdr queue))

(defun set-front-ptr! (queue item)
  (rplaca queue item))

(defun set-rear-ptr! (queue item)
  (rplacd queue item))

(defun empty-queue? (queue)
  (null (front-ptr queue)))

(defun front-queue (queue)
  (if (empty-queue? queue)
      (error "FRONT called with an empty queue ~a" queue)
      (car (front-ptr queue))))

(defun insert-queue! (queue item)
  (let ((new-pair (cons item '())))
    (cond
      ((empty-queue? queue)
       (set-front-ptr! queue new-pair)
       (set-rear-ptr! queue new-pair)
       queue)
      (t
       (rplacd (rear-ptr queue) new-pair)
       (set-rear-ptr! queue new-pair)
       queue))))

(defun delete-queue! (queue)
  (cond
    ((empty-queue? queue)
     (error "DELETE! called with an empty queue ~a" queue))
    (t
     (set-front-ptr! queue (cdr (front-ptr queue)))
     queue)))

;;----------------------------------------
;; time segment

(defun make-time-segment (time queue)
  (cons time queue))

(defun segment-time (segment)
  (car segment))

(defun segment-queue (segment)
  (cdr segment))

;;----------------------------------------
;; agenda

(defun make-agenda ()
  (list 0))

(defun current-time (agenda)
  (car agenda))

(defun set-current-time! (agenda time)
  (rplaca agenda time))

(defun segments (agenda)
  (cdr agenda))

(defun set-segments! (agenda segments)
  (rplacd agenda segments))

(defun first-segment (agenda)
  (car (segments agenda)))

(defun rest-segment (agenda)
  (cdr (segments agenda)))

(defun empty-agenda? (agenda)
  (null (segments agenda)))

(defun add-to-agenda! (time action agenda)
  (labels
      ((belongs-before? (segments)
  (or (null segments)
      (< time (segment-time (car segments)))))
       (make-new-time-segment (time action)
  (let ((queue (make-queue)))
    (insert-queue! queue action)
    (make-time-segment time queue)))
       (add-to-segments! (segments)
  (if (= time (segment-time (car segments)))
      (insert-queue! (segment-queue (car segments))
       action)
      (let ((rest (cdr segments)))
        (if (belongs-before? rest)
     (rplacd segments
      (cons (make-new-time-segment time action) rest))
     (add-to-segments! rest))))))
    (let ((segments (segments agenda)))
      (if (belongs-before? segments)
   (set-segments! agenda
    (cons (make-new-time-segment time action)
          segments))
   (add-to-segments! segments)))))

(defun remove-first-agenda-item! (agenda)
  (let ((q (segment-queue (first-segment agenda))))
    (delete-queue! q)
    (if (empty-queue? q)
 (set-segments! agenda (rest-segment agenda)))))

(defun first-agenda-item (agenda)
  (if (empty-agenda? agenda)
      (error "Agenda is empty -- FIRST-AGENDA-ITEM")
      (let ((segment (first-segment agenda)))
 (set-current-time! agenda (segment-time segment))
 (front-queue (segment-queue segment)))))

;;----------------------------------------
;; simulator

(defvar *agenda* nil)

(setf *agenda* (make-agenda))

(defun after-delay (delay action)
  (add-to-agenda! (+ delay (current-time *agenda*)) action *agenda*))

(defun propagate ()
  (if (empty-agenda? *agenda*)
      'done
      (let ((first-item (first-agenda-item *agenda*)))
 (funcall first-item)
 (remove-first-agenda-item! *agenda*)
 (propagate))))

(defun call-each (procs)
  (if (null procs)
      'done
      (progn
 (funcall (car procs))
 (call-each (cdr procs)))))

(defun make-wire ()
  (let ((signal-value 0)
 (action-procs '()))
    (labels ((set-signal! (value)
        (if (= value signal-value)
     'done
     (progn
       (setf signal-value value)
       (call-each action-procs))))
      (add-action! (proc)
        (setf action-procs (cons proc action-procs))
        (funcall proc))
      (dispatch (method)
        (case method
   ('get-signal signal-value)
   ('set-signal! #'set-signal!)
   ('add-action! #'add-action!)
   (t (error "Unknown operation ~a -- WIRE" method)))))
      #'dispatch)))

(defun get-signal (wire)
  (funcall wire 'get-signal))

(defun set-signal! (wire value)
  (funcall (funcall wire 'set-signal!) value))

(defun add-action! (wire proc)
  (funcall (funcall wire 'add-action!) proc))

(defun logical-not (value)
  (cond
    ((= value 0) 1)
    ((= value 1) 0)
    (t (error "Invalid signal ~a" value))))

(defvar *inverter-delay* 2)

(defun inverter (input output)
  (add-action! input
        #'(lambda ()
     (let ((new-value (logical-not (get-signal input))))
       (after-delay *inverter-delay*
      #'(lambda ()
          (set-signal! output new-value))))))
  'ok)

(defun probe (name wire)
  (add-action! wire
        #'(lambda ()
     (format t "~a ~a ~a~%"
      (current-time *agenda*)
      name
      (get-signal wire)))))

;;----------------------------------------
;; circuit

(defparameter w1 (make-wire))
(defparameter w2 (make-wire))

(inverter w1 w2)
(probe "w1" w1)
(probe "w2" w2)

動かしてみる。

0 w1 0
0 w2 0
CL-USER> (propagate)
2 w2 1
DONE
CL-USER> (set-signal! w1 1)
2 w1 1
DONE
CL-USER> (propagate)
4 w2 0
DONE
CL-USER>

もう少しいろいろ遊びたいところだけど、先に進もう。

2014年5月16日金曜日

[OCaml][Emacs]mlファイルとmliファイルを交互に切り替えるelisp

Emacsのtuareg-modeでOCamlのコードを書いている。
編集中はmliファイルまたはmlファイルを頻繁に切り替えているのだけど、switch-buffer(C-x b)で指定するのが面倒になったので、簡単に切り替えることができるelispを作成した。
tuaregには対応するコマンドは、きっとあるだろうなと思い、tuareg-* を探してみたが、見当たらなかった。残念。

キーバインディングは、空いていた C-c , に割り当てた。

(defun switch-file-ext (file-name ext1 ext2)
  (let ((file-ext (file-name-extension file-name)))
    (unless (member file-ext (list ext1 ext2))
      (error "unmatch file extension"))
    (message file-ext)
    (concat (file-name-directory file-name)
            (file-name-base file-name)
            "."
            (if (string= file-ext ext1) ext2 ext1))))

(defun switch-to-ml-or-mli ()
  (interactive)                                                                      (find-file (switch-file-ext (buffer-file-name) "ml" "mli")))
 
(define-key tuareg-mode-map (kbd "C-c ,") 'switch-to-ml-or-mli)

これで、mliファイルまたはmlファイルを編集中に、C-c , をキー入力すると、
例えば、hoge.mli を編集している場合は hoge.ml に、
逆に、hoge.ml の場合は、 hoge.mli に切り替えできるようになった。

こんなふうに手軽に機能拡張できるEmacsは素晴しいね。

2014/5/17 追記
Twitterで、nomaddoさん星のキャミバ様に教えていただいたが、上記の関数を作らなくても、tuareg-find-alternate-fileがあることが判明。
デフォルトでは、C-c C-aに割り当てられていた。
 /(^o^)\ナンテコッタイ

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月24日火曜日

[Scheme][Racket]RacketをDebian Wheezy 32bitにインストール

少しだけ読んで積読していたSICP(計算機プログラムの構造と解釈)、冬休みになるし、再読しようと思う。
書中では、Schemeを使うので、グラフィックの描画や、Webサーバの実装が簡単にできそうな、Racket を使おう。

Debian 32bit版にインストールしようとしたが、http://racket-lang.org/download/ を見ると、Linux X86_64 (Debian squeeze) はあるが、32bit版が無いので、ソースからインストールした。

インストール

http://racket-lang.org/download/ から Unix sourceをダウンロードする。

現在は racket-5.3.6-src-unix.tgz が最新版。

~/src/ にダウンロードしたソースを展開する。
$ cd src
$ tar zxf racket-5.3.6-src-unix.tgz

インストール方法は racket-5.3.6/src/README に書いてある。
展開したディレクトリのsrcディレクトリで、
$ cd racket-5.3.6/src
$ mkdir build
$ cd build
$ ../configure
$ make
$ make install

make install には結構時間がかかる。

configure に --prefix を付けていないので、
> If the `--prefix' flag is omitted, the binaries are built for an
> in-place installation (i.e., the parent of the directory containing
> this README will be used directly).
となり、~/src/racket-5.3.6 自身にインストールされる。

起動

racket-5.3.6/bin/ にパスを通して、drracket を実行する。

Emacsとの連携
http://docs.racket-lang.org/guide/Emacs.html を見ると Emacsのmajorモードもあるようなので、後で試してみよう。

参考

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年12月17日火曜日

[Emacs]リージョンを任意の文字列で囲むコマンドを作成した

Emacsで文章を作成していると、

hogehoge
fugafuga

--------------------
hogehoge
fugafuga
--------------------

と----で囲ったり、
Redmineのコメントで、

hogehoge
fugafuga

<pre>
hogehoge
fugafuga
</pre>

と書くことが結構多い。
指定したリージョンの前後に、任意の文字列を挿入できるコマンドがあると便利そうなので作ってみた。

使い方

リージョンを選択して、
M-x enclose-string
ミニバッファで
String(s/t/num):
と聞いてくるので、以下のいずれかを入力する。

(1)任意の文字列で囲む場合
最初の文字にsを指定する。
例: shoge
hogeで囲む。

(2)タグで囲む場合
最初の文字にtを指定する。
例: tpre
<pre>と</pre>で囲む。

(3)指定した回数を繰り返す文字列で囲む場合
例: 30-
------------------------------(「-」30個)で囲む。

ソース

(defun multi-string (n str)
  (loop repeat n concat str))

(defun pre-post-text-string (str)
 (list str str))

(defun pre-post-tag-string (str)
 (list (concat "<" str ">") (concat "</" str ">")))

(defun pre-post-multi-string (n str)
 (let ((n-str (multi-string (string-to-int n) str)))
    (list n-str n-str)))

(defun pre-post-string (str)
 "書式文字列に応じた前後の文字列を取得する。\n
前の文字列と後ろの文字列のリストを返す。\n
最初の文字が s の場合、それ以降の文字列を前後の文字列とする。\n
例: s--- → (\"---\" \"---\")\n
最初の文字が t の場合、それ以降の文字列をタグ名として扱う。\n
例: tpre → (\"<pre>\" \"</pre>\")\n
最初の文字列が数値の場合、数値の後ろの文字列を数値分繰替えした文字列を、前後の文字列とする。
例: 10* → (\"**********\" \"**********\")\n"
 (cond
  ((string-match "^s\\(.*\\)" str)
    (pre-post-text-string (match-string 1 str)))
  ((string-match "^t\\(.*\\)" str)
    (pre-post-tag-string (match-string 1 str)))
  ((string-match "^\\([0-9]+\\)\\(.*\\)" str)
    (pre-post-multi-string (match-string 1 str) (match-string 2 str)))))

(defun enclose-region (start end str)
  "リージョンの前後に文字列を挿入する。"
  (interactive "r\nsString(s/t/num):")
  (destructuring-bind (pre-str post-str) (pre-post-string str)
    (message (concat pre-str ":" post-str))
    (save-excursion
      (save-restriction
    (narrow-to-region start end)
    (goto-char (point-min))
    (insert pre-str "\n")
    (goto-char (point-max))
    (unless (bolp)
      (insert "\n"))
    (insert-before-markers post-str "\n")))))


Emacsは、手軽に機能を拡張できるので、便利だ。

2013年12月16日月曜日

[Clojure][Emacs]EmacsのCiderを起動するLeiningen pluginを作ってみた

$ lein cider
とすると、REPLを起動して、Emacsのciderコマンドを実行して、Emacs側でもREPLを起動するLeiningen pluginを作ってみた。

私はDebianを常用しており、Clojureでの開発には、REPLとして、Emacs上で動作するCiderを使用している。Emacsと同時に複数のターミナルを開いて、Emacsとターミナルの間を行き来している。
lein replは、ターミナルで起動して、Emacsからはciderコマンドで接続していた。
EmacsとターミナルのREPLを同時に使っていることも多い。
毎回ターミナルでlein replした後に、Emacsからciderで再接続するのが面倒なので、このpluginを作ってみた。

便利かなと思って作ってみたものの、cider-jack-inコマンドがあることを考えると、機能的には微妙なプラグインだな...

使い方

~/.lein/profiles.clj に lein-cider の設定を追加する。

{:user {:plugins [[lein-cider "0.1.0-SNAPSHOT"]]}}

Emacsをサーバーとして起動しておく(server-start関数を使用)。
プロジェクトのディレクトリで、
$ lein cider
を実行すると、Emacs上でciderコマンドが実行されて、REPLを起動する。

ソース

ソースはこちら
標準のREPL(lein repl)を起動後に、emacsclientコマンド経由で ELispのcider関数を呼び出している。
REPLが完全に起動した後に、Emacs側でcider関数を実行しないと、
IllegalAccessError pp does not exist clojure.core/refer
というエラーがでてしまうので、手っ取り早く、もう完全に起動した頃かなーという3秒後に、emacsclientコマンドを実行するようにした。

(defn server [project cfg headless?]
  (let [port (apply original-server-func [project cfg headless?])
        host (:host cfg)]
    (future
      (Thread/sleep leiningen.cider/WAIT_TIME) ; ←◆コレ
      (call-cider host port))
    port))


うーむ。まったくもって、行き当たりばったりな実装だ。

Emacsサーバー

こちらを見ると、
Note that since Emacs 23 this is the preferred way to use Emacs in daemon mode. (start-server) is now mostly deprecated.
というコメントがあった。
ほー。今はdaemon modeで使う方が良いようだ。

参考

2013年12月14日土曜日

[Clojure]lein-tryで外部ライブラリを使う

外部のClojureライブラリを使うには、project.cljのdependenciesに記述しなければならない。ちょっとお試しでライブラリを使用したい場合、これは面倒くさかった。

lein-tryプラグインを使うと、project.cljはそのままで、使用したい外部ライブラリを引数に渡すだけで、試すことができるようになる。

準備

以下のように~/.lein/profiles.cljを編集して、lein-tryプラグインを使えるようにする。
{:user {:plugins [[lein-try "0.4.1"]]}}

試してみる

satoshi@debian:~/workspace/clojure$ lein deps
Retrieving lein-try/lein-try/0.4.1/lein-try-0.4.1.pom from clojars
Retrieving lein-try/lein-try/0.4.1/lein-try-0.4.1.jar from clojars
Couldn't find project.clj, which is needed for deps

プラグインを読み込んだことを確認。

clj-timeの最新版を試してみる。

satoshi@debian:~/workspace/clojure$ lein try clj-time
Retrieving clj-time/clj-time/0.6.0/clj-time-0.6.0.pom from clojars ← clj-timeをロードしている
Retrieving joda-time/joda-time/2.2/joda-time-2.2.pom from central
Retrieving clj-time/clj-time/0.6.0/clj-time-0.6.0.jar from clojars
Retrieving joda-time/joda-time/2.2/joda-time-2.2.jar from central
nREPL server started on port 51459 on host 127.0.0.1
REPL-y 0.3.0
Clojure 1.5.1
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

user=> (require '[clj-time.core :as t])
nil
user=> t/date-time
#<core$date_time clj_time.core$date_time@6849b9>
user=> (t/date-time 2013 12 13)
#<DateTime 2013-12-13T00:00:00.000Z>
user=>

バージョン番号の指定もできる。

satoshi@debian:~/workspace/clojure$ lein try clj-time "0.5.1"
Retrieving clj-time/clj-time/0.5.1/clj-time-0.5.1.pom from clojars
Retrieving clj-time/clj-time/0.5.1/clj-time-0.5.1.jar from clojars
nREPL server started on port 49481 on host 127.0.0.1
REPL-y 0.3.0
Clojure 1.5.1
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

user=>

今まで、いちいちproject.cljにライブラリの設定を書いてからlein replしていたので、これはお手軽で、とても便利。

参考


2013年12月13日金曜日

[Clojure]マルコフ連鎖に基づく文生成

先日購入した、はじめてのAIプログラミング C言語で作る人工知能と人工無能を読んでいる。
文生成が面白そうだったので、Clojureで書いてみた。

処理内容としては、以下の通り。
(1)形態素解析ライブラリKuromojiを使用して、元ネタとなる文章を形態素解析する。
(2)その形態素の連鎖をモデル化して、マルコフ連鎖に基づく文生成をする。

プログラム

(ns ai.sentence
  (:import (org.atilika.kuromoji Token Tokenizer)))

(def ^:dynamic *words*
  "マルコフ連鎖のモデル
次のキーと値のマップ
キー: 形態素
値: キーを次の形態素、値を出現数とするマップ"
  (ref {}))

(defn tokenize [text]
  (let [tokenizer (. (Tokenizer/builder) build)]
    (. tokenizer tokenize text)))

(defn token-word [token]
  (.trim (.getSurfaceForm token)))

(defn inc-map-value [m k]
  (if (get m k)
    (update-in m [k] inc)
    (assoc m k 1)))

(defn register-word [m word1 word2]
  (let [word2-map (get m word1 {})]
    (assoc m word1 (inc-map-value word2-map word2))))

(defn load-text [file-name]
  (let [text (slurp file-name)
        tokens (tokenize text)]
    (reduce (fn [m [token1 token2]]
              (let [word1 (token-word token1)
                    word2 (token-word token2)]
                (if (or (= word1 ""))
                  m
                  (register-word m word1 word2))))
            {} (partition 2 1 tokens))))

(defn select-word [word-map]
  (first (rand-nth (seq word-map))))

(defn select-next-word [word-map word]
  (let [next-word-map (get word-map word)]
    (select-word next-word-map)))

(defn create-sentence [word-map word]
  (loop [sentence ""
         word word]
;    (println (str "*" word))
    (if word
      (if (or (= word "。") (= word "?") )
        sentence
        (recur (str sentence word) (select-next-word word-map word)))
      sentence)))

(defn init []
  (dosync
   (ref-set *words*
            (load-text "/home/satoshi/work/sample.txt"))))


サンプルの文章(sample.txt)

夏目漱石 「我輩は猫である」(青空文庫より)の最初の部分を使用した。

吾輩は猫である。
名前はまだ無い。
どこで生れたかとんと見当がつかぬ。
何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。
吾輩はここで始めて人間というものを見た。
しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。
この書生というのは時々我々を捕えて煮て食うという話である。
しかしその当時は何という考もなかったから別段恐しいとも思わなかった。
ただ彼の掌に載せられてスーと持ち上げられた時何だかフワフワした感じがあったばかりである。
掌の上で少し落ちついて書生の顔を見たのがいわゆる人間というものの見始であろう。
この時妙なものだと思った感じが今でも残っている。
第一毛をもって装飾されべきはずの顔がつるつるしてまるで薬缶だ。
その後猫にもだいぶ逢ったがこんな片輪には一度も出会わした事がない。
のみならず顔の真中があまりに突起している。
そうしてその穴の中から時々ぷうぷうと煙を吹く。
どうも咽せぽくて実に弱った。
これが人間の飲む煙草というものである事はようやくこの頃知った。

実行結果

user> (in-ns 'ai.sentence)
#<Namespace ai.sentence>
ai.sentence> (init)
{"だ" {"と" 1, "。" 2}, "ニャーニャー" {"泣い" 1}, "一" {"度" 1, "毛" 1}, "何だか" {"フワフワ" 1}, "所" {"で" 1}, "ここ" {"で" 1}, "名前" {"は" 1}, "あっ" {"た" 2}, "
〜略〜
ai.sentence> (create-sentence @*words* "何")
"何というの飲む煙草という人間というのは何でも残って食うという考もだいぶ逢った"
ai.sentence> (create-sentence @*words* "猫")
"猫でニャーニャー泣いてまるで薬缶だと思った時何だかフワフワして書生のがない"
ai.sentence> (create-sentence @*words* "煙草")
"煙草というものを吹く"

# まさに、人工無能...

形態素を使っているので、n-gramを使用した場合より、まともな文章になっているが、文法に従った文生成をしていないため、意味不明な文になっている。
また、本来は、ある形態素の次の形態素を決めるときには、遷移確率を考慮しなければならないが、均等な確率としている(select-word関数のrand-nth関数)。
このような文字列を扱う処理は、C言語よりも圧倒的にClojureの方が書き易い。

2013年12月12日木曜日

[Clojure]トランザクションの最大リトライ回数

トランザクション内で、リファレンスの値の変更に失敗した場合は、トランザクションの最初から処理をリトライする。

トランザクションの処理に時間がかかり、何回繰り返してもリファレンスの値を更新できない場合は Live lock 状態になる。Clojureでは、これを防ぐため、リトライ回数がある一定値を越えると、例外を投げる仕組みになっている。

リトライ回数の最大値はどのくらいかなと、調べてみた。

(def x (ref 0))

(defn test1 []
 (dosync
  @(future (dosync (alter x inc)))
  (ref-set x -1)))


user> (test1)
RuntimeException Transaction failed after reaching retry limit  clojure.lang.Util.runtimeException (Util.java:219)
user> x
#<Ref@843d62: 10000>
user>

最大10000回もリトライしていた。
数十回程度かなと思っていたので、少しびっくり。
この値は変更できるのか、ざっと調べてみたが、変更方法は見付からなかった。
Clojure の STM のドキュメントのどこかに書いてあるのかな?

2013年12月7日土曜日

[Clojure]形態素解析ライブラリKuromojiを使う

オーム社の100周年記念セールで、はじめてのAIプログラミング C言語で作る人工知能と人工無能を購入した。2006年に出版された書籍で、C言語(Borland C)での実装が解説してある。
この類のプログラムをCで実装するのは煩雑になってしまうので、Clojureで作ったらどんなふうになるのかなと思い、文字列の解析時に使用する形態素ライブラリを調べてみた。
Lucene や Solr に対応したlucene-gosen が、結構使われているようだけど、mavenリポジトリにあるバージョンは少し古く、また、多くのライブラリに依存していており、それらのラブラリのバージョンも少し古かった。
その点、Kuromojiは、依存ライブラリがなく、lucene-gosenと同様に辞書も同梱されているので、こちらのライブラリを試してみた。

project.cljにKuromojiの設定を追加

Kuromojiをmavenで使うためには、リポジトリの追加が必要である。
project.cljに次の設定を書く。

:repositories [["Atilika Open Source repository"
                "http://www.atilika.org/nexus/content/repositories/atilika"]]
:dependencies [[[org.atilika.kuromoji/kuromoji "0.7.7"]]


リポジトリのURLの追加方法が最初分からず、いろいろ調べたが、
sample.project.cljのサンプルが参考になった。


サンプルコード

(ns gosen.core
  (:import (org.atilika.kuromoji Token Tokenizer)))

(defn -main []
  (let [tokenizer (. (Tokenizer/builder) build)]
    (doseq [token (. tokenizer tokenize "Javaより楽しいClojure。")]
      (println (str (. token getSurfaceForm) "\t"
                    (. token getAllFeatures))))))


何のことはない。Kuromojiのクラスを使う単純なコードだ。

実行結果

replより実行した。

gosen.core> (-main)
Java    名詞,固有名詞,組織,*,*,*,*
より    助詞,格助詞,一般,*,*,*,より,ヨリ,ヨリ
楽しい    形容詞,自立,*,*,形容詞・イ段,基本形,楽しい,タノシイ,タノシイ
Clojure    名詞,一般,*,*,*,*,*
。    記号,句点,*,*,*,*,。,。,。
nil

ユーザ辞書を追加することも簡単なようなので、試してみる予定。

参考

Java製形態素解析器「Kuromoji」を試してみる

2013年12月2日月曜日

[Clojure][Emacs]Clojure Cheatsheet for Emacs

EmacsでClojureのCheatsheetを閲覧できるELispがあった。
Clojure Cheatsheet for Emacs
試しに使ってみたら、結構便利。

インストール

Emacs24なら、MELPA経由で簡単にインストールできる。

Emacs初期化ファイルに以下のような設定を書いておけば良い。

;; MELPAを有効にする
(add-to-list 'package-archives '("melpa" . "http://melpa.milkbox.net/packages/") t)


サイトの説明通り、以下のコマンドでインストールする。
M-x package-refresh-contents
M-x package-install RET clojure-cheatsheet

実行

M-x clojure-cheatsheet を実行すると、
ミニバッファに
pattern:
と表示されるので、検索したい文字列を入力する。
sort mapを入力した場合は、次のように表示される。



キーバインドは次の通り。
C-p 前の行
C-n 次の行
RET 選択した項目の表示

clojure-mode時に有効になるような、キーバインドを書いておくと便利かも。
試していないが、Helmのsourceにも対応している。

2013年12月1日日曜日

[Haskell][Clojure]逆ポーランド記法の解析処理

Learn You a Haskell for Great Good で書かれていた逆ポーランド記法の解析処理をClojureで書いてみた。
四則演算のみサポートする単純な処理である。

Haskellの場合

module RPN where
       
solveRPN :: String -> Double
solveRPN = head . foldl func [] . words
    where
      func (x:y:ys) "+" = (y + x) : ys
      func (x:y:ys) "-" = (y - x) : ys
      func (x:y:ys) "*" = (y * x) : ys
      func (x:y:ys) "/" = (y / x) : ys
      func xs num = read num : xs

*RPN> solveRPN "1 2 + 3 * 1 - 2 /"
4.0
     
whereを使うとコードが分かり易くて、なかなか良いね。

Clojureの場合

(ns rpn.core
  (:use [clojure.string :only (split)]))

(defn words [s]
  (split s #"\s+"))

(defn op2 [[x y & ys] f]
  (conj ys (apply f [y x])))

(defn operate [stack item]
  (case item
    "+" (op2 stack +)
    "-" (op2 stack -)
    "*" (op2 stack *)
    "/" (op2 stack /)
    (conj stack (Double/parseDouble item))))

(defn solve-rpn [exprs]
  (first (reduce operate '() (words exprs))))

rpn.core> (solve-rpn "1 2 + 3 * 1 - 2 /")
4.0

solve-rpn関数の中でoperate関数を定義することも可能だが、かえって分かり難くなりそうなので、外出しして関数定義した。
Haskellに比べると冗長になってしまった。
もっと、すっきり書けるのかな?
文字列のdouble型への変換 (Double/parseDouble item) は美しくない。→と思ったときは parse-double関数を作れば良いのだけど。

2013年10月6日日曜日

[Clojure] clj-webdriverでChromeのUser-Agentを変更する方法(その2)

[Clojure] clj-webdriverでChromeのUser-Agentを変更する方法 でUser-Agentを変更する方法を書いたが、先日、最新のChromeとchromedriverを使ってみると、User-Agentが変更できなくなっていた。
原因を調べてみると、DesiredCapabilitiesオブジェクトにchrome.switchesを追加しても、Chromeの起動オプションに追加されなくなっていた。
起動オプションの追加方法が変更されたようで、ChromeOptions#addArgumentsを使うと問題なく動作した。

実行環境

  • Windows 8
  • clj-webdriver 0.6.0
  • chromedriver 2.4
chromedriver 2.4 はこちらからダウンロードした。
従来のダウンロード先にあったドライバは全てdeprecatedになっており、http://code.google.com/p/chromedriver/wiki/WheredAllTheDownloadsGo?tm=2 では、以下の説明があった。
Downloads have been moved to http://chromedriver.storage.googleapis.com/index.html, because Google Code downloads are being deprecated!

project.clj

次のライブラリをdependenciesに追加した。

[clj-webdriver "0.6.0"]
[org.seleniumhq.selenium/selenium-java "2.35.0"]

src/cli_webdriver_patch.clj

前回と同様に、clj-webdriver.coreの関数を再定義して対応する。

(ns clj-webdriver-patch)

(in-ns 'clj-webdriver.core)
(import 'org.openqa.selenium.chrome.ChromeOptions)
(declare new-chrome-driver)

(defn new-driver
  "Start a new Driver instance. The `browser-spec` can include `:browser`, `:profile`, and `:cache-spec` keys.
The `:browser` can be one of `:firefox`, `:ie`, `:chrome` or `:htmlunit`.
The `:profile` should be an instance of FirefoxProfile you wish to use.
The `:cache-spec` can contain `:strategy`, `:args`, `:include` and/or `:exclude keys. See documentation on caching for more details."
  ([browser-spec]
     (let [{:keys [browser profile chrome-switches cache-spec]
            :or {browser :firefox
                 profile nil
                 chrome-switches nil
                 cache-spec {}}} browser-spec]
       (if (= browser :chrome)
         (new-chrome-driver chrome-switches)
         (init-driver {:webdriver (new-webdriver* {:browser browser
                                                   :profile profile})
                       :cache-spec cache-spec})))))
(defn new-chrome-driver
  [chrome-switches]
  (let [option (ChromeOptions.)]
    ;; for Linux
    ;; (.setBinary option "chrome.binary" "/usr/bin/google-chrome")
    (if chrome-switches
      (.addArguments option (into-array chrome-switches)))
    (init-driver (ChromeDriver. option))))


src/chrometest/core.clj

サンプルコード。
User-AgentにiPhoneを設定し、googleで「hogehoge」を検索する。

(ns chrometest.core
  (:require [clj-webdriver.core])
  (:use clj-webdriver-patch)
  (:require [clj-webdriver.taxi :as taxi]))

(System/setProperty "webdriver.chrome.driver" "c:/Application/chromedriver/chromedriver-2.4.exe")

(def ^:dynamic *ua-iPhone-3G* "Mozilla/5.0 (iPhone; U; CPU iPhone OS 2_0_1 like Mac OS X; ja-jp) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5B108 Safari/525.20")

(defn user-agent-switch [user-agent]
 (str "--user-agent=\"" user-agent "\""))

(defn test1 []
  (taxi/set-driver! {:browser :chrome
                     :chrome-switches [(user-agent-switch *ua-iPhone-3G*)]})
  (taxi/to "https://www.google.com/")
  (taxi/input-text "input[name='q']" "hogehoge")
  (taxi/click "button[name='btnG']"))


Debian Wheezyでの実行

chromedriverの最新版はglibc2.1.15に依存しており、glibcのバージョンを上げなければならない。
How to upgrade glibc from version 2.13 to 2.15 on Debian? にglibcのバージョンを上げる方法が書かれていたが、試していない。

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は使わない等の強い制約の元で実装することで、オブジェクト指向プログラミングがうまくなるらしい。
このルールに従ってリファクタリングしてみるのも、いろいろと得るものがありそうだ。