实现懒惰的函数式语言

时间:2011-03-27 19:13:03

标签: haskell

实现惰性函数语言时,有必要将值存储为未评估的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")

4 个答案:

答案 0 :(得分:13)

关键是记录:通知!rr := 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部分时编写的。