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

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年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年4月22日月曜日

[SICP][Haskell] ニュートン法で平方根を求める

ニュートン法を使って平方根を求める処理をHaskellで書いてみた。

コード

prec = 0.0001

mysqrt :: Double -> Double
mysqrt x = sqrt_iter 1 x x
  where sqrt_iter guess last_guess x = if good_enough guess last_guess then
                                         guess
                                       else
                                         sqrt_iter (improve guess x) guess x
          where
            good_enough guess last_guess = (abs (guess - last_guess) / guess) < prec
            improve guess x = average guess (x / guess)
                  where average a b = (a + b) / 2

実行結果

*Main> mysqrt 2
1.4142135623746899
*Main> mysqrt 20000
141.42135623738412
*Main> 

SICPのSchemeのコードほぼそのまま。何のひねりもない。
Schemeよりは分かりやすいような気もするけど、この程度じゃあまり変らないよね...
Schemeで hoge? 、Common Lispで hoge-p と命名するような predicateな関数は、Haskellではどのように命名するのが一般的なんだろうか?


コードよりは、
  • ニュートン法とは?
  • どうしてニュートン法で平方根を求めることができるの?
の方が気になって、理解できるまで、いろいろ調べてしまった。
接線の方程式とか、微分とか、いろいろ忘れているなぁ。
たまには数学を再勉強しなきゃ。

参考

2013年4月21日日曜日

[SICP][Lisp]Gauche + Emacsで環境構築

今更だけど、SICP (Structure and Interpreter Computer Programming)を読み始めた。
この本では、Schemeが使われている。
自分でコードを書きながら、読み進めた方が楽しいので、
まずは、Scheme処理系の一つであるGauche+Emacsを使い、
REPLとコード補完できる環境を構築した。

OSはUbuntu、Emacsは自分でインストールしたEmacs 24.2を使用している。

Gaucheのインストール

gaucheはパッケージが用意されているのでapt-getでインストールする。
$ apt-get install gauche

quackとscheme-completeのインストール

パッケージのダウンロード

インストール先は ~/.emacs.d/gauche-el とした。
$ cd ~/.emacs.d/
$ mkdir gauche-el
$ cd gauche-el
$ curl -O http://www.neilvandyke.org/quack/quack.el
$ curl -O http://synthcode.com/emacs/scheme-complete-0.8.11.el.gz
$ gunzip scheme-complete-0.8.11.el.gz
$ mv scheme-complete-0.8.11.el scheme-complete.el

init.elの設定

emacsの設定ファイルinit.elにquackとscheme-completeの設定を追加する。
(add-to-path 'load-path "~/.emacs.d/gauche-el")
(setq quack-default-program "gosh")
(require 'quack)
(require 'scheme-complete)
;;(autoload 'scheme-smart-complete "scheme-complete" nil t)
;; auto-completeを使っているので不要
;; (eval-after-load 'scheme
;;   '(define-key scheme-mode-map "\e\t" 'scheme-smart-complete))
(add-hook 'scheme-mode-hook
  (lambda ()
    (make-local-variable 'eldoc-documentation-function)
    (setq eldoc-documentation-function 'scheme-get-current-symbol-info)
    (eldoc-mode)))
(setq lisp-indent-function 'scheme-smart-indent-function)

動作確認

Emacsを起動後、hello.scm を新規作成する。
(print "こんにちは、世界!")

hello.scmのバッファでC-c C-lを押下する。
mini-bufferで、以下を聞かれる。
それぞれデフォルトのままで良い。
Load Scheme file: (default hello.scm) ~/workspace/sicp/
Run Scheme (default "gosh"):

REPLが開き、hello.scmの実行を結果が表示される。


gosh> こんにちは、世界!
#t
gosh>


とりあえず、他に、以下のコマンドが分かればOK。
  • C-c C-e S式を評価する。
  • M-x run-scheme REPLを起動する。

eldocで関数の説明が表示されるし、コード補完もできる。
いつも使っているslimeとあまり違和感はないようで、なかなか便利だ。


参考

quack
http://www.neilvandyke.org/quack/

scheme-complete
http://synthcode.com/wiki/scheme-complete

auto-complete
http://cx4a.org/software/auto-complete/index.ja.html

SCIPのmobiファイル
https://github.com/twcamper/sicp-kindle