【问题标题】:Haskell: to fix or not to fixHaskell:修复还是不修复
【发布时间】:2014-03-08 10:12:09
【问题描述】:

我最近了解了Data.Function.fix,现在我想将它应用到任何地方。例如,每当我看到一个递归函数时,我都想“fix”它。所以基本上我的问题是我应该在何时何地使用它。

为了更具体:

1) 假设我有以下代码来分解n

f n = f' n primes
  where
    f' n (p:ps) = ...
    -- if p^2<=n: returns (p,k):f' (n `div` p^k) ps for k = maximum power of p in n
    -- if n<=1: returns []
    -- otherwise: returns [(n,1)]

如果我用fix 重写它,我会有所收获吗?丢东西?是否有可能通过将显式递归重写为fix-version 我将解决或反之亦然创建堆栈溢出?

2) 处理列表时,有几种解决方案:递归/修复、foldr/foldl/foldl',可能还有其他方法。是否有关于何时使用每种方法的一般指南/建议?例如,你会在无限的素数列表上使用foldr 重写上面的代码吗?

这里可能还没有涉及其他重要问题。也欢迎任何与fix 的使用相关的其他 cmets。

【问题讨论】:

  • "我最近了解了 Data.Function.fix,现在看来我想把它应用到任何地方。" 这让你成为一名童子军 Haskell 程序员然后 - willamette.edu/~fruehr/haskell/evolution.html#boyscout
  • 如果可以,您应该使用foldrfoldl',如果必须使用fix 或显式递归。后者的功能较弱,因此您的代码的读者可以从中推断出更多的属性。
  • @stephentetley 这是一个很棒的链接,但我已经看到了!实际上,在我第一次看到它时(并仔细研究了它!)我对其中几个实现还有另一个问题,但也许其他时间......无论如何,“童子军”实现正是我“倾向于”做的现在在我的大部分代码中。 :)
  • @TomEllis 如果您能抽出时间详细说明这一点,我将不胜感激。关于我的第一个问题,Joseph 在下面已经给了我一个很好的提示,我仍然希望根据大师的经验建立一个更通用的“指南”。
  • 注意_Y f = f (_Y f)(递归,值--复制)和fix f = x where x = f x(核心递归,引用--共享)之间的区别。

标签: haskell fixpoint-combinators


【解决方案1】:

我想提一下fix 的另一种用法;假设您有一种由加法、负数和整数文字组成的简单语言。也许你已经编写了一个解析器,它接受 String 并输出 Tree

data Tree = Leaf String | Node String [Tree]
parse :: String -> Tree

-- parse "-(1+2)" == Node "Neg" [Node "Add" [Node "Lit" [Leaf "1"], Node "Lit" [Leaf "2"]]]

现在您想将您的树评估为单个整数:

fromTree (Node "Lit" [Leaf n]) = case reads n of {[(x,"")] -> Just x; _ -> Nothing}
fromTree (Node "Neg" [e])      = liftM negate (fromTree e) 
fromTree (Node "Add" [e1,e2])  = liftM2 (+) (fromTree e1) (fromTree e2)

假设其他人决定扩展语言;他们想加乘法。他们必须有权访问原始源代码。他们可以尝试以下方法:

fromTree' (Node "Mul" [e1, e2]) = ...
fromTree' e                     = fromTree e

但是Mul 只能出现一次,在表达式的顶层,因为对fromTree 的调用不会意识到Node "Mul" 的情况。 Tree "Neg" [Tree "Mul" a b] 不起作用,因为原始 fromTree 没有 "Mul" 的模式。但是,如果使用fix编写相同的函数:

fromTreeExt :: (Tree -> Maybe Int) -> (Tree -> Maybe Int)
fromTreeExt self (Node "Neg" [e]) = liftM negate (self e)
fromTreeExt .... -- other cases

fromTree = fix fromTreeExt

那么扩展语言是可能的:

fromTreeExt' self (Node "Mul" [e1, e2]) = ...
fromTreeExt' self e                     = fromTreeExt self e

fromTree' = fix fromTreeExt'

现在,扩展的fromTree' 将正确评估树,因为fromTreeExt' 中的self 指的是整个函数,包括“Mul”案例。

这种方法用于here(上面的例子是论文中用法的一个紧密改编的版本)。

【讨论】:

  • 太棒了。这和约瑟夫的回答是修复似乎有用的两个很好的例子。
  • 不是fromTreeExt self (Node "Neg" [e]) = liftM negate (self e)吗?使用self 而不是fromTree
  • @DanielVelkov 是的,错字。
