78

我正在阅读Learn You a Haskell,在 monad 章节中,在我看来,()每种类型都被视为一种“null”。当我检查()GHCi 中的类型时,我得到

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

这是一个非常令人困惑的陈述。这似乎()是一种完全属于自己的类型。我对它如何融入语言以及它似乎能够代表任何类型感到困惑。

4

6 回答 6

157

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为在节点处存储元素的二叉树类型声明一个类型构造函数a,其值由值构造函数LeafNode. 我喜欢将类型构造函数 ( Tree) 着色为蓝色,将值构造函数 ( Leaf, Node) 着色为红色。表达式中不应有蓝色,并且(除非您使用高级功能)类型中不应有红色。Bool可以声明内置类型,

data Bool = True | False

在类型语言中添加蓝色,在表达式语言中添加Bool红色True和。False可悲的是,我的 markdown-fu 不足以完成为这篇文章添加颜色的任务,所以你只需要学会在头脑中添加颜色。

“单位”类型()用作特殊符号,但它的工作方式如同声明

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

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

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

添加(,)到类型和表达式语言。但我离题了。]

因此 type (),通常发音为“Unit”,是一种包含一个值得一提的值的类型:该值也是用表达式()语言编写的,有时发音为“void”。只有一个值的类型不是很有趣。类型的值提供零位信息:您已经知道它必须是什么。因此,虽然类型没有什么特别的地方来表示副作用,但它通常显示为一元类型中的值组件。一元操作往往具有看起来像()()

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

其中返回类型是一个类型应用程序: (type) 函数告诉您哪些效果是可能的, (type) 参数告诉您操作会产生什么样的值。例如

put :: s -> State s ()

这被读取(因为应用程序关联到左侧 [“正如我们在 60 年代所做的那样”,Roger Hindley])

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

有一个 value input type s, effect-monadState s和 value output type ()。当您将()其视为值输出类型时,这仅意味着“此操作仅用于其效果;传递的值是无趣的”。相似地

putStr :: String -> IO ()

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

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

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

于 2013-06-03T09:44:02.170 回答
34

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

Monadic 的东西像IOandState有一个返回值,以及执行副作用。有时操作的唯一目的是执行副作用,例如写入屏幕或存储某些状态。为了写入屏幕,putStrLn必须有类型String -> IO ?——IO总是必须有一些返回类型,但是这里没有什么有用的返回。那么我们应该返回什么类型呢?我们可以说 Int,并且总是返回 0,但这是有误导性的。所以我们 return (),只有一个值(因此没有有用的信息)的类型,以表明没有任何有用的返回。

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

需要注意的()是,并不是特别神奇。如果你愿意,你可以将它存储在一个变量中并对其进行模式匹配(尽管没有多大意义):

main = do
  x <- putStrLn "Hello"
  case x of
    () -> putStrLn "The only value..."
于 2013-06-03T08:47:07.517 回答
14

它被称为Unit类型,通常用来表示副作用。你可以模糊地认为它就像Void在 Java 中一样。阅读更多herehere等。可能令人困惑的是,它在()语法上表示类型及其唯一的值文字。另请注意,它与nullJava 中的不同,这意味着未定义的引用 -()实际上只是一个 0 大小的元组。

于 2013-06-03T08:24:29.010 回答
11

我真的很喜欢()用元组类比来思考。

(Int, Char)Int是 an和 a的所有对的类型Char,所以它的值是所有可能的值Int与所有可能的值交叉Char(Int, Char, String)同样是Inta 、 aChar和 a的所有三元组的类型String

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

(Int)将是“1 元组”类型,由 的所有可能值组成Int。但是这会被 Haskell 解析为只是把括号括起来Int,因此只是类型Int。这种类型的值将是(1), (2),(3)等,它们也将被解析为Int括号中的普通值。但如果你仔细想想,“1-tuple”与单个值完全相同,因此实际上没有必要让它们存在。

再往下走一步零元组给我们(),它应该是一个空类型列表中所有可能的值组合。好吧,只有一种方法可以做到这一点,即不包含其他值,因此 type 中应该只有一个值()。并且通过与元组值语法类比,我们可以将该值写为(),它看起来肯定看起来像一个不包含值的元组。

这正是它的工作原理。没有魔法,这种类型()及其值()绝不会被语言特殊处理。

()在 LYAH 书中的单子示例中,实际上并未被视为“任何类型的空值”。每当()使用该类型时,唯一可以返回的值是(). 所以它被用作一种类型来明确地不能有任何其他返回值。同样,在应该返回另一种类型的地方,你不能return ()

要记住的是,当一堆 monad 计算与do块或运算符(如>>=,等)组合在一起时,它们将为某些 monad>>构建一个类型的值。这种选择必须在整个组成部分中保持不变(没有办法以这种方式组合 a ),但在每个阶段 can 而且经常是不同的。m ammMaybe IntIO Inta

因此,当有人在计算过程IO ()中插入 an时IO String,这并不是()在类型中使用 the 作为 null String,它只是在构建 an的过程中使用an ,就像在构建 a 的过程中使用an一样。IO ()IO StringIntString

于 2013-06-04T03:24:57.503 回答
7

另一个角度:

()是一个集合的名称,它包含一个名为 的元素()

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

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

于 2014-04-18T14:50:14.893 回答
6

混淆来自其他编程语言:“void”在大多数命令式语言中意味着内存中没有存储值的结构。这似乎不一致,因为“布尔”有 2 个值而不是 2 位,而“空”没有位而不是没有值,但它是关于函数在实际意义上返回的内容。准确地说:它的单个值不消耗任何存储空间。

让我们暂时忽略值底部(书面_|_)......

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


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

如果你想给“那个值”一个名字,一个永远不会返回的函数返回(是的,这听起来像疯话),然后把它称为底部(“ _|_”,写成倒转的 T)。它可能表示异常或无限循环或死锁或“等待更长时间”。(某些函数只会返回底部,如果它们的参数之一是底部。)

当您创建笛卡尔积/这些类型的元组时,您将观察到相同的行为: (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 个元素,除非你算作_|_一个值。

于 2013-06-18T16:25:26.963 回答