我对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仍然不太满意。
所有这三个列表都表示相同的方式吗?
答案 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
我们可以很容易地告诉cycliczq
和cyclic''
具有完全相同的定义,我们可以分别告诉它们与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'
必须构造一个新的存根。递归的步骤。