【解决方案2】:

1) fix 只是一个函数,当你使用一些递归时它会改进你的代码。它使您的代码更漂亮。例如使用访问:Haskell Wikibook - Fix and recursion

2) 你知道foldr 是做什么的吗?似乎 foldr 在分解中没有用(或者我不明白你的意思)。 这是一个没有修正的素数分解:

fact xs = map (\x->takeWhile (\y->y/=[]) x) . map (\x->factIt x) $ xs
 where factIt n = map (\x->getFact x n []) [2..n]
   getFact i n xs
    | n `mod` i == 0 = getFact i (div n i) xs++[i]
    | otherwise      = xs

并带有修复(这与之前的完全一样):

fact xs = map (\x->takeWhile (\y->y/=[]) x) . map (\x->getfact x) $ xs
  where getfact n  = map (\x->defact x n) [2..n]
       defact i n  = 
        fix (\rec j k xs->if(mod k j == 0)then (rec j (div k j) xs++[j]) else xs ) i n []

这不是很好,因为在这种情况下修复不是一个好的选择(但总有人可以写得更好)。

【讨论】:

    【解决方案3】:

    注意_Y f = f (_Y f)(递归,值--复制)和fix f = x where x = f x(核心递归,引用--共享)之间的区别。

    Haskell 的 letwhere 绑定是递归的:LHS 和 RHS 上的相同名称指的是同一个实体。参考是共享的

    _Y 的定义中没有共享(除非编译器对公共子表达式的消除进行了积极的优化)。这意味着它描述了递归,其中重复是通过应用原件的副本来实现的,就像在recursive function creating its own copies 的经典隐喻中一样。另一方面,Corecursion 依赖于共享,依赖于引用同一实体。

    一个例子,素数由

    2 : _Y ((3:) . gaps 5 . _U . map (\p-> [p*p, p*p+2*p..]))
    
    -- gaps 5 == ([5,7..] \\)
    -- _U     == sort . concat
    

    重用自己的输出(fixlet g = ((3:)...) ; ps = g ps in 2 : ps)或为自己创建单独的素数供应(_Ylet g () = ((3:)...) (g ()) in 2 : g ())。

    另见:


    或者,用通常的阶乘函数示例,

    gen rec n = n<2 -> 1 ; n * rec (n-1)            -- "if" notation
    
    facrec   = _Y gen 
    facrec 4 = gen (_Y gen) 4 
             = let {rec=_Y gen} in (\n-> ...) 4
             = let {rec=_Y gen} in (4<2 -> 1 ; 4*rec 3)
             = 4*_Y gen 3
             = 4*gen (_Y gen) 3
             = 4*let {rec2=_Y gen} in (3<2 -> 1 ; 3*rec2 2) 
             = 4*3*_Y gen 2                         -- (_Y gen) recalculated
             .....
    
    fac      = fix gen 
    fac 4    = (let f = gen f in f) 4             
             = (let f = (let {rec=f} in (\n-> ...)) in f) 4
             = let {rec=f} in (4<2 -> 1 ; 4*rec 3)  -- f binding is created
             = 4*f 3
             = 4*let {rec=f} in (3<2 -> 1 ; 3*rec 2)  
             = 4*3*f 2                              -- f binding is reused
             .....
    

    【讨论】:

      【解决方案4】:

      通过以明确的fixed 形式编写可以获得的一件事是递归保持“开放”状态。

      factOpen :: (Integer -> Integer) -> Integer -> Integer
      factOpen recur 0 = 1
      factOpen recur n = n * recur (pred n)
      

      我们可以使用fix 来获取常规的fact 回复

      fact :: Integer -> Integer
      fact = fix factOpen
      

      这是因为fix 有效地将函数本身作为其第一个参数传递。然而,通过保持递归打开,我们可以修改哪个函数被“传回”。使用此属性的最佳示例是使用 memoFix from the memoize package 之类的内容。

      factM :: Integer -> Integer
      factM = memoFix factOpen
      

      现在factM 具有内置记忆功能。

      实际上,开放式递归要求我们将递归位归为一阶事物。递归绑定是 Haskell 允许在语言级别进行递归的一种方式,但我们可以构建其他更专业的形式。

      【讨论】:

      • 非常有趣。而且功能强大!
      猜你喜欢
      • 1970-01-01
      • 2010-09-07
      • 2018-10-03
      • 2017-04-03
      • 2015-01-19
      • 2013-05-18
      • 2014-04-26
      • 2019-08-17
      • 1970-01-01
      相关资源
      最近更新 更多