究竟Haskell中的()是什么?


72

我正在阅读Learn Has a Haskell,在monad章节中,对我来说,()每种类型都将其视为一种“空值”。当我检查()GHCi的类型时,我得到

>> :t ()
() :: ()

这是一个非常令人困惑的声明。看来这()本身就是一种类型。我对它如何适合该语言以及它似乎能够代表任何类型感到困惑。


3
这是Unit类型。它只有一个值。。不返回任何有趣值的动作。
本杰明·格林鲍姆

2
具有恰好一个非底值的类型。
Daniel Fischer 2013年

1
什么是底值?
MYV 2013年

1
@Maksim:底值是属于所有类型的值。它通常表示没有其他值返回(函数终止,异常等)
Maciej Piechotka 2013年

1
奇怪的是,这里没有答案提到范畴论……
Bakuriu

Answers:


144

tl; dr ()不会为每种类型添加“ null”值,地狱号;()是其自身类型中的“暗淡”值:()

让我从问题中退后一步,解决常见的混乱根源。学习Haskell时要吸收的关键是它的表达语言和类型语言之间的区别。您可能知道这两者是分开的。但这允许在两者中使用相同的符号,这就是这里发生的情况。有简单的文字提示可以告诉您您正在使用哪种语言。您无需解析整个语言即可检测到这些提示。

默认情况下,Haskell模块的顶层使用表达式语言。您可以通过在表达式之间编写方程式来定义函数。但是,当您在表达式语言中看到foo :: bar时,表示foo是一个表达式,而bar是其类型。因此,当您阅读时() :: (),会看到一条语句,该语句()将表达式语言中的与()类型语言中的相关联。这两个()符号含义不同,因为它们的语言不同。这种重复通常会使初学者感到困惑,直到表达/类型语言的分离安装在他们的潜意识中,这时它便成为了助记符。

关键字data引入了一个新的数据类型声明,涉及表达式和类型语言的仔细混合,因为它首先说明了新类型是什么,其次是其值。

数据TyCon tyvar ... tyvar = ValCon1类型...类型| ... | ValConn类型...类型

在这样的声明中,类型构造函数TyCon被添加到类型语言,而ValCon值构造函数被添加到表达语言(及其模式子语言)。在data声明中,ValCon的参数位置中的内容告诉您在表达式中使用ValCon时为参数提供的类型。例如,

data Tree a = Leaf | Node (Tree a) a (Tree a)

Tree为在节点上存储元素的二叉树类型声明一个类型构造函数,其值由值构造函数Leaf和给出Node。我喜欢将类型构造函数(树)设为蓝色,将值构造函数(叶节点)设为红色。表达式中不应有蓝色,并且(除非您正在使用高级功能)类型中不应有红色。Bool可以声明内置类型,

data Bool = True | False

加入蓝色Bool的类型语言,和红色True以及False到表达式语言。可悲的是,我的markdown-fu不足以为帖子添加颜色,因此您只需要学习在脑海中添加颜色即可。

“单位”类型()用作特殊符号,但其工作方式类似于声明

data () = ()  -- the left () is blue; the right () is red

