2013年5月17日金曜日

((Rで) 書く (Lisp) インタプリタ)

Rに慣れるために、Lispインタプリタを書いてみました。

元ねたはPeter Norvigの (How to Write a (Lisp) Interpreter (in Python)) (日本語訳: ((Pythonで) 書く (Lisp) インタプリタ))です。

コード

## Scheme Interpreter in R
## (an R port of the Peter Norvig's "lis.py")
Env <- setRefClass('Env',
fields = c(".outer", ".dict"),
methods = list(
initialize = function(outer=NULL) {
.outer <<- outer
.dict <<- list()
},
get = function(key) {
.dict[[key]]
},
set = function(key, value) {
if (is.list(key)) {
.dict[unlist(key)] <<- value
} else {
.dict[[key]] <<- value
}
},
find = function(key) {
if (!is.null(.dict[[key]])) .self else .outer$find(key)
}
))
addGlobals <- function(env) {
procs <- list("+" = function(x) do.call(sum, x),
"*" = function(x) do.call(prod, x),
"-" = function(x) Reduce("-", x),
"/" = function(x) Reduce("/", x),
"=" = function(x) do.call("==", x),
"eq?" = function(x) do.call("==", x),
"equal?" = function(x) do.call("identical", x),
"not" = function(x) do.call("!", x),
"cons" = function(x) append(x[1], x[[-1]]),
"car" = function(x) x[[1]][[1]],
"cdr" = function(x) x[[1]][-1],
"list?" = function(x) is.list(x[[1]]),
"null?" = function(x) length(x[[1]]) == 0,
"symbol?" = function(x) is.character(x[[1]])
)
rfname <- c(">", "<", ">=", "<=",
"list", "length")
rfunc <- lapply(rfname,
function(name) {
n <- name
function(x) do.call(n, x)
})
names(rfunc) <- rfname
procs <- append(procs, rfunc)
env$set(as.list(names(procs)), procs)
}
eval <- function(x, env) {
if (is.character(x)) { # variable reference
env$find(x)$get(x)
} else if (!is.list(x)) { # constant literal
x
} else if (identical(x[[1]], "quote")) { # (quote exp)
x[-1][[1]]
} else if (identical(x[[1]], "if")) { # (if test conseq alt)
test <- x[[2]]
conseq <- x[[3]]
alt <- x[[4]]
if (eval(test, env)) {
eval(conseq, env)
} else {
eval(alt, env)
}
} else if (identical(x[[1]], "set!")) { # (set! var exp)
var <- x[[2]]
exp <- x[[3]]
env$find(var)$set(var, eval(exp, env))
} else if (identical(x[[1]], "define")) { # (define var exp)
var <- x[[2]]
exp <- x[[3]]
env$set(var, eval(exp, env))
} else if (identical(x[[1]], "lambda")) { # (lambda (var*) exp*)
vars <- x[[2]]
exps <- x[-c(1, 2)]
function(args) {
inner <- Env$new(env)
inner$set(vars, args)
for (exp in exps) val <- eval(exp, inner)
val
}
} else if (identical(x[[1]], "begin")) { # (begin exp*)
for (exp in x[-1]) val <- eval(exp, env)
val
} else { # (proc exp*)
xeval <- lapply(x, function(exp) eval(exp, env))
proc <- xeval[[1]]
exps <- xeval[-1]
proc(exps)
}
}
read <- function(s) {
readFrom(tokenize(s), 1)[[1]]
}
parse <- read
tokenize <- function(s) {
s <- gsub("\\(", " ( ", s)
s <- gsub("\\)", " ) ", s)
s <- sub("^\\s+", "", s)
strsplit(s, "\\s+")[[1]]
}
readFrom <- function(tokens, i) {
if (length(tokens) < i) stop("unexpected EOF while reading")
if (tokens[i] == "(") {
L <- list()
i <- i + 1 # skip "("
while(tokens[i] != ")") {
res <- readFrom(tokens, i)
L <- append(L, res[1])
i <- res[[2]]
}
i <- i + 1 # skip ")"
return(list(L, i))
} else if (tokens[i] == ")") {
stop("unexpected )")
} else {
return(list(atom(tokens[i]), i + 1))
}
}
atom <- function(token) {
num <- suppressWarnings(as.numeric(token))
if (is.na(num)) token else num
}
toString <- function(exp) {
if (is.list(exp)) {
paste("(", do.call(paste, lapply(exp, toString)), ")", sep="")
} else {
tryCatch(as.character(exp),
error=function(e) paste("#<", typeof(exp), ">", sep=""))
}
}
repl <- function(prompt='lisp.R> ') {
env <- Env$new()
addGlobals(env)
while(TRUE) {
val <- eval(parse(readline(prompt)), env)
cat(toString(val))
cat("\n")
}
}
view raw lisp.R hosted with ❤ by GitHub

