Я рад видеть, что вы используете свой собственный язык! Это открывает глаза, потому что вы вынуждены понимать тонкие детали в значении языка.
Не очевидно, как добавить конструкцию цикла for к языку в его текущей форме. До сих пор ваш язык выглядит очень функционально в своем стиле. Значение выражения описывается в терминах значения его подвыражений. Очень хорошо! Возможны также некоторые побочные эффекты, например println.
Проблема с текущей формой интерпретатора заключается в том, что выражение никак не может изменить свою среду. Другими словами, вы не можете назначать переменные. Для императивной циклической конструкции, такой как «for», это необходимо. Сейчас самое время решить, хотите ли вы, чтобы циклы были обязательными (например, «для») или функциональными (с использованием рекурсии или чего-то вроде цикла / рекурсии в Clojure).
Чтобы ответить на реальный вопрос, я опишу, как добавить императивные конструкции в язык и добавить «для». Сначала я опишу чисто функциональный подход.
Сначала вам нужно реализовать конструкцию set. Что оно делает? Учитывая '(set x 2)
и среду '((x 1))
, она должна создать новую среду, такую как '((x 2))
. Это хорошо согласуется с существующими функциями интерпретации, поскольку они получают выражение и среду и возвращают значение. Нам также нужно иметь возможность вернуть измененную среду, чтобы реализовать set!
Одним из решений является изменение контракта функций «interpret-foo» и разрешение им возвращать пару значений и среду. Вот как выглядит interpret-plus в новой схеме:
(defn interpret-plus [plus-expn env0]
(let [[_ l-expn r-expn] plus-expn
[l-val env1] (interpret l-expn env0)
[r-val env2] (interpret r-expn env1)]
[(+ val1 val2) env2]))
Обратите внимание на то, как эффекты изменения среды «пронизывают» через интерпретирующие вызовы. Я также позволил себе немного реорганизовать код: (def f (fn ...))
→ (defn f ...)
, (f (first x) (second x))
→ (let [[a b] x] (f a b))
, (list a b)
→ [a b]
. В новой схеме «набор» легко реализовать:
(defn set-var [env var val]
(cond (empty? env) 'error
(= (first (first env)) var) (cons (list var val) (rest env))
:else (cons (first env) (set-var (rest env) var val))))
(defn interpret-set [set-expn env0]
(let [[_ var val-expn] set-expn
[val env1] (interpret val-expn env0)]
[val (set-var env1 var val)]))
Здесь я решил позволить выражению (set ...)
вычислять значение, которое оно связывает с переменной, как =
в C. Также подходят другие значения, например nil
или 'ok
. Теперь можно реализовать конструкцию «for»:
(defn apply-decl [decl env0]
(let [[var expn] decl
[val env1] (interpret expn env0)]
(add-var env1 var val)))
(defn apply-decls [decls env0]
(reduce (fn [env decl]
(apply-decl devl env))
env0
decls))
(defn interpret-for [for-expn env0]
(let [[_ decls end-test loop-update _ statement] for-expn
env1 (apply-decls devls env0)]
(loop [env env1]
(let [[end-test-val env2] (interpret end-test env)]
(if end-test-val
(let [[_ env3] (interpret statement env2)
[_ env4] (interpret loop-update env3)]
(recur env4))
[nil env2])))))
(defn interpret-seq [seq-expn env0]
(reduce (fn [env expn]
(let [[_ env1] (interpret expn env)]
env1))
env0
(rest seq-expn)))
Вот оно! Это чисто функциональная реализация. Вы также можете «позаимствовать» побочные эффекты у Clojure и позволить средам внутренне мутировать. Код становится короче, но менее явным. Вот альтернативная реализация в исходной схеме интерпретатора (где функции интерпретации просто возвращают значение):
(defn make-env []
'())
(defn add-var [env var val]
(cons (list var (atom val))
env))
(defn lookup-var [env var]
(cond (empty? env) 'error
(= (first (first env)) var) (deref (second (first env)))
:else (recur (rest env) var)))
(defn set-var [env var val]
(cond (empty? env) 'error
(= (first (first env)) var) (reset! (second (first env)) val)
:else (recur (rest env) var val)))
(defn interpret-set [expn env]
(set-var env (second expn) (interpret (third expn) env)))
(defn apply-decl [decl env]
(add-var env (first decl) (interpret (second decl) env)))
(defn apply-decls [decls env0]
(reduce (fn [env decl]
(apply-decl devl env))
env0
decls))
(defn interpret-for [expn env0]
(let [env1 (apply-decls (second expn) env0)]
(loop []
(if (interpret (third expn) env1)
(do (interpret (sixth expn) env1)
(interpret (fourth expn) env1)
(recur))
nil))))
(defn interpret-seq [seq-expn env]
(doseq [expn (rest seq-expn)]
(interpret expn env)))
Как видите, Clojure предпочитает открыто говорить о мутации. Используемый здесь эталонный примитив - это атом, а соответствующими функциями являются atom
, deref
и reset!
. Также деталь (loop [] (if x (do a b c) nil))
можно заменить на (while x a b c)
. Я признаю, что был немного небрежным; Я не проверял весь код выше. Пожалуйста, оставьте комментарий, если что-то не работает или если вы хотите, чтобы я кое-что прояснил.