这是一个曲折的答案!
正如您可能从评论中看到的,以及Thomas出色的(但非常技术性的)答案,您提出了一个非常棘手的问题。做得好!
我没有尝试解释技术性答案,而是试图向您提供Haskell在幕后所做的工作的广泛概述,而无需深入探讨技术细节。希望它可以帮助您大致了解正在发生的事情。
return
是类型推断的示例。
大多数现代语言都有一些多态性的概念。例如,var x = 1 + 1
将x
设置为等于2。在静态类型的语言中,2通常是int。如果您说var y = 1.0 + 1.0
,那么y
将是浮点数。运算符+
(这只是具有特殊语法的函数)
大多数命令式语言,尤其是面向对象的语言,只能以一种方式进行类型推断。每个变量都有固定的类型。当您调用一个函数时,它会检查参数的类型,然后选择适合该类型的函数的版本(或抱怨是否找不到一个)。
当您将函数的结果分配给变量时,变量已经具有类型,并且如果它与返回值的类型不一致,则会出现错误。
因此,在命令式语言中,类型推导的“流程”会随着程序中的时间而变化。推导变量的类型,对其进行处理,然后推导结果的类型。在动态类型化的语言(例如Python或javascript)中,直到计算出变量的值(这就是为什么似乎没有类型)后才分配变量的类型。在静态类型语言中,类型是提前(由编译器确定)的,但逻辑是相同的。编译器可以确定变量的类型,但是可以通过遵循程序逻辑来实现,就像运行程序一样。
在Haskell中,类型推断也遵循程序的逻辑。作为Haskell,它以一种非常数学上纯净的方式(称为系统F)这样做。类型的语言(即推导类型的规则)类似于Haskell本身。
现在记住Haskell是一种惰性语言。直到需要它时,它才能计算出任何东西的价值。这就是为什么在Haskell中具有无限的数据结构是有意义的。 Haskell从未想到数据结构是无限的,因为在需要之前它不会费心地解决它。
现在,所有这些懒惰魔术也发生在类型级别。就像Haskell直到真正需要时才算出表达式的值一样,Haskell直到真正需要时才算出表达式的类型。
考虑这个功能
func (x : y : rest) = (x,y) : func rest
func _ = []
如果您向Haskell询问此函数的类型,它将查看其定义,查看
[]
和
:
并推断出它正在使用列表。但是它不需要查看x和y的类型,它只是知道它们必须相同,因为它们最终位于同一列表中。因此,它将函数的类型推导为
[a] -> [a]
,其中a是尚未费力解决的类型。
到目前为止还没有魔术。但是,有必要注意此想法与以OO语言实现它之间的区别。 Haskell不会将参数转换为Object,先执行此操作,然后再转换回。只是没有明确询问Haskell列表的类型是什么。因此,它不在乎。
现在尝试在ghci中输入以下内容
maxBound - length ""
maxBound : "Hello"
现在发生了什么! minBound胸围为Char,因为我将其放在字符串的开头,并且必须为整数,因为我将其添加到0并得到了一个数字。加上这两个值明显不同。
那么minBound的类型是什么?让我们问ghci!
:type minBound
minBound :: Bounded a => a
啊!这意味着什么?基本上,这意味着您不必费心找出确切的
a
,但是如果您键入
Bounded
,则必须是
:info Bounded
,您会得到三行有用的提示
class Bounded a where
minBound :: a
maxBound :: a
还有很多不太有用的行
因此,如果
a
是
Bounded
,则存在类型
a
的值minBound和maxBound。
实际上
Bounded
只是一个值,它的“类型”是具有minBound和maxBound字段的记录。因为这是一个价值,Haskell直到真正需要它时才看待它。
因此,我似乎在您问题答案的区域中徘徊。在介绍
return
(您可能已经从注释中注意到它是一个非常复杂的野兽)之前,让我们看一下
read
。
ghci再次
read "42" + 7
read "'H'" : "ello"
length (read "[1,2,3]")
希望您不会惊讶地发现其中有定义
read :: Read a => String -> a
class Read where
read :: String -> a
所以
Read a
只是一个包含单个值的记录,该值是一个函数
String -> a
。非常有诱惑力的是,假设有一个读取函数可以查看字符串,计算出字符串中包含的类型并返回该类型。但这恰恰相反。在需要之前,它会完全忽略该字符串。当需要该值时,Haskell首先确定期望的类型,一旦完成,它将获取适当版本的read函数并将其与字符串组合。
现在考虑稍微复杂一点的东西
readList :: Read a => [String] -> a
readList strs = map read strs
实际情况下,readList实际上有两个参数
readList'(读取a)-> [String]-> [a]
readList'{read = f} strs = map f strs
再次,因为Haskell懒惰,它只在需要查找返回值时才打扰查看参数,这时它知道
a
是什么,因此编译器可以对正确的Read版本进行优化。在那之前,它不在乎。
希望这使您对正在发生的事情以及为何Haskell可以在返回类型上“重载”的方式有所了解。但是重要的是要记住,它不是传统意义上的过载。每个函数只有一个定义。只是其中一个参数是一袋函数。
read_str
永远不知道它正在处理什么类型。它只知道它获得了一个函数
String -> a
和一些Strings来执行应用程序,它只是将参数传递给
map
。
map
甚至不知道它获取字符串。当您深入了解Haskell时,使函数对所处理的类型的了解不多就变得非常重要。
现在让我们看看
return
。
记住我曾说过Haskell中的类型系统与Haskell本身非常相似。请记住,在Haskell函数中只是普通值。
这是否意味着我可以有一个将一个类型作为参数并返回另一个类型的类型?当然可以!
您已经看到一些类型函数
Maybe
接受了
a
类型,并返回了另一种类型,可以是
Just a
或
Nothing
。
[]
采用
a
类型,并返回
a
的列表。 Haskell中的类型函数通常是容器。例如,我可以定义一个类型函数
BinaryTree
,它将一个
a
的负载存储在类似树的结构中。当然有很多陌生人。
因此,如果这些类型函数与普通类型相似,那么我可以拥有一个包含类型函数的类型类。这样的类型类就是
Monad
class Monad m where
return a -> m a
(>>=) m a (a -> m b) -> m b
所以这里
m
是一些类型函数。如果要为
Monad
定义
m
,则需要定义
return
及其下面的吓人运算符(称为绑定(bind))
正如其他人指出的那样,对于相当无聊的函数,
return
是一个确实令人误解的名称。从那以后,设计Haskell的团队就意识到了自己的错误,对此我们深表歉意。
return
只是一个普通函数,它接受一个参数并返回其中带有该类型的
Monad
。 (您从未问过Monad实际上是什么,所以我不会告诉您)
让我们为
Monad
定义
m = Maybe
!
首先,我需要定义
return
。
return x
应该是什么?请记住,我只能定义一次函数,所以我看不到
x
,因为我不知道它是什么类型。我总是可以返回
Nothing
,但这似乎浪费了一个非常好的函数。让我们定义
return x = Just x
,因为这实际上是我唯一可以做的事情。
那可怕的 bundle 物呢?关于
x >>= f
我们能说些什么?好的
x
是某种未知类型的
Maybe a
a
,而
f
是一个需要
a
并返回
Maybe b
的函数。我需要以某种方式将这些结合起来以获得Mayb
所以我需要定义
Nothing >== f
。我无法调用
f
,因为它需要
a
类型的参数,而且我没有
a
类型的值,我什至不知道'a'是什么。我只有一个选择是定义
Nothing >== f = Nothing
那
Just x >>= f
呢?我知道
x
是
a
类型,而
f
以
a
作为参数,因此我可以设置
y = f a
并推断出
y
是
b
类型。现在我需要制作一个
Maybe b
,我已经有了一个
b
,所以...
刚x >> = f =刚(f x)
所以我有一个
Monad
!如果
m
是
List
怎么办?好吧,我可以遵循类似的逻辑并定义
return x = [x]
[] >>= f = []
(x : xs) >>= a = f x ++ (xs >>= f)
万岁另一个
Monad
!逐步进行操作并说服自己,没有其他明智的定义方法,这是一个不错的练习。
那么,当我调用
return 1
时会发生什么?
没有!
Haskell的懒惰人记得。笨拙的
return 1
(技术术语)就坐在那里,直到有人需要该值为止。 Haskell一旦需要该值,便知道该值应为哪种类型。特别是,可以推断出
m
是
List
。现在,它知道Haskell可以为
Monad
找到
List
的实例。一旦这样做,它就可以访问正确版本的return。
因此,最终Haskell准备好调用return了,在这种情况下将返回[1]!