在haskell中创建循环列表的三种方法

时间:2013-09-17 00:56:53

标签: list haskell circular-list

我对Haskell中的循环/无限列表感兴趣。我读到了 let..in 语句和 where 条款,我觉得他们有重要的作用,但我仍然没有得到它。

具体来说,我已经编写了三个版本的代码,用于交替0和1的无限列表。我认为这就是Haskell中循环列表的含义。

cyclic = let x = 0 : y 
             y = 1 : x
         in x

cyclic' = [0,1] ++ cyclic'

cyclic'' = [0,1] ++ x
    where x = cyclic''

第二个对我来说似乎最简单,最短和最自然,但也许这是因为我对let..in和while仍然不太满意。

所有这三个列表都表示相同的方式吗?

3 个答案:

答案 0 :(得分:8)

我想提一个重要的区别:

cyclic' = [0,1] ++ cyclic'

cyclic'' = [0,1] ++ x
    where x = cyclic''

这两个函数是递归的,因为函数的定义引用了它自己。但

cyclic = let x = 0 : y 
             y = 1 : x
         in x

不是!它在内部使用x,这是递归的,但整个cyclic不是 - 在其定义中没有对自身的引用。这也是编译成核心语言时它们不同的原因。

这有一些重要的实际意义,即递归函数不能是inlined,而非递归函数可以。这就是为什么你经常看到像

这样的定义
fix :: (a -> a) -> a
fix f = let x = f x in x

(来自Data.Function的{​​{3}})而不是更直接的

fix f = f (fix f)

(我不确定为什么GHC不会自动执行此操作。)

为了完整起见,还有其他简短的方法来定义cyclic

-- recursive:
cyclic = 0 : 1 : cyclic
-- non-recursive:
cyclic = let x = 0 : 1 : x in x
cyclic = cycle [0,1]
cyclic = fix ([0,1] ++)

更新:举个例子:让我们来定义

-- The `const` combinator, defined explicitly so that
-- it gets inlined.
k :: a -> b -> a
k x y = x

fix, fix' :: (a -> a) -> a
fix f     = let x = f x in x
fix' f    = f (fix' f)

main = do
    print $ fix (k 1)
    print $ fix' (k 2)

因此fix'是递归的,而fix则不是(fix的定义是从Data.Function复制的)。使用fix'时会发生什么?编译器无法内联它,因为在内联之后,它将再次获得包含fix'的表达式。它应该第二次内联吗?然后是第三次?因此,递归函数永远不会被设计内联。另一方面,fix不是递归的,因此fix (k 1)被内联到let x = k 1 x in x。然后编译器内联k,结果为let x = 1 in x,仅由1替换。

我们可以通过以核心语言转储已编译的代码来验证上述声明:

$ ghc -ddump-simpl  -dsuppress-all Main.hs 
[1 of 1] Compiling Main             ( Main.hs, Main.o )

==================== Tidy Core ====================
Result size of Tidy Core = {terms: 24, types: 27, coercions: 0}

Rec {
fix'_reJ
fix'_reJ = \ @ a_c f_aeR -> f_aeR (fix'_reJ f_aeR)
end Rec }

main
main =
  >>
    $fMonadIO
    ($ (print $fShowInteger) (__integer 1))
    ($ (print $fShowInteger)
       (fix'_reJ
          (let {
             x_aeN
             x_aeN = __integer 2 } in
           \ _ -> x_aeN)))

main
main = runMainIO main

答案 1 :(得分:2)

您可以通过编译-fext-core轻松地自行检查,.hrc将为每个源文件编写相应的%module main:Main %rec {main:Main.cycliczqzq :: ghczmprim:GHCziTypes.ZMZN ghczmprim:GHCziTypes.Int = base:GHCziBase.zpzp @ ghczmprim:GHCziTypes.Int (ghczmprim:GHCziTypes.ZC @ ghczmprim:GHCziTypes.Int (ghczmprim:GHCziTypes.Izh (0::ghczmprim:GHCziPrim.Intzh)) (ghczmprim:GHCziTypes.ZC @ ghczmprim:GHCziTypes.Int (ghczmprim:GHCziTypes.Izh (1::ghczmprim:GHCziPrim.Intzh)) (ghczmprim:GHCziTypes.ZMZN @ ghczmprim:GHCziTypes.Int))) main:Main.cycliczqzq}; %rec {main:Main.cycliczq :: ghczmprim:GHCziTypes.ZMZN ghczmprim:GHCziTypes.Int = base:GHCziBase.zpzp @ ghczmprim:GHCziTypes.Int (ghczmprim:GHCziTypes.ZC @ ghczmprim:GHCziTypes.Int (ghczmprim:GHCziTypes.Izh (0::ghczmprim:GHCziPrim.Intzh)) (ghczmprim:GHCziTypes.ZC @ ghczmprim:GHCziTypes.Int (ghczmprim:GHCziTypes.Izh (1::ghczmprim:GHCziPrim.Intzh)) (ghczmprim:GHCziTypes.ZMZN @ ghczmprim:GHCziTypes.Int))) main:Main.cycliczq}; arot :: ghczmprim:GHCziTypes.Int = ghczmprim:GHCziTypes.Izh (0::ghczmprim:GHCziPrim.Intzh); a1rou :: ghczmprim:GHCziTypes.Int = ghczmprim:GHCziTypes.Izh (1::ghczmprim:GHCziPrim.Intzh); %rec {main:Main.cyclic :: ghczmprim:GHCziTypes.ZMZN ghczmprim:GHCziTypes.Int = ghczmprim:GHCziTypes.ZC @ ghczmprim:GHCziTypes.Int arot yrov; yrov :: ghczmprim:GHCziTypes.ZMZN ghczmprim:GHCziTypes.Int = ghczmprim:GHCziTypes.ZC @ ghczmprim:GHCziTypes.Int a1rou main:Main.cyclic}; 文件,其中包含Haskell的中间“核心语言”。在这种情况下,如果我们编译这段代码,我们就会得到相当难以阅读的代码:

