return语句在Haskell中是如何工作的?

时间:2018-01-09 22:52:31

标签: haskell typeclass

考虑这些功能

f1 :: Maybe Int
f1 = return 1

f2 :: [Int]
f2 = return 1

两者都有相同的陈述return 1。但结果却不同。 f1给出了值Just 1f2给出了值[1]

看起来Haskell根据返回类型调用两个不同版本的return。我想更多地了解这种函数调用。编程语言中是否有此功能的名称?

2 个答案:

答案 0 :(得分:2)

return函数来自Monad类:

class Applicative m => Monad (m :: * -> *) where
  ...
  return :: a -> m a

因此,return将获取a类型的任何值,并生成类型为m a的值。您观察到的monad m是使用Haskell类型类Monad进行ad hoc多态的多态。

此时你可能意识到return不是一个好的,直观的名字。它甚至不是内置函数或许多其他语言的语句。事实上,存在一个名称更好,操作相同的函数 - pure。在几乎所有情况下return = pure

也就是说,函数return与函数pure相同(来自Applicative类) - 我经常认为“这个monadic值纯粹是底层a”如果代码库中还没有约定,我会尝试使用pure而不是return。

您可以将return(或纯粹)用于Monad类的任何类型。这包括Maybe monad获取类型为Maybe a的值:

instance Monad Maybe where
...
    return      = pure  -- which is from Applicative

...
instance Applicative Maybe where
    pure = Just

或者列表monad获取值[a]

instance Applicative [] where
    {-# INLINE pure #-}
    pure x    = [x]

或者,作为一个更复杂的例子,A​​eson的解析monad获得类型为Parser a的值:

instance Applicative Parser where
    pure a = Parser $ \_path _kf ks -> ks a

答案 1 :(得分:1)

这是一个长期蜿蜒的答案!

正如你可能从评论和托马斯的优秀(但非常技术性)答案中看到的那样你已经提出了一个非常棘手的问题。做得好!

我没有尝试解释技术答案,而是试图向您概述Haskell在幕后所做的工作,而不是深入了解技术细节。希望它能帮助您全面了解正在发生的事情。

return类型推断的示例

大多数现代语言都有一些多态性的概念。例如var x = 1 + 1x设置为2.在静态类型语言中,2通常是int。如果你说var y = 1.0 + 1.0那么y将是一个浮点数。运算符+(这只是一个具有特殊语法的函数)

大多数命令式语言,尤其是面向对象语言,只能以一种方式进行类型推断。每个变量都有固定的类型。当你调用一个函数时,它会查看参数的类型,并选择适合这些类型的函数版本(或者如果它找不到那个就会抱怨)。

当您将函数的结果赋给变量时,变量已经具有类型,如果它不符合返回值的类型,则会出现错误。

因此,在一种命令式语言中," flow"类型推导遵循程序中的时间推断变量的类型,用它做一些事情并推导出结果的类型。在动态类型语言(例如Python或javascript)中,在计算变量的值之前不会分配变量的类型(这就是为什么似乎不是类型)。在静态类型语言中,类型是提前计算出来的(由编译器),但逻辑是相同的。编译器可以计算出变量的类型,但它是通过以与程序运行相同的方式遵循程序的逻辑来实现的。

在Haskell中,类型推断也遵循程序的逻辑。作为Haskell,它以非常数学上纯粹的方式(称为System 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 bust是一个Char,因为我把它放在一个字符串的前面,它必须是一个整数,因为我把它添加到0并得到一个数字。此外,这两个值显然非常不同。

那么minBound的类型是什么?我们问ghci!

:type minBound
minBound :: Bounded a => a

AAargh!那是什么意思?基本上,这意味着它并不打算完全解决a的问题,但如果您键入Bounded,则必须为:info Bounded,您会得到三条有用的行

class Bounded a where
  minBound :: a
  maxBound :: a

以及许多不太有用的行

因此,如果aBounded,则存在类型为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。假设有一个查看字符串的read函数,可以确定字符串中包含的类型并返回该类型,这非常诱人。但它恰恰相反。它完全忽略了字符串,直到需要它为止。当需要该值时,Haskell首先计算出它所期望的类型,一旦它完成它就会获得读取函数的适当版本并将其与字符串组合。

现在考虑一些稍微复杂的东西

readList :: Read a => [String] -> a
readList strs = map read strs
在引擎盖下,readList实际上有两个参数     readList' (阅读一篇) - > [String] - > [一个]     readList' {read = f} strs = map f strs

再一次,因为Haskell是懒惰的,只有当它需要找出返回值时才会查看参数,此时它知道a是什么,所以编译器可以去做正确的阅读版本。在那之前它并不关心。

希望能让您对发生的事情以及为什么Haskell可以超载"关于返回类型。但重要的是要记住它并不是传统意义上的超载。每个函数只有一个定义。只是其中一个论点是一系列功能。 read_str并不知道它正在处理什么类型。它只知道它获得了一个函数String -> a和一些字符串,为了执行应用程序它只是将参数传递给mapmap反过来甚至不知道它会变成字符串。当你深入了解Haskell时,函数对于他们所处理的类型并不是很了解就变得非常重要。

现在让我们看一下return

还记得我是怎么说Haskell中的类型系统与Haskell本身非常相似。请记住,在Haskell中,函数只是普通值。 这是否意味着我可以使用一个类型作为参数并返回另一种类型?当然可以!

您已经看到某些类型函数Maybe采用类型a并返回另一种类型,可以是Just aNothing[]采用类型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和它下面的可怕外观运算符(称为绑定)

正如其他人所指出的,return对于一个相当无聊的功能来说是一个非常具有误导性的名称。设计Haskell的团队已经意识到他们的错误,他们真的很抱歉。 return只是一个普通的函数,它接受一个参数并返回一个Monad的类型。 (你从未问过Monad究竟是什么,所以我不会告诉你)

让我们为Monad定义m = Maybe! 首先,我需要定义returnreturn x应该是什么?请记住,我只允许定义一次该功能,因此我无法查看x,因为我不知道它是什么类型。我总是可以返回Nothing,但这似乎浪费了一个非常好的功能。让我们定义return x = Just x,因为这实际上是我能做的唯一其他事情。

可怕的绑定事情怎么样?关于x >>= f,我们可以说什么?好x是某种未知类型的Maybe a a,而f是一个接受a并返回Maybe b的函数。不知何故,我需要结合这些来获得一个Maybe b`

所以我需要定义Nothing >== f。我无法调用f,因为它需要a类型的参数,而且我没有a类型的值我甚至不知道是什么'一个'是。我只有一个选择,即定义

Nothing >== f = Nothing

Just x >>= f怎么样?我知道x的类型为af的格式为a,因此我可以设置y = f a并推断yb类型为Maybe b。现在我需要制作b并且我已经Monad所以...

只是x>> = f = Just(f x)

所以我有m!如果Listreturn x = [x] [] >>= f = [] (x : xs) >>= a = f x ++ (xs >>= f) ,该怎么办?好吧,我可以遵循类似的逻辑并定义

Monad

万岁另一个return 1!这是一个很好的练习,可以通过这些步骤来说服自己没有其他明智的方法来定义它。

那么当我致电return 1时会发生什么?

没有!

哈斯克尔的懒惰记得。 thunk m(技术术语)就在那里,直到有人需要这个值。一旦Haskell需要该值,它就知道该值应该是什么类型。特别是它可以推断ListMonad。现在它知道Haskell可以为List找到{{1}}的实例。一旦它这样做,它就可以访问正确的返回版本。

所以最后Haskell准备好调用return,在这种情况下返回[1]!