我正在阅读Learn You a Haskell,在 monad 章节中,在我看来,()每种类型都被视为一种“null”。当我检查()GHCi 中的类型时,我得到
>> :t ()
() :: ()
这是一个非常令人困惑的陈述。这似乎()是一种完全属于自己的类型。我对它如何融入语言以及它似乎能够代表任何类型感到困惑。
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,其值由值构造函数Leaf和Node. 我喜欢将类型构造函数 ( 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 或容器),它告诉您只有上下文是有趣的。
该()类型可以被认为是一个零元素元组。它是一种只能有一个值的类型,因此它用于需要类型但实际上不需要传达任何信息的地方。这里有几个用途。
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..."
我真的很喜欢()用元组类比来思考。
(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
另一个角度:
()是一个集合的名称,它包含一个名为 的元素()。
在这种情况下,集合的名称和其中的元素恰好相同,这确实有点令人困惑。
请记住:在 Haskell 中,类型是一个集合,其中包含其可能的值作为元素。
混淆来自其他编程语言:“void”在大多数命令式语言中意味着内存中没有存储值的结构。这似乎不一致,因为“布尔”有 2 个值而不是 2 位,而“空”没有位而不是没有值,但它是关于函数在实际意义上返回的内容。准确地说:它的单个值不消耗任何存储空间。
让我们暂时忽略值底部(书面_|_)......
()称为 Unit,写得像一个空元组。它只有一个值。并且它没有被调用
Void,因为Void它甚至没有任何值,因此不能被任何函数返回。
观察这一点:Bool有 2 个值(True和False),()有一个值(()),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 个元素,除非你算作_|_一个值。