【问题标题】:three ways to create a circular list in haskell在haskell中创建循环列表的三种方法
【发布时间】:2013-09-17 00:56:52
【问题描述】:

我对 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。

这三个列表的表示方式是否相同?

【问题讨论】:

  • 当我这样做的时候,让我再添加一个版本:'cyclic3 = let x = [0,1] ++ y y = x in x '
  • 你可以用Data.List函数repeatcycle创建无限列表
  • 第四个选项,就是cyclic = 0:1:cyclic

标签: list haskell circular-list


【解决方案1】:

我想提一个重要的区别:

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.Functionsource)而不是更直接的

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

【讨论】:

  • 区分很重要。在功能上你得到了相同的结果,但你应该知道机器内部发生了什么。
  • 解释太短了。你能详细说明一下吗? x = f x 是递归的。那你买什么?内联修复?但是 fix 的“内部”无论如何都不能内联,因为它是递归的。
  • 哦,酷! x = f x 意味着 fix g 被编译成 g 调用自己;然而定义 fix g = g (fix g) 被编译成 g 调用 fix,在递归的每一步都添加了间接性。
  • @SassaNF 我添加了一个示例,展示了非递归定义如何带来显着的改进。
  • @PetrPudlák 我认为一个更有趣的例子很重要。用常量替换递归可以深入了解一层内联。 k :: (Int -> Int) -> Int -> Int 定义为 k f 0 = 1; k f i = f $ i-1 会产生更有趣的代码。在那里我们可以看到“递归调用”函数在x = f x 情况下作为常数计算一次,并且必须在f (fix' f) 情况下的每一步递归计算。
【解决方案2】:

您可以通过使用-fext-core 进行编译来轻松检查这一点,这将为您的每个源文件编写一个相应的.hrc 文件,其中包含Haskell 的中间“核心语言”。在这种情况下,如果我们编译这段代码,我们会得到相当难以阅读的代码:

%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};

如果我们稍微“清理”一下并删除一些ghczmprims 和诸如此类的东西,我们就会得到

{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};

其中我们可以很容易地看出cycliczqzqcycliczq 具有完全相同的定义,并且我们可以看出它们分别与cyclic''cyclic' 相关。对于cyclic,我们可以看出它是以不同的方式定义的。

编辑:

添加第四个定义

cyclic4 :: [Int]
cyclic4 =
    let xx = [1, 0] ++ yy
        yy = xx
    in xx

我还将它们全部重命名为 cyclic1cyclic4 以提高可读性。 -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};

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

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

【讨论】:

    【解决方案3】:

    扩展@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' 必须在递归的每一步中构造一个新的存根。

    【讨论】:

      猜你喜欢
      • 2014-10-12
      • 1970-01-01
      • 2022-01-22
      • 2019-06-19
      • 2020-07-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-16
      相关资源
      最近更新 更多