实现惰性函数语言时,有必要将值存储为未评估的thunk,仅在需要时进行评估。
有效实施的挑战之一,如在例如 Spineless无标记G-machine ,这个评估必须只为每个thunk执行一次,后续访问必须重用计算值 - 失败这样做会导致至少二次减速(也许指数?我不确定我的头脑。)
我正在寻找一个简单的示例实现,其操作很容易理解(与GHC这样的工业级实现相反,GHC专为简化性能而设计)。我在http://www.andrej.com/plzoo/遇到了minihaskell,其中包含以下代码。
由于它被称为“一个有效的解释器”,我认为它确实只执行了一次每次评估,并保存计算值以便重复使用,但我很难看到何处和如何;我只能在解释器本身看到一个赋值语句,看起来它不会覆盖thunk记录的一部分。
所以我的问题是,这个解释器确实在做这样的缓存,如果是这样的话在哪里以及如何? (如果没有,那么最简单的现存实现是什么呢?)
来自http://www.andrej.com/plzoo/html/minihaskell.html的代码
let rec interp env = function
| Var x ->
(try
let r = List.assoc x env in
match !r with
VClosure (env', e) -> let v = interp env' e in r := v ; v
| v -> v
with
Not_found -> runtime_error ("Unknown variable " ^ x))
... snipping the easy stuff ...
| Fun _ as e -> VClosure (env, e)
| Apply (e1, e2) ->
(match interp env e1 with
VClosure (env', Fun (x, _, e)) ->
interp ((x, ref (VClosure (env, e2)))::env') e
| _ -> runtime_error "Function expected in application")
| Pair _ as e -> VClosure (env, e)
| Fst e ->
(match interp env e with
VClosure (env', Pair (e1, e2)) -> interp env' e1
| _ -> runtime_error "Pair expected in fst")
| Snd e ->
(match interp env e with
VClosure (env', Pair (e1, e2)) -> interp env' e2
| _ -> runtime_error "Pair expected in snd")
| Rec (x, _, e) ->
let rec env' = (x,ref (VClosure (env',e))) :: env in
interp env' e
| Nil ty -> VNil ty
| Cons _ as e -> VClosure (env, e)
| Match (e1, _, e2, x, y, e3) ->
(match interp env e1 with
VNil _ -> interp env e2
| VClosure (env', Cons (d1, d2)) ->
interp ((x,ref (VClosure(env',d1)))::(y,ref (VClosure(env',d2)))::env) e3
| _ -> runtime_error "List expected in match")
答案 0 :(得分:13)
关键是记录:通知!r
,r := v
。每当我们从环境中查找变量时,我们实际上都会返回一条记录,我们将其取消引用以查看它是否为thunk。如果是thunk,我们会对其进行评估,然后保存结果。我们在应用程序期间创建thunk(注意对ref
构造函数的调用),递归定义和模式匹配,因为这些是绑定变量的构造。
答案 1 :(得分:10)
这是两个按需调用的口译员;一个在Haskell,一个在Scheme。两者的关键是你可以在没有参数(thunk)的过程中暂停评估。无论您的宿主语言是按需调用(Haskell)还是按值调用(Scheme,ML),lambda表单都被视为值,因此在应用thunk之前,不会评估lambda下的任何内容。
因此,当解释函数应用于参数时,您只需将参数的未评估语法表示包装在新的thunk中。然后,当你遇到一个变量时,你在环境中查找并迅速评估thunk,给你参数的值
简单地说到这一点会使你的解释器变得懒惰,因为参数在被使用之前不会被实际评估;这是一个按名称的口译员。但是,正如您所指出的,一种有效的惰性语言只会评估这些参数一次;这种语言是需要的。为了提高效率,您需要更新环境,而不是包含仅包含参数值的thunk,而不是整个参数表达式。
这里的第一个解释器是在Haskell中,与你粘贴的ML代码非常相似。当然,Haskell面临的挑战是:1)由于Haskell内置的懒惰,并没有轻易实现懒惰,2)将副作用与代码纠缠在一起。 Haskell的IORef
用于允许更新环境。
module Interp where
import Data.IORef
data Expr = ExprBool Bool
| ExprInt Integer
| ExprVar String
| ExprZeroP Expr
| ExprSub1 Expr
| ExprMult Expr Expr
| ExprIf Expr Expr Expr
| ExprLam String Expr
| ExprApp Expr Expr
deriving (Show)
data Val = ValBool Bool
| ValInt Integer
| ValClos ((() -> IO Val) -> IO Val)
instance Show Val where
show (ValBool b) = show b
show (ValInt n) = show n
show (ValClos c) = "Closure"
data Envr = EnvrEmpty
| EnvrExt String (IORef (() -> IO Val)) Envr
applyEnv :: Envr -> String -> IO (IORef (() -> IO Val))
applyEnv EnvrEmpty y = error $ "unbound variable " ++ y
applyEnv (EnvrExt x v env) y =
if x == y
then return v
else applyEnv env y
eval :: Expr -> Envr -> IO Val
eval exp env = case exp of
(ExprBool b) -> return $ ValBool b
(ExprInt n) -> return $ ValInt n
(ExprVar y) -> do
thRef <- applyEnv env y
th <- readIORef thRef
v <- th ()
writeIORef thRef (\() -> return v)
return v
(ExprZeroP e) -> do
(ValInt n) <- eval e env
return $ ValBool (n == 0)
(ExprSub1 e) -> do
(ValInt n) <- eval e env
return $ ValInt (n - 1)
(ExprMult e1 e2) -> do
(ValInt n1) <- eval e1 env
(ValInt n2) <- eval e2 env
return $ ValInt (n1 * n2)
(ExprIf te ce ae) -> do
(ValBool t) <- eval te env
if t then eval ce env else eval ae env
(ExprLam x body) ->
return $ ValClos (\a -> do
a' <- newIORef a
eval body (EnvrExt x a' env))
(ExprApp rator rand) -> do
(ValClos c) <- eval rator env
c (\() -> eval rand env)
-- "poor man's Y" factorial definition
fact = ExprApp f f
where f = (ExprLam "f" (ExprLam "n" (ExprIf (ExprZeroP (ExprVar "n"))
(ExprInt 1)
(ExprMult (ExprVar "n")
(ExprApp (ExprApp (ExprVar "f")
(ExprVar "f"))
(ExprSub1 (ExprVar "n")))))))
-- test factorial 5 = 120
testFact5 = eval (ExprApp fact (ExprInt 5)) EnvrEmpty
-- Omega, the delightful infinite loop
omega = ExprApp (ExprLam "x" (ExprApp (ExprVar "x") (ExprVar "x")))
(ExprLam "x" (ExprApp (ExprVar "x") (ExprVar "x")))
-- show that ((\y -> 5) omega) does not diverge, because the
-- interpreter is lazy
testOmega = eval (ExprApp (ExprLam "y" (ExprInt 5)) omega) EnvrEmpty
第二个解释器在Scheme中,唯一真正的样板是Oleg的模式匹配宏。我发现在Scheme版本中可以更容易地看到懒惰的来源。 box
函数允许更新环境; Chez Scheme包括它们,但我已经包含了适用于其他人的定义。
(define box
(lambda (x)
(cons x '())))
(define unbox
(lambda (b)
(car b)))
(define set-box!
(lambda (b v)
(set-car! b v)))
;; Oleg Kiselyov's linear pattern matcher
(define-syntax pmatch
(syntax-rules (else guard)
((_ (rator rand ...) cs ...)
(let ((v (rator rand ...)))
(pmatch v cs ...)))
((_ v) (errorf 'pmatch "failed: ~s" v))
((_ v (else e0 e ...)) (begin e0 e ...))
((_ v (pat (guard g ...) e0 e ...) cs ...)
(let ((fk (lambda () (pmatch v cs ...))))
(ppat v pat (if (and g ...) (begin e0 e ...) (fk)) (fk))))
((_ v (pat e0 e ...) cs ...)
(let ((fk (lambda () (pmatch v cs ...))))
(ppat v pat (begin e0 e ...) (fk))))))
(define-syntax ppat
(syntax-rules (uscore quote unquote)
((_ v uscore kt kf)
; _ can't be listed in literals list in R6RS Scheme
(and (identifier? #'uscore) (free-identifier=? #'uscore #'_))
kt)
((_ v () kt kf) (if (null? v) kt kf))
((_ v (quote lit) kt kf) (if (equal? v (quote lit)) kt kf))
((_ v (unquote var) kt kf) (let ((var v)) kt))
((_ v (x . y) kt kf)
(if (pair? v)
(let ((vx (car v)) (vy (cdr v)))
(ppat vx x (ppat vy y kt kf) kf))
kf))
((_ v lit kt kf) (if (equal? v (quote lit)) kt kf))))
(define empty-env
(lambda ()
`(empty-env)))
(define extend-env
(lambda (x v env)
`(extend-env ,x ,v ,env)))
(define apply-env
(lambda (env y)
(pmatch env
[(extend-env ,x ,v ,env)
(if (eq? x y)
v
(apply-env env y))])))
(define value-of
(lambda (exp env)
(pmatch exp
[,b (guard (boolean? b)) b]
[,n (guard (integer? n)) n]
[,y (guard (symbol? y))
(let* ([box (apply-env env y)]
[th (unbox box)]
[v (th)])
(begin (set-box! box (lambda () v)) v))]
[(zero? ,e) (zero? (value-of e env))]
[(sub1 ,e) (sub1 (value-of e env))]
[(* ,e1 ,e2) (* (value-of e1 env) (value-of e2 env))]
[(if ,t ,c ,a) (if (value-of t env)
(value-of c env)
(value-of a env))]
[(lambda (,x) ,body)
(lambda (a) (value-of body (extend-env x a env)))]
[(,rator ,rand) ((value-of rator env)
(box (lambda () (value-of rand env))))])))
;; "poor man's Y" factorial definition
(define fact
(let ([f '(lambda (f)
(lambda (n)
(if (zero? n)
1
(* n ((f f) (sub1 n))))))])
`(,f ,f)))
;; test factorial 5 = 120
(define testFact5
(lambda ()
(value-of `(,fact 5) (empty-env))))
;; Omega, the delightful infinite loop
(define omega
'((lambda (x) (x x)) (lambda (x) (x x))))
;; show that ((lambda (y) 5) omega) does not diverge, because the interpreter
;; is lazy
(define testOmega
(lambda ()
(value-of `((lambda (y) 5) ,omega) (empty-env))))
答案 2 :(得分:2)
你应该看看使用组合器(SKI)减少图形。它美观而简单,并说明了懒惰的评估是如何运作的。
答案 3 :(得分:0)
您可能对Alef( Alef Lazily Evaluates Functions )感兴趣,这是一种非常简单,纯粹,懒惰的函数式编程语言,我最初专门用于通过图形缩减来解释惰性求值。它在不到500行的Common Lisp中实现,包括一些简洁的可视化功能。 http://gergo.erdi.hu/blog/2013-02-17-write_yourself_a_haskell..._in_lisp/
不幸的是,我还没有完成“在Lisp中使用Typecheck Yourself a Haskell”,尽管大部分代码都是在我发布第1部分时编写的。