ghczmprim

如果我们“清理”了一下并删除了一些{cycliczqzq :: ZMZN Int = zpzp @ Int (ZC @ Int (Izh (0 :: Intzh)) (ZC @ Int (Izh (1 :: Intzh)) (ZMZN @ Int))) cycliczqzq}; {cycliczq :: ZMZN Int = zpzp @ Int (ZC @ Int (Izh (0 :: Intzh)) (ZC @ Int (Izh (1 :: Intzh)) (ZMZN @ Int))) cycliczq}; arot :: Int = Izh (0 :: Intzh); a1rou :: Int = Izh (1 :: Intzh); {cyclic :: ZMZN Int = ZC @ Int arot yrov; yrov :: ZMZN Int = ZC @ Int a1rou cyclic}; 和诸如此类的东西,我们就得到了

cycliczqzq

我们可以很容易地告诉cycliczqcyclic''具有完全相同的定义,我们可以分别告诉它们与cyclic'cyclic相关联。对于cyclic4 :: [Int] cyclic4 = let xx = [1, 0] ++ yy yy = xx in xx ,我们可以告诉它以不同的方式定义。

修改

添加第四个定义

cyclic1

我还将其全部重命名为cyclic4-fext-core,以提高可读性。删除了所有垃圾的{cyclic4 :: ZMZN Int = zpzp @ Int (ZC @ Int (Izh (1::Intzh)) (ZC @ Int (Izh (0::Intzh)) (ZMZN @ Int))) cyclic4}; {cyclic3 :: ZMZN Int = zpzp @ Int (ZC @ Int (Izh (0::Intzh)) (ZC @ Int (Izh (1::Intzh)) (ZMZN @ Int))) cyclic3}; {cyclic2 :: ZMZN Int = zpzp @ Int (ZC @ Int (Izh (0::Intzh)) (ZC @ Int (Izh (1::Intzh)) (ZMZN @ Int))) cyclic2}; aroR :: Int = Izh (0::Intzh); a1roS :: Int = Izh (1::Intzh); {cyclic1 :: ZMZN Int = ZC @ Int aroR yroT; yroT :: ZMZN Int = ZC @ Int a1roS cyclic1}; 输出为

{{1}}

因此我们可以看到最后三个定义实际上变成了相同的字节代码。

此外,这些都是在没有优化的情况下编译的,因为这使得阅读更加困难。

答案 2 :(得分:1)

扩展@PetrPudlak示例提供了进一步的见解:

fix f = let x = f x in x

fix' f = f (fix' f)

k :: (Int -> Int) -> Int -> Int
k f 0 = 1
k f i = f $ i-1

main = do
         print $ fix k 10
         print $ fix' k 10

编译:

ghc -ddump-simpl -dsuppress-all c.hs

==================== Tidy Core ====================
Result size = 59

k_ra0
k_ra0 =
  \ f_aa4 ds_dru ->
    case ds_dru of wild_X6 { I# ds1_drv ->
    case ds1_drv of _ {
      __DEFAULT -> $ f_aa4 (- $fNumInt wild_X6 (I# 1));
      0 -> I# 1
    }
    }

main
main =
  >>
    $fMonadIO
    ($ (print $fShowInt)
       (letrec {
          x_ah6
          x_ah6 = k_ra0 x_ah6; } in
        x_ah6 (I# 10)))
    ($ (print $fShowInt)
       (letrec {
          fix'_ah0
          fix'_ah0 = \ f_aa3 -> f_aa3 (fix'_ah0 f_aa3); } in
        fix'_ah0 k_ra0 (I# 10)))

main
main = runMainIO main

很明显,第一种情况fix可以构造一个常量,它会在递归中重用,但第二种情况fix'必须构造一个新的存根。递归的步骤。