make a lispをやる
まずはes6実装見ながらコピペしつつ差分読んだ
ちゃんと自分で書かなかったので2周めやろう
いろんな言語で実装済み
jsでやるか ブラウザで動かしたいし
jsのやつを見ると、ES2015以前だったりするので修正する
step0で必要なその他ファイル
filesnode_readline.js | readlineする |
printer.js | printする |
types.js | lisp上の型あたり |
history機能がついているのだが、最初は要らないので削ってみると読みやすい
node_readlineから削る
make a lisp自体がいろんな言語で実装できるようになってて、jsでもffiで共通のインターフェースになってる
まあこれはそのまま使う
各ステップごとにそこで完成している
$ node step0_repl.js
これでもう、何もせずechoしてくれるやつが動く
トップレベルで make test^yourimpl^step0
をやれば、既存のテストが走ってくれる
step1
step0は文字列返すだけだった
パースしよう
READがパースする
read_str
printのとこに_pr_strが挟まった
printer readerはコピペして目を通した
正規表現でまるっとトークナイズして
素直にオブジェクトに変換
再帰呼び出ししてる
read_atom
js } else if (token.match(/^"(?:\\.|[^\\"])*"$/)) {
return token.slice(1, token.length - 1).replace(/\\(.)/g, function (_, c) {
return c === "n" ? "\n" : c;
});
ここがわからん
defferable (遅延可能)
step2
evalにenvとして、処理したい中身を渡すのか
jsfunction _EVAL(ast, env) {
//printer.println("EVAL:", printer._pr_str(ast, true));
if (!types._list_Q(ast)) {
return eval_ast(ast, env);
}
if (ast.length === 0) {
return ast;
}
// apply list
var el = eval_ast(ast, env);
var f = el[0];
return f.apply(f, el.slice(1));
}
function EVAL(ast, env) {
var result = _EVAL(ast, env);
return (typeof result !== "undefined") ? result : null;
}
リストでなければ、astをそのまま評価するが
リストであれば、関数呼び出しとして返す
js// eval
function eval_ast(ast, env) {
if (types._symbol_Q(ast)) {
if (ast in env) {
return env[ast];
} else {
throw new Error("'" + ast.value + "' not found");
}
} else if (types._list_Q(ast)) {
return ast.map(function(a) { return EVAL(a, env); });
} else if (types._vector_Q(ast)) {
var v = ast.map(function(a) { return EVAL(a, env); });
v.__isvector__ = true;
return v;
} else if (types._hash_map_Q(ast)) {
var new_hm = {};
for (k in ast) {
new_hm[EVAL(k, env)] = EVAL(ast[k], env);
}
return new_hm;
} else {
return ast;
}
}
_symbol_Qの名前がよくわからんが、astがシンボルかどうかを返す
シンボルをそのままevalした場合、 env[ast]
とあるように、環境上にある何らかの中身そのものを返す
リストやベクタだったら、中身をEVALしてから返す
ハッシュマップも同様に中身をEVALする
どれでもなかったらそのまま帰す
環境というものを、単なる文字と関数のセットとみなせば良い?

step3 environments
前のステップで加減乗除の関数をenvとして定義したのを考慮すれば、関数定義というのはenvに対する操作であるということは簡単にわかる
let!で新規環境作成
def!で既存の環境を変更
evalのとこ
js // apply list
var a0 = ast[0], a1 = ast[1], a2 = ast[2], a3 = ast[3];
switch (a0.value) {
case "def!":
var res = EVAL(a2, env);
return env.set(a1, res);
case "let*":
var let_env = new Env(env);
for (var i=0; i < a1.length; i+=2) {
let_env.set(a1[i], EVAL(a1[i+1], let_env));
}
return EVAL(a2, let_env);
default:
var el = eval_ast(ast, env), f = el[0];
return f.apply(f, el.slice(1));
}
listの部分
defかletかで変わる
そうでなければ関数呼び出し
def!
環境にsetする
let*
既存のenvから新規作成
(let* (c 2) c) -> 2
2つ目のとこに文字、値のセットを繰り返せる
letによるenvの作成は、既存のものを持ったまま別に作成する感じなのか
こっからesmのやつ見つけたのでそっちに切り替えた
step4
if, fn, do
これらはenvの操作をする特別なform
js case "do":
return eval_ast(ast.slice(1), env)[ast.length - 2];
case "if":
let cond = EVAL(a1, env);
if (cond === null || cond === false) {
return typeof a3 !== "undefined" ? EVAL(a3, env) : null;
} else {
return EVAL(a2, env);
}
case "fn*":
return (...args) => EVAL(a2, new_env(env, a1, args));
do
do以降を評価する。最後の部分を返り値とする
if
最初を評価して、それに基づいてどっちか評価して返す
fn*
他と違い、評価結果を返すのではなく関数を返す
envの操作をしてる
lisp(fn* (a) a) -> #<function>
( (fn* (a) a) 7) -> 7
( (fn* (a) (+ a 1)) 10) -> 11
( (fn* (a b) (+ a b)) 2 3) -> 5
なるほど
関数定義はこれでいいのか
envのところで、引数をkeyとして、渡されたやつをvalueとしてsetすればいいのか
tail call optimization 略してTCO
単にtail callsともいうらしい
eval全体をwhileループに囲う
let*
js case "let*":
let let_env = new_env(env);
for (let i = 0; i < a1.length; i += 2) {
env_set(let_env, a1[i], EVAL(a1[i + 1], let_env));
}
return EVAL(a2, let_env);
js case "let*":
let let_env = new_env(env);
for (let i = 0; i < a1.length; i += 2) {
env_set(let_env, a1[i], EVAL(a1[i + 1], let_env));
}
env = let_env;
ast = a2;
break; // continue TCO loop
はあーなるほどね
自分自身を呼ぶんじゃなくて、渡したかった引数にsetしてもっかいwhileループに任せればいいと
なんかv8はここらへん、勝手にいい感じにしてくれないのかな?しらんけど
あと関数をenvとかastをラップできるようにしてある
malfunc
step6 file mutation eval
evalをevilという流れなに?
diff+env_set(repl_env, Symbol.for("eval"), (a) => EVAL(a, repl_env));
+env_set(repl_env, Symbol.for("*ARGV*"), []);
// core.mal: defined using language itself
REP("(def! not (fn* (a) (if a false true)))");
+REP(
+ '(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))'
+);
+
+if (process.argv.length > 2) {
+ env_set(repl_env, Symbol.for("*ARGV*"), process.argv.slice(3));
+ REP(`(load-file "${process.argv[2]}")`);
+ process.exit(0);
+}
こんだけ
evalとARGV
load-file
read-string
coreで定義してるが、これはReaderで定義したread_str
strを読んでtokenizeする
slurpもcore
nodeだったらreadFileSyncして、ブラウザっぽかったらhttp requestする
js// String functions
function slurp(f) {
if (typeof process !== 'undefined') {
return readFileSync(f, 'utf-8')
} else {
var req = new XMLHttpRequest()
req.open('GET', f, false)
req.send()
if (req.status !== 200) {
_error(`Failed to slurp file: ${f}`)
}
return req.responseText
}
}
slurpをdo で囲って、ひらすらファイルを読んで最後に\n
これをread-stringにわたすので、まるまるファイルを1行で入力したことになる
atomのサポート
clojureのatomにinspiredされてる
atomは任意の型の一つの値への参照
symbolっぽい?

参照を変更できるらしい
これでstateを表現する?
typesにAtom classある
jsexport class Atom {
constructor(val) {
this.val = val;
}
}
といってもこんだけ
coreに各種関数がある
atom
atom?
deref
atm => atm.val
reset!
(atm, a) => atm.val = a
swap!
(atm, f, ...args) => (atm.val = f(atm.val, ...args))
readerにもread_atomとかある
あとderefのaliasとして@が使えるらしい
step7 quoting
eval ループ
jscase "quote":
return a1;
case "quasiquote":
ast = quasiquote(a1);
break; // continue TCO loop
quoteは評価せず、そのまま第一引数を返す
quasiquote
js// eval
const is_pair = (x) => Array.isArray(x) && x.length > 0;
const quasiquote = (ast) => {
if (!is_pair(ast)) {
return [Symbol.for("quote"), ast];
} else if (ast[0] === Symbol.for("unquote")) {
return ast[1];
} else if (is_pair(ast[0]) && ast[0][0] === Symbol.for("splice-unquote")) {
return [Symbol.for("concat"), ast[0][1], quasiquote(ast.slice(1))];
} else {
return [Symbol.for("cons"), quasiquote(ast[0]), quasiquote(ast.slice(1))];
}
};
再帰呼び出ししてる
lisp(def! lst (quote (2 3))) -> (2 3)
(quasiquote (1 (unquote lst))) -> (1 (2 3))
(quasiquote (1 (splice-unquote lst))) -> (1 2 3)
quoteの意味は分かるのだが、何も指定しなければlistは関数呼び出しという制約に例外を作るから初学者には悪い気がする
quasiquote
ググってみた感じ
quoteしたいけど内側は評価したい!みたいな感じか?
unquoteで埋め込める
その上で定義見たら分かってきた
lispuser> (quasiquote (1 4 ( 5 6 (5 (unquote lst )) ) ))
(1 4 (5 6 (5 (3 5))))
再帰でもちゃんとunquoteできた
だったら最初からquoteの動作これでいいのでは?

まあいいや
step8 macro
repl
diff+ case "defmacro!":
+ let func = _clone(EVAL(a2, env));
+ func.ismacro = true;
+ return env_set(env, a1, func);
+ case "macroexpand":
+ return macroexpand(a1, env);
_clone lispのデータをcloneするやつ
defmacroはenv_setの結果を返す
jsfunction macroexpand(ast, env) {
while (_list_Q(ast) && typeof ast[0] === "symbol" && ast[0] in env) {
let f = env_get(env, ast[0]);
if (!f.ismacro) {
break;
}
ast = f(...ast.slice(1));
}
return ast;
}
astがlist かつ第一引数がsymbolで、かつenvに存在する間ループ
マクロでなければbreak
astの残りをmacroに流す
lisp(defmacro! one (fn* () 1))
(one) #=> 1
これだとあまり
lisp(defmacro! unless (fn* (pred a b) `(if ~pred ~b ~a)))
(unless false 7 8)
ほんでeval時にlistなら必ずmacroexpandを呼ぶようになってる
macroじゃなければ何も変化なし
lispuser> (macroexpand (unless true 1 4))
(if true 4 1)
user> (macroexpand (unless false 1 4))
(if false 4 1)
ちゃんと呼べる
macroも結局はismacroが付いてるだけの関数で
js case "def!":
return env_set(env, a1, EVAL(a2, env));
case "defmacro!":
let func = _clone(EVAL(a2, env));
func.ismacro = true;
return env_set(env, a1, func);
何が違うんだろう
cloneしているかどうか
呼び出し側
expandmacro
jslet f = env_get(env, ast[0]);
if(!f.ismacro) break;
ast = f(...ast.slice(1));
eval内のlistの評価(defaultのとこ
js// astに (somefn 5 6) がきてる
let [f, ...args] = eval_ast(ast, env);
if (_malfunc_Q(f)) {
env = new_env(f.env, f.params, args);
ast = f.ast;
break; // continue TCO loop
} else {
return f(...args);
}
よくわからん
new_envしてないとかはあるが
macroが何やっているのかはわかるのだが、defとどう違うのかがいまいちつかめてない
def
ただたんに評価した結果を埋め込む
defmacro
リストを受けてリストを返す関数を定義して、macroとして呼び出せるようにする
defmacroでcondを定義してる
lisp(defmacro! cond
(fn*
(& xs)
(if
(> (count xs) 0)
(list
'if
(first xs)
(if
(> (count xs) 1)
(nth xs 1)
(throw "odd number of forms to cond"))
(cons 'cond (rest (rest xs)))))))
step9 try
try*
フォーム末尾のアスタリスクって時々出てくるけどどういう意味だろう
実装側でもtry catchしてやればいい
CとかGOで実装するときってどうやるんだろ
返り値で見るのかな?
diff--- a/step8_macros.js
+++ b/step9_try.js
@@ -87,6 +87,19 @@ const EVAL = (ast, env) => {
return env_set(env, a1, func);
case "macroexpand":
return macroexpand(a1, env);
+ case "try*":
+ try {
+ return EVAL(a1, env);
+ } catch (exc) {
+ if (a2 && a2[0] === Symbol.for("catch*")) {
+ if (exc instanceof Error) {
+ exc = exc.message;
+ }
+ return EVAL(a2[2], new_env(env, [a2[1]], [exc]));
+ } else {
+ throw exc;
+ }
+ }
case "do":
eval_ast(ast.slice(1, -1), env);
ast = ast[ast.length - 1];
@@ -162,7 +175,7 @@ while (true) {
if (exc instanceof Error) {
console.warn(exc.stack);
} else {
- console.warn(`Error: ${exc}`);
+ console.warn(`Error: ${pr_str(exc, true)}`);
}
}
}
throwのcore関数が必要
stepA
Aってなに?