haskell 为什么要检查()类型的值?

iyr7buue  于 2022-11-14  发布在  其他
关注(0)|答案(3)|浏览(135)

在Haskell中,()类型有两个值,即()和bottom。如果你有一个表达式e :: (),实际上检查它是没有意义的,因为它要么是e = (),要么通过检查它,你正在崩溃一个程序,否则它不会崩溃。
因此,我认为对()类型的值的操作可能不会检查该值,也不会区分()和bottom。
然而,这完全是不真实的:

▎λ ghci
GHCi, version 9.0.2: https://www.haskell.org/ghc/  :? for help
ghci> u = (undefined :: ())
ghci> show u
"*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:75:14 in base:GHC.Err
  undefined, called at <interactive>:1:6 in interactive:Ghci1
ghci> () == u
*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:75:14 in base:GHC.Err
  undefined, called at <interactive>:1:6 in interactive:Ghci1
ghci> f () = "ok"
ghci> f u
"*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/Err.hs:75:14 in base:GHC.Err
  undefined, called at <interactive>:1:6 in interactive:Ghci1

原因何在?以下是一些猜想:
1.出于某种我想不出的原因,在()上不懒惰是有用的。

  1. Haskell语义是以这样一种方式编写的,即解构 * 任何 * ADT,即使是微不足道的ADT,都会检查它们。
  2. ()是一个非常特殊的情况,在像Haskell这样的大型语言中,为了确保这一点额外的安全性,根本不值得关注
    1.还有一种可能的2+3的组合解释,即Haskell * 可能 * 具有语义,指示表达式case e of检查e *,除非 * 它是()类型,但这会污染语言规范,带来相对较低的好处
emeijp43

emeijp431#

我将解决这一部分:
由于某些我想不出的原因,在()上不懒惰是有用的。
让我们看一下Control.Parallel.Strategies(版本1,一个较旧的版本)。这是一个用于并行计算的模块。为了简单起见,让我们集中讨论它的一个函数:

parMap :: Strategy b -> (a -> b) -> [a] -> [b]

parMap strat f xs的结果与map f xs相同,只是列表是并行计算的。strat参数是什么?

strat :: Strategy b

手段

strat :: b -> ()

使用strat只能做两件事:

  • 调用它,忽略结果,懒惰等于不调用它;
  • 调用它并 * 强制 * 得到结果,即使您知道它是()或底部。

parMap并行执行后一个操作。这允许调用方指定一个strat参数,该参数根据需要 * 计算b * 类型的列表值。例如

parMap (\(x,y) -> ()) f xs
parMap (\(x,y) -> x `seq` ()) f xs
parMap (\(x,y) -> x `seq` y `seq` ()) f xs

是有效的调用,并且将导致parMap仅计算新的对列表,以分别公开对构造器、第一组件和第二组件。
因此,在这种情况下,强制strat()结果允许用户控制在parMap期间执行多少求值,即,强制该结果多少(并行),并且因此结果的哪些部分应该被留下不被评估。(相比之下,map f xs会使结果完全不求值--它完全是惰性的。parMap不能这样做,否则它就不再是并行的。)
小题外话:注意,GADT

data a :~: b where
    Refl :: t :~: t

有一个构造函数,如()。在这里,强制使用这样的值,如下所示:

foo :: Int :~: String -> Int -> String
foo Refl x = x ++ " hello"

这里的第一个参数必须是一个bottom。通过强制它,我们会使函数出错并产生一个异常。如果我们不强制它,我们会得到一个非常糟糕的未定义行为,就像C和C++中的那些行为一样,完全破坏了类型安全。Haskell会正确地拒绝任何绕过它的尝试:

foo :: Int :~: String -> Int -> String
foo _ x = x ++ " hello"

会在编译阶段触发型别错误。

bqf10yzr

bqf10yzr2#

我不确定,但我怀疑这不是你说的那些话,而是为了让你的语言具有可预测性和一致性。
从本质上讲,你观察到了两件事,我认为它们是分开的。第一件事是,用case语句检查x是否确实是(),强制对x求值;第二个是(ShowEq的)示例被写入以使用case语句。

  • 模式匹配:这里可预测的一致性规则是如果你写case <e0> of <pat> -> <e1>,那么e0的计算就足够检查pat中的构造函数是否真的在给定的位置。让我们假设e0的计算足够深入,足以检查pat是否真的匹配!对于()类型,这意味着模式()导致完全计算--因为您已经指定了预期的完整值--而模式x_可以不经过进一步的计算而匹配。
  • 类别执行严修:指定各种类示例所做的事情的自然归纳方法是总是有一个最外层的case,它与每个可用的构造函数匹配,并为字段提供简单的变量模式,然后依次对每个字段做一些事情(可能是递归调用)。也就是说,稍微简化一下,show的实现如下所示:
show x = case x of
    <Con0> field00 field01 field02 <...> -> "<Con0>"
        ++ " " ++ show field00
        ++ " " ++ show field01
        ++ " " ++ show field02
        ++ <...>
    <Con1> field10 field11 field12 <...> -> "<Con1>"
        ++ " " ++ show field10
        ++ " " ++ show field11
        ++ " " ++ show field12
        ++ <...>
    <...>

将此方案专门化为单构造函数、零字段类型()是很自然的:

show x = case x of
    () -> "()"

(此外,该报告指出,(==)在两个参数中始终是严格的;但该属性也会自然地从编写通用Eq示例派生算法的明显方式中产生。)因此,最不令人惊讶的方法是类示例在其参数上进行模式匹配。

hs1rzwqc

hs1rzwqc3#

第二条绝对正确。
()类型只是一个空的data类型,具有特殊的类型/数据构造函数语法:

data () = ()

因此,Haskell 2010年的报告虽然只提供了一个非正式语义,但在第 *3.17.2节模式匹配的非正式语义 * 中非常清楚地表明,表达式:

case undefined of () -> "ack!"

将根据第5条规则进行评估:
将模式con pat1 … patn与某个值进行匹配(其中con是由data定义的构造函数)取决于以下值:

  • 如果该值具有con v1 … vn的形式,则子模式相对于数据值的分量从左到右匹配;如果全部匹配成功,则整体匹配成功;第一个失败或发散分别导致整个匹配失败或发散。
  • 如果值的格式为con′ v1 … vm,其中concon′的不同建构函式,则比对会失败。
  • 如果值为⊥,则匹配发散。

这里,undefined的值是⊥,所以第三点适用,匹配发散。如果匹配发散,程序发散,如果程序发散,它必须以错误终止,或者--最坏的情况下--永远循环。它不能继续下去,好像什么都没有发生。诚然,最后一部分没有明确说明,但它是表达式的发散求值的语义的唯一合理解释。

相关问题