Haskells 弱头范式

Posted

技术标签:

【中文标题】Haskells 弱头范式【英文标题】:Haskells Weak Head Normal Form 【发布时间】:2020-05-20 04:52:19 【问题描述】:

我偶然发现了一些恼人的事情。我知道 haskell 适用于弱头范式(WHNF),我知道这是什么。在 ghci 中键入以下代码(据我所知,我正在使用命令 :sprint 将表达式简化为 WHNF。):

let intlist = [[1,2],[2,3]]
:sprint intlist

intlist = _ 这对我来说完全有意义。

let stringlist = ["hi","there"]
:sprint stringlist 

stringlist = [_,_] 这已经让我很困惑了。但后来:

let charlist = [['h','i'], ['t','h','e','r','e']]
:sprint charlist

居然给charlist = ["hi","there"]

据我了解 Haskell,字符串只不过是字符列表,这似乎可以通过检查 "hi" :: [Char]['h','i'] :: [Char] 类型来确认。

我很困惑,因为根据我的理解,上面所有三个示例都或多或少相同(列表列表),因此应该简化为相同的 WHNF,即_。我错过了什么?

谢谢

【问题讨论】:

This 好像有关系 @Bergi 这些问题肯定是相关的,但似乎都没有解决为什么 "bla"['b','l','a'] 会出现不同的结果。 @leftaroundabout 因为"bla" 可能被重载,但['b','l','a'] 已知是String/[Char]? @Bergi 我也想过这个问题,但这不太合理,因为['b', 'l', 'a'] 可以also be overloaded,同样"bla" 只有在-XOverloadedStrings 开启时才会过载。 似乎与解析器相关,可能特定于 GHCi? (我不知道您如何在 GHC 编译的代码中测试 WHNF。)引号本身似乎是触发器。 【参考方案1】:

请注意,:sprint 确实将表达式简化为 WHNF。如果是这样,那么以下将给出4 而不是_

Prelude> let four = 2 + 2 :: Int
Prelude> :sprint four
four = _

相反,:sprint 采用绑定的名称,遍历绑定值的内部表示,并在使用 _ 作为占位符时显示已经“评估的部分”(即构造函数的部分)未评估的 thunk(即暂停的惰性函数调用)。如果该值完全未评估,则不会进行评估,甚至不会对 WHNF 进行评估。 (如果该值被完全评估,您将得到它,而不仅仅是 WHNF。)

您在实验中观察到的是多态与单态数字类型、字符串文字的不同内部表示与显式字符列表等的组合。基本上,您正在观察不同文字表达式如何编译为的技术差异字节码。因此,将这些实现细节解释为与 WHNF 有关,这会让您感到困惑。通常,您应该将:sprint 仅用作调试工具,而不是作为了解 WHNF 和 Haskell 评估语义的一种方式。

如果你真的想了解:sprint 在做什么,你可以在 GHCi 中打开一些标志来查看表达式是如何被实际处理并最终编译为字节码的:

> :set -ddump-simpl -dsuppress-all -dsuppress-uniques

在此之后,我们可以看到您的intlist 给出_ 的原因:

> let intlist = [[1,2],[2,3]]
==================== Simplified expression ====================
returnIO
  (: ((\ @ a $dNum ->
         : (: (fromInteger $dNum 1) (: (fromInteger $dNum 2) []))
           (: (: (fromInteger $dNum 2) (: (fromInteger $dNum 3) [])) []))
      `cast` <Co:10>)
     [])

您可以忽略returnIO 和外部: 调用,专注于以((\ @ a $dNum -&gt; ... 开头的部分

这里的$dNumNum 约束的字典。这意味着生成的代码尚未解析Num a =&gt; [[a]] 类型中的实际类型a,因此整个表达式仍表示为一个函数调用,该函数调用采用(字典)适当的Num 类型。换句话说,这是一个未经评估的重击,我们得到:

> :sprint intlist
_

另一方面,指定类型为Int,代码完全不同:

> let intlist = [[1::Int,2],[2,3]]
==================== Simplified expression ====================
returnIO
  (: ((: (: (I# 1#) (: (I# 2#) []))
         (: (: (I# 2#) (: (I# 3#) [])) []))
      `cast` <Co:6>)
     [])

:sprint 输出也是如此:

> :sprint intlist
intlist = [[1,2],[2,3]]

同样,文字字符串和显式字符列表具有完全不同的表示:

> let stringlist = ["hi", "there"]
==================== Simplified expression ====================
returnIO
  (: ((: (unpackCString# "hi"#) (: (unpackCString# "there"#) []))
      `cast` <Co:6>)
     [])

> let charlist = [['h','i'], ['t','h','e','r','e']]
==================== Simplified expression ====================
returnIO
  (: ((: (: (C# 'h'#) (: (C# 'i'#) []))
         (: (: (C# 't'#)
               (: (C# 'h'#) (: (C# 'e'#) (: (C# 'r'#) (: (C# 'e'#) [])))))
            []))
      `cast` <Co:6>)
     [])

:sprint 输出中的差异表示 GHCi 认为已评估(显式 : 构造函数)与未评估(unpackCString# thunks)的表达式部分的工件。

【讨论】:

以上是关于Haskells 弱头范式的主要内容,如果未能解决你的问题,请参考以下文章

理解Haskells类型签名(例如zip with with(+))

Haskell“资源缩减”

seq如何评估Haskell中的无限列表?

函数的概念Lambda 演算与 Haskell 语言——洪峰老师讲创客道(三十二)

Matrix技术分享| Haskell与函数式编程简介

函数式编程-将Monad(单子)融入Swift