我正在阅读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 的东西像IO
andState
有一个返回值,以及执行副作用。有时操作的唯一目的是执行副作用,例如写入屏幕或存储某些状态。为了写入屏幕,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)
同样是Int
a 、 aChar
和 a的所有三元组的类型String
。
很容易看出如何继续向上扩展这种模式,但是向下呢?
(Int)
将是“1 元组”类型,由 的所有可能值组成Int
。但是这会被 Haskell 解析为只是把括号括起来Int
,因此只是类型Int
。这种类型的值将是(1)
, (2)
,(3)
等,它们也将被解析为Int
括号中的普通值。但如果你仔细想想,“1-tuple”与单个值完全相同,因此实际上没有必要让它们存在。
再往下走一步零元组给我们()
,它应该是一个空类型列表中所有可能的值组合。好吧,只有一种方法可以做到这一点,即不包含其他值,因此 type 中应该只有一个值()
。并且通过与元组值语法类比,我们可以将该值写为()
,它看起来肯定看起来像一个不包含值的元组。
这正是它的工作原理。没有魔法,这种类型()
及其值()
绝不会被语言特殊处理。
()
在 LYAH 书中的单子示例中,实际上并未被视为“任何类型的空值”。每当()
使用该类型时,唯一可以返回的值是()
. 所以它被用作一种类型来明确地说不能有任何其他返回值。同样,在应该返回另一种类型的地方,你不能return ()
。
要记住的是,当一堆 monad 计算与do
块或运算符(如>>=
,等)组合在一起时,它们将为某些 monad>>
构建一个类型的值。这种选择必须在整个组成部分中保持不变(没有办法以这种方式组合 a ),但在每个阶段 can 而且经常是不同的。m a
m
m
Maybe Int
IO Int
a
因此,当有人在计算过程IO ()
中插入 an时IO String
,这并不是()
在类型中使用 the 作为 null String
,它只是在构建 an的过程中使用an ,就像在构建 a 的过程中使用an一样。IO ()
IO String
Int
String
另一个角度:
()
是一个集合的名称,它包含一个名为 的元素()
。
在这种情况下,集合的名称和其中的元素恰好相同,这确实有点令人困惑。
请记住:在 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 个元素,除非你算作_|_
一个值。