遊び方

コードを取ってきます。

$ git clone https://gist.github.com/5598108.git

Rインタプリタを起動してコードを読み込ませます。
repl() を実行するとLispの対話式インタプリタが起動します。

$ cd 5598108/
$ R -q
> source("lisp.R")
> repl()
lisp.R> (+ 1 2)
3
lisp.R> (define l (list 1 2 3))
(1 2 3)
lisp.R> (car l)
1
lisp.R> (cdr l)
(2 3)
lisp.R> (define add2 (lambda (x) (+ x 2)))
#<closure>
lisp.R> (add2 40)
42
lisp.R> (equal? (list 1 2) (list 1 (- 5 3)))
TRUE
lisp.R> (define map (lambda (f l) (if (null? l) (quote ()) (cons (f (car l)) (map f (cdr l))))))
#<closure>
lisp.R> (map add2 (list 10 20 30))
(12 22 32)
lisp.R>

感想

Rの言語機能(ファーストクラスの関数、レキシカルスコープ、クロージャなど)のおかげで、割と楽に書けたと思います。

ただリストの操作はちょっと面倒くさいかな、と思いました。
あと値渡しとか遅延評価とかでところどころはまりました。まだまだ慣れが必要です。

でも今回初めて使った機能がいくつかあって勉強になったし、何より言語処理系を作るのはとても楽しいです。

(追記)

続編を書きました。((Rで) 書く ((もっとRっぽい) Lisp) インタプリタ)

2013年5月10日金曜日

Rとクロージャ その2

前回の続きです。

Rのプロンプトで demo(scoping) を実行すると、クロージャの使用例を見ることができます。
SICPの銀行口座 (3.1.1 Local State Variables) をRで実装したものだと思います。

> demo(scoping)


        demo(scoping)
        ---- ~~~~~~~

Type  <Return>   to start : 

> ## Here is a little example which shows a fundamental difference between
> ## R and S.  It is a little example from Abelson and Sussman which models
> ## the way in which bank accounts work.       It shows how R functions can
> ## encapsulate state information.
> ##
> ## When invoked, "open.account" defines and returns three functions
> ## in a list.  Because the variable "total" exists in the environment
> ## where these functions are defined they have access to its value.
> ## This is even true when "open.account" has returned.  The only way
> ## to access the value of "total" is through the accessor functions
> ## withdraw, deposit and balance.  Separate accounts maintain their
> ## own balances.
> ##
> ## This is a very nifty way of creating "closures" and a little thought
> ## will show you that there are many ways of using this in statistics.
> 
> open.account <- function(total) {
+ 
+     list(
+        deposit = function(amount) {
+            if(amount <= 0)
+                stop("Deposits must be positive!\n")
+            total <<- total + amount
+            cat(amount,"deposited. Your balance is", total, "\n\n")
+        },
+        withdraw = function(amount) {
+            if(amount > total)
+                stop("You don't have that much money!\n")
+            total <<- total - amount
+            cat(amount,"withdrawn.  Your balance is", total, "\n\n")
+        },
+        balance = function() {
+            cat("Your balance is", total, "\n\n")
+        }
+        )
+ }

> ross <- open.account(100)

> robert <- open.account(200)

> ross$withdraw(30)
30 withdrawn.  Your balance is 70 


> ross$balance()
Your balance is 70 


> robert$balance()
Your balance is 200 


> ross$deposit(50)
50 deposited. Your balance is 120 


> ross$balance()
Your balance is 120 


> try(ross$withdraw(500)) # no way..
Error in ross$withdraw(500) : You don't have that much money!

> 

銀行口座の状態(total: 残高)はカプセル化されていて、外部からは直接参照できません。 代わりにアクセサ(deposit: 預入れ、 withdraw: 払出し、 balance: 残高確認) を通してやり取りしています。

オブジェクト指向っぽいものを簡単に実現できていて素敵ですね。