在 Haskell 中评估函数 a -> () 有啥规则?

Posted

技术标签:

【中文标题】在 Haskell 中评估函数 a -> () 有啥规则?【英文标题】:What rules are there about a function a -> () being evaluated in Haskell?在 Haskell 中评估函数 a -> () 有什么规则? 【发布时间】:2020-03-23 02:16:32 【问题描述】:

正如标题所说:对 Haskell 函数返回单元进行评估有什么保证?有人会认为在这种情况下不需要运行任何类型的评估,编译器可以用立即的 () 值替换所有此类调用,除非存在明确的严格要求,在这种情况下,代码可能必须决定是否应该返回() 或底部。 我在 GHCi 中对此进行了实验,但似乎发生了相反的情况,即似乎对这样的函数进行了评估。一个非常原始的例子是

f :: a -> ()
f _ = undefined

由于undefined 的存在,评估f 1 会引发错误,因此肯定会发生一些评估。不过,目前尚不清楚评估的深度。有时它似乎需要评估所有对返回()的函数的调用。示例:

g :: [a] -> ()
g [] = ()
g (_:xs) = g xs

如果出现g (let x = 1:x in x),此代码将无限循环。但后来

f :: a -> ()
f _ = undefined
h :: a -> ()
h _ = ()

可用于表明h (f 1) 返回(),因此在这种情况下,并非所有单位值子表达式都被计算。这里的一般规则是什么?

ETA:我当然知道懒惰。我在问是什么阻止了编译器编写者使这种特殊情况比通常更懒惰。

ETA2:示例摘要:GHC 似乎将() 视为与任何其他类型完全一样,即,好像存在一个关于应该从函数返回哪个类型的常规值的问题。优化算法似乎没有(ab)使用只有一个这样的值这一事实。

ETA3:当我说 Haskell 时,我指的是 Haskell-as-defined-by-the-Report,而不是 Haskell-the-H-in-GHC。似乎是一个没有像我想象的那样广泛共享的假设(这是“100% 的读者”),或者我可能能够提出一个更清晰的问题。即便如此,我还是很遗憾更改了问题的标题,因为它最初询问了调用这样一个函数的保证

ETA4:看来这个问题已经解决了,我认为它没有答案。 (我一直在寻找一个“关闭问题”功能,但只找到了“回答你自己的问题”,由于无法回答,我没有走那条路。)没有人从报告中提出任何可以决定它的东西,我很想将其解释为一个强有力但不确定的“不能保证这种语言”的答案。我们所知道的是,当前的 GHC 实现不会跳过对此类函数的评估。

我在将 OCaml 应用程序移植到 Haskell 时遇到了实际问题。原始应用程序具有多种类型的相互递归结构,并且代码在 1..6 或 7 中为 N 声明了许多称为 assert_structureN_is_correct 的函数,如果结构确实正确,则每个函数都返回单位,如果它不是。此外,这些函数在分解正确性条件时相互调用。在 Haskell 中,使用 Either String monad 可以更好地处理这个问题,所以我以这种方式转录它,但作为理论问题的问题仍然存在。感谢所有输入和回复。

【问题讨论】:

这是工作上的懒惰。除非需要函数的结果(例如,通过对构造函数的模式匹配),否则不会评估函数的主体。要观察差异,请尝试比较 h1::()->() ; h1 () = ()h2::()->() ; h2 _ = ()。同时运行h1 (f 1)h2 (f 1),看到只有第一个需要(f 1) “懒惰似乎决定了它被 () 取代而没有发生任何类型的评估。”那是什么意思?在所有情况下,f 1 都被undefined“替换”。 函数 ... -> () 可以 1) 终止并返回 (),2) 以异常/运行时错误终止并且无法返回任何内容,或者 3) 发散(无限递归)。 GHC 不会优化代码,假设只有 1) 可能发生:如果需要 f 1,它不会跳过其评估并返回 ()。 Haskell 语义是评估它,看看 1,2,3 之间会发生什么。 这个问题中的()(类型或值)真的没有什么特别之处。如果您在任何地方将() :: () 替换为0 :: Int,所有相同的观察结果都会发生。这些都是懒惰评估的无聊旧后果。 不,“避免”等不是 Haskell 语义。并且()类型有两个可能的值,()undefined 【参考方案1】:

您似乎假设() 类型只有一个可能的值(),因此期望返回() 类型值的任何 函数调用应该自动假设确实产生了值()