意思是,蓝色()在类型语言中是类型构造函数,但是红色()在表达式语言中实际上是值构造函数() :: ()。[这不是这种双关语的唯一例子。大元组的类型遵循相同的模式:配对语法就像由

data (a, b) = (a, b)

在类型和表达式语言中都添加(,)。但是我离题了。

因此(),通常称为“ Unit”的类型是一种包含一个值得说的值的类型:该值是用()表达式语言编写的,有时称为“ void”。只有一个值的类型不是很有趣。类型值()贡献零信息位:您已经知道它必须是什么。因此,尽管没有什么特别的类型()可以指示副作用,但它通常以单调类型显示为值成分。Monadic运算的类型通常看起来像

val-in-type-1- > ...-> val-in-type-n- > effect-monad val-out-type

返回类型是类型应用程序:函数告诉您可能产生的效果,参数告诉您操作产生哪种值。例如

put :: s -> State s ()

读为(因为应用程序关联到左侧[“就像我们六十年代一样,” Roger Hindley])为

put :: s -> (State s) ()

具有一个值输入类型s,effect-monadState s和值输出类型()。当您将其()视为值输出类型时,仅表示“此操作仅出于其效果;所传递的值没有意义”。相似地

putStr :: String -> IO ()

将字符串传递给stdout但不返回任何令人兴奋的信息。

()类型还可用作容器状结构的元素类型,它表示数据仅由shape组成,没有有趣的有效负载。例如,如果Tree如上所述声明,Tree ()则为二叉树形状的类型,在节点上不存储任何感兴趣的内容。同样[()],沉闷元素列表的类型也是如此,如果列表元素中没有什么令人感兴趣的内容,那么它贡献的唯一信息就是它的长度。

概括起来,()是一种类型。它的一个值()恰好具有相同的名称,但这没关系,因为类型和表达式语言是分开的。具有表示“无信息”的类型很有用,因为在上下文中(例如,单子或容器),它告诉您只有上下文才是有趣的。


4
可悲的是,除了默认的语法突出显示之外,没有其他方法可以使用颜色。如果使用HTML,则可以使用斜体和粗体(例如)<pre>data <b>Bool</b> = <i>True</i> | <i>False</i></pre>,但是鉴于SO降价的局限性,我担心这是最好的选择。另一种选择是使用图像。
hammar

12
用彩色图像替换单色声明是很诱人的,但是我的长期目标是说服人们即使在没有颜色的情况下也能感知颜色。
猪工

2
人们实际上称红色为()“无效”吗?这似乎是错误的,因为“空缺”对我来说没有任何价值。
Xeo

3
有些人这样做,尤其是像我一样,如果他们在Haskell之前学过ML。您对连接的直觉在我看来似乎很合理,但是您应该向C员工讲解!如果Void是一种类型(请参见Data.Void),我们可能希望它具有零值(这就是为什么我建议时将其称为零)。但是,void应类似地是不包含任何信息的值。就是这种情况。集合理论家也有类似的情况,其中空集合也是单元集合中唯一可能的元素(因此变钝)。
养猪工人

1
随机偶然发现这一点,并发现这一点非常清楚,谢谢!
agam

33

()类型可以认为是零元素元组。它是一个只能具有一个值的类型,因此可以在需要具有类型的地方使用,但实际上不需要传达任何信息。这有两个用途。

单子事物喜欢IOState具有返回值,并且会产生副作用。有时,操作的唯一目的是执行副作用,例如写入屏幕或存储某些状态。要写入屏幕,putStrLn必须具有类型String -> IO ?-IO始终必须具有一些返回类型,但是这里没有有用的返回值。那么我们应该返回什么类型呢?我们可以说Int,并且总是返回0,但这是一种误导。因此,我们返回(),该类型只有一个值(因此没有有用的信息),以指示没有有用的返回。

有时可能会有一个没有有用值的类型,这很有用。考虑是否实现了将type的Map k v键映射到typek的值的type v。然后,您想要实现一个Set与映射非常相似的,只是不需要值部分,而只需要键。在Java之类的语言中,您可能会使用布尔值作为伪值类型,但实际上您只想要一个没有有用值的类型。所以你可以说type Set k = Map k ()

应当指出,这()并不是特别神奇。如果需要,可以将其存储在变量中并对其进行模式匹配(尽管没有太多意义):

main = do
  x <- putStrLn "Hello"
  case x of
    () -> putStrLn "The only value..."

13

它称为Unit类型,通常用于表示副作用。您可以像Void在Java中那样模糊地考虑它。在此处此处了解更多内容。令人困惑的是,在()语法上既表示类型又表示其唯一值文字。还要注意,它与nullJava中的相似,这意味着未定义的引用-()实际上只是一个0大小的元组。


4
我应该补充一点,它不仅是Unit类型,而且是表示该类型唯一值的文字。
kirelagin

11

我真的很想()比喻为元组。

(Int, Char)是anInt和a的所有对的类型Char,因此它的值是Int与的所有可能值相交的所有可能值Char(Int, Char, String)同样是an Int,aChar和a的所有三元组的类型String

很容易看出如何保持这种模式向上扩展,但是向下如何呢?

(Int)将是“ 1-元组”类型,由的所有可能值组成Int。但这可以被Haskell解析为只是Int加上括号,从而成为类型Int。而在这种类型的值是(1)(2)(3)等,这也将只得到解析为普通的Int括号内的数值。但是,如果考虑一下,“ 1-元组”与单个值完全相同,因此实际上不需要存在它们。

再往下走到零元组,我们得到(),这应该是空类型列表中所有可能的值组合。嗯,确实有一种方法可以做到这一点,即不包含其他值,因此type中应该只有一个值()。并且类似于元组值语法,我们可以将该值写为(),这肯定看起来像一个不包含任何值的元组。

这就是它的工作原理。没有魔力,这种语言()及其类型()也不会被特殊对待。

()在LYAH书中的monads示例中,实际上不被视为“任何类型的null值”。每当使用该类型时()唯一可以返回的值是()。因此,它用作明确表示不能有其他返回值的类型。同样,在应该返回其他类型的地方,也不能返回()

要记住的事情是,当一元计算的一堆与组合在一起do的块或类似的经营者>>=>>等等,他们将建立一个类型的值,m a对于一些单子m。的这一选择m已经留在整个零件(有没有办法组成一个同样Maybe Int具有IO Int以这种方式),但a可以而且往往是在每个阶段不同。

因此,当有人坚持的IO ()在中间IO String计算,这是不使用()作为一个空String类型,它只是使用IO ()道路上的建设IO String,以同样的方式,你可以使用一个Int的方式来构建String


6

另一个角度:

()是包含单个元素的集合的名称()

在这种情况下,集合的名称和其中的元素恰好是相同的,这确实有些令人困惑。

请记住:在Haskell中,类型是一个集合,它具有可能的值作为元素。


6

混乱来自其他编程语言:“无效”在大多数命令式语言中表示内存中没有存储值的结构。这似乎不一致,因为“布尔”有2个值,而不是2位,而“作废”没有位的,而不是没有价值,但它是关于什么在实际意义上的函数返回。确切地说:它的单个值不占用任何存储空间。

让我们_|_暂时忽略值bottom(写成)...

()称为单元,写为空元组。它只有一个值。而且它没有被调用 Void,因为Void甚至没有任何值,因此不能由任何函数返回。


请注意:Bool具有2个值(TrueFalse),()具有一个值(()),并且Void没有值(不存在)。它们就像带有两个/一个/没有元素的集合。他们需要存储的最少内存是1位/无位/不可能。这意味着返回a的函数()可能返回的结果值(显而易见的结果)可能对您没有用。Void另一方面,这意味着该函数将永远不会返回,也不会给您任何结果,因为将不存在任何结果。

如果您想给“那个值”起一个函数返回的名称,而该函数永不返回(是的,这听起来像是疯话),然后将其命名为“底部”(“ _|_”,就像倒T一样)。它可能表示异常,无限循环,死锁或“等待更长的时间”。(如果某些函数的参数之一是bottom,则某些函数只会返回bottom。)

创建笛卡尔乘积/这些类型的元组时,将观察到相同的行为: (Bool,Bool,Bool,(),())具有2·2·2·1·1 = 6个不同的值。(Bool,Bool,Bool,(),Void)就像集合{t,f}×{t,f}×{t,f}×{u}×{}的集合一样,它具有2·2·2·1·0 = 0个元素,除非您算作_|_一个值。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.