generated at
make a lispをやる



まずはes6実装見ながらコピペしつつ差分読んだ
ちゃんと自分で書かなかったので2周めやろう

いろんな言語で実装済み
jsでやるか ブラウザで動かしたいし

jsのやつを見ると、ES2015以前だったりするので修正する

step0で必要なその他ファイル
files
node_readline.jsreadlineする
printer.jsprintする
types.jslisp上の型あたり
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として、処理したい中身を渡すのか
js
function _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する
どれでもなかったらそのまま帰す

環境というものを、単なる文字と関数のセットとみなせば良い?miyamonz


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っぽい?miyamonz
参照を変更できるらしい
これでstateを表現する?
typesにAtom classある
js
export 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 ループ
js
case "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で埋め込める
その上で定義見たら分かってきた

lisp
user> (quasiquote (1 4 ( 5 6 (5 (unquote lst )) ) )) (1 4 (5 6 (5 (3 5))))
再帰でもちゃんとunquoteできた

だったら最初からquoteの動作これでいいのでは?miyamonz
まあいいや

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の結果を返す

js
function 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じゃなければ何も変化なし

lisp
user> (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
js
let 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ってなに?