这不是 Haskell 的工作方式。在 Haskell 中,每种类型都有一个值,即 no 值、错误或所谓的“底部”,由undefined 编码。因此,实际上正在进行评估:

main = print (f 1)

相当于核心语言的

main = _Case (f 1) _Of x -> print x   -- pseudocode illustration

甚至(*)

main = _Case (f 1) _Of x -> putStr "()"

Core 的_Caseforcing

“评估%case [表达式] 会强制评估正在测试的表达式(“审查员”)。审查员的值绑定到%of 关键字后面的变量,...”。

该值被强制为弱头正常形式。这是语言定义的一部分。

Haskell 不是一种声明性编程语言。


(*)print x = putStr (show x)show () = "()",因此可以完全编译掉 show 调用。

这个值确实是提前知道的(),甚至show ()的值也提前知道是"()"。仍然接受的 Haskell 语义要求 (f 1) 的值在继续打印预先知道的字符串 "()" 之前被强制为弱头部正常形式。


编辑:考虑concat (repeat [])。应该是[],还是无限循环?

“声明性语言”对此的回答很可能是[]。 Haskell 的答案是,无限循环

懒惰“穷人的声明式编程”,但它仍然不是真正的东西

edit2print $ h (f 1) == _Case (h (f 1)) _Of () -> print () 并且只有h 是强制的,而不是f;并且根据h _ = () 的定义,产生its 答案h 不必强制执行任何操作。

临别感言: 懒惰可能有存在的理由,但这不是它的定义。懒惰就是这样。它被定义为根据来自main 的要求,所有值最初都是强制转换为 WHNF 的值。如果它有助于根据其具体情况在某种特定情况下避免触底,那么它确实如此。如果不是,则不是。就这些。

您可以用您最喜欢的语言自己实现它,以感受一下。但是我们也可以仔细追踪任何表达式的求值,当它们出现时,naming all interim values。


通过the Report,我们有

f :: a -> ()
f = \_ -> (undefined :: ())

然后

print (f 1)
 = print ((\ _ ->  undefined :: ()) 1)
 = print          (undefined :: ())
 = putStrLn (show (undefined :: ()))

instance Show () where
    show :: () -> String
    show x = case x of () -> "()"

继续

 = putStrLn (case (undefined :: ()) of () -> "()")

现在,报告的3.17.3 Formal Semantics of Pattern Matching 部分说

case 表达式的语义[在图 3.1-3.3 中给出]。任何实现都应该表现得使这些身份保持[...]。

(r) 在Figure 3.2 状态下的情况

(r)     case ⊥ of  K x1 … xn -> e; _ -> e′  =  ⊥
        where K is a data constructor of arity n 

()是arity 0的数据构造函数,所以和

(r)     case ⊥ of  () -> e; _ -> e′  =  ⊥

因此评估的总体结果是

【讨论】:

@DanielWagner 实际上我想到了来自 Core 的 case,并忽略了这个大洞。 :) 我已经编辑提到核心。 强制不是在show 中由print 调用吗?类似show x = case x of () -> "()" @user253751 似乎是这样。我想我将print 视为给定的,其语义是“显示价值”。我的“核心翻译”只是一个例证。这种强制发生的具体地方,它发生。这就是我要说的。 我指的是 Core 中的 case,而不是 Haskell 本身。 Haskell 被翻译成Core,它有一个强制case,AFAIK。你是对的,Haskell 中的case 本身并没有强制。我可以用 Scheme 或 ML(如果我可以写 ML)或伪代码来写一些东西。 所有这些的权威答案可能在报告中的某个地方。我所知道的是这里没有进行“优化”,并且“常规值”在这种情况下不是一个有意义的术语。什么是强迫的,什么都是强迫的。 print 尽可能多地打印。它不看类型,类型消失了,被擦除了,当程序运行时,已经根据类型在编译时选择并编译了正确的打印子例程;该子例程仍将在运行时将其输入值强制为 WHNF,如果未定义,则会导致错误。

以上是关于在 Haskell 中评估函数 a -> () 有啥规则?的主要内容,如果未能解决你的问题,请参考以下文章

在 Haskell 中自动插入惰性

Haskell如何强制在haskell中评估Data.Map?

在Haskell中扩展数据类型

Haskell如何知道`xs`是函数定义中的列表?

是否有使用严格评估的 Haskell 编译器或预处理器?

pseq可以用seq来定义吗?