【问题标题】:Implementing lazy functional languages实现惰性函数式语言
【发布时间】:2014-12-01 19:11:17
【问题描述】:

在实现惰性函数式语言时,有必要将值存储为未评估的 thunk,仅在需要时进行评估。

高效实施的挑战之一,如在例如Spineless Tagless G-machine,这个评估必须为每个 thunk 执行一次,并且后续访问必须重用计算值 - 不这样做会导致至少二次减速(也许指数级的?我不确定。)

我正在寻找一个简单的示例实现,它的操作很容易理解(而不是像 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")

【问题讨论】:

  • 另见 PLAI 第 8 章,实现惰性。

标签: haskell


【解决方案1】:

关键是记录:通知!rr := v。每当我们从环境中查找一个变量时,我们实际上会返回一个记录,我们取消引用它以查看它是否是一个 thunk。如果它是一个 thunk,我们评估它然后保存结果。我们在应用期间创建 thunk(注意对 ref 构造函数的调用)、递归定义和模式匹配,因为这些是绑定变量的构造。

【讨论】:

    【解决方案2】:

    这里有两个按需调用的口译员;一个在 Haskell 中,一个在 Scheme 中。两者的关键是您可以在没有参数(thunk)的过程中暂停评估。无论您的宿主语言是按需调用 (Haskell) 还是按值调用 (Scheme, ML),lambda 形式都被视为值,因此在应用 thunk 之前不会评估 lambda 下的任何内容。

    因此,当将解释函数应用于参数时,您只需将参数的未评估句法表示包装在新的 thunk 中。然后,当您遇到一个变量时,您会在环境中查找它并立即评估 thunk,从而为您提供参数的值

    仅仅达到这一点会使您的解释器变得懒惰,因为在使用参数之前不会实际评估它们;这是一个按名称调用的解释器。但是,正如您所指出的,一种高效的惰性语言只会对这些参数进行一次评估;这种语言是按需调用的。为了获得这种效率,您需要更新环境以包含一个仅包含参数值而不是整个参数表达式的 thunk。

    这里的第一个解释器是在 Haskell 中,与您粘贴的 ML 代码非常相似。当然,Haskell 中的挑战在于 1) 由于 Haskell 内置的惰性,不能轻易实现惰性,以及 2) 将副作用处理到代码中。 Haskell 的IORefs 用于允许更新环境。

    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))))
    

    【讨论】:

      【解决方案3】:

      您应该看看使用组合器 (SKI) 的图形缩减。它既美观又简单,说明了惰性求值的工作原理。

      【讨论】:

      • 而且,不幸的是,它比 STG 机器慢得多。甚至比基于超级组合器的方法还要慢。
      • 是的,减少组合子很慢,但它很容易而且很有启发性。
      【解决方案4】:

      您可能对 Alef (Alef Lazily Evaluates Functions) 感兴趣,它是一种非常简单、纯粹、惰性的函数式编程语言,我最初创建它是为了通过图形缩减来解释惰性求值。它是用不到 500 行的 Common Lisp 实现的,包括一些简洁的可视化功能。 http://gergo.erdi.hu/blog/2013-02-17-write_yourself_a_haskell..._in_lisp/

      不幸的是,我还没有完成“在 Lisp 中对自己的 Haskell 进行类型检查...”,尽管大部分代码在我发布第 1 部分时已经编写完毕。

      【讨论】:

        猜你喜欢
        • 2010-11-21
        • 2012-04-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-09
        • 2011-12-18
        • 1970-01-01
        • 2011-10-11
        相关资源
        最近更新 更多