解释 Haskell 中的柯里化实现

Maa*_*mon 9 haskell scala

我是一名 Scala 程序员,正在通过“从第一原则开始的 Haskell 编程”学习 Haskell,以使我在 Scala 中使用的函数概念更加一致。

我非常理解柯里化的概念。在 Scala 中很好地实现了它。

但是我不明白作者提供的currying和uncurry的实现:

curry f a b = f (a, b) 
:t curry 
-- curry :: ((t1, t2) -> t) -> t1 -> t2 -> t 

:t fst 
-- fst :: (a, b) -> a 
:t curry fst 
-- curry fst :: t -> b -> t

Run Code Online (Sandbox Code Playgroud)
uncurry f (a, b) = f a b 
:t uncurry 
-- uncurry :: (t1 -> t2 -> t) -> (t1, t2) -> t  
Run Code Online (Sandbox Code Playgroud)

(第 134 页)。

这里发生了什么?

在第一种情况下,是不是因为应用f (a, b),我们推断 f 实际上是,((t1, t2) -> t)但究竟是什么把它变成了t1 -> t2 -> t

我不明白推论和整个咖喱/非咖喱过程。

编辑 1

从到目前为止给出的所有回复中,我现在确实明白了。我想我现在正在努力让 Haskell 变得聪明。

以咖喱为例:

如果我在 Scala 和 Haskell 中编写与如何解决该问题相同的代码:

myCurry :: ((a,b) -> c) -> a ->b -> c 
myCurry f = \a -> \b -> f(a,b)
Run Code Online (Sandbox Code Playgroud)

作为参考,Scala 等价物:

def curry[A,B,C](f: (A, B) => C): A => (B => C) = a => b => f(a, b)
Run Code Online (Sandbox Code Playgroud)

显然,这不是 Haskell 中解决该问题的惯用方法。我对从 Haskell 的角度来看该解决方案有什么问题很感兴趣,所以我可以开始更好地了解是什么让 Haskell 类型系统如此受人尊敬。

到目前为止,我看到的唯一区别是惯用的 Haskell 解决方案有更多的推理,并且依赖于 curry 的部分应用。Scala 解决方案需要更明确的手册。

不确定通过一些心理体操来理解如此简单的事情是否有用。但为什么不呢。

编辑 2

实际上找到了在 Scala 中做同样的事情的方法,尽管一直减去泛型类型。

def fst[A, B](a: A, b:B): A = a
//fst: fst[A,B](val a: A,val b: B) => A

fst[Int, Int] _
//res0: (Int, Int) => Int

def sCurry[A,B,C](f: (A, B) => C) (a: A) (b: B) = f(a,b)
//sCurry: sCurry[A,B,C](val f: (A, B) => C)(val a: A)(val b: B) => C

sCurry[Int, Int, Int] _
//((Int, Int) => Int) => (Int => (Int => Int))

val s = sCurry(fst[Int, Int]) _
//s: Int => (Int => Int)

s (4) (2)
//res1: Int = 4
Run Code Online (Sandbox Code Playgroud)

Apl*_*123 10

我们可以一次启动一个变量:

curry f a b = f (a, b)
Run Code Online (Sandbox Code Playgroud)

aandb是不受限制的值(除了作为函数的参数f),这意味着我们可以给它们类型,比如说ab(非常有创意的命名,我知道)。

所以,当前的签名看起来像(滥用符号):

curry :: (type of f) -> a -> b -> (type of f (a, b))
Run Code Online (Sandbox Code Playgroud)

由于在右侧f调用(a, b),我们可以假设它是一个函数,它接受一个元组(a, b)并返回一些值,比方说c。所以,f(a, b) -> c,所以f (a, b)是应用一个级别,或者只是c。所以,函数的签名是:

curry :: ((a, b) -> c) -> a -> b -> c
Run Code Online (Sandbox Code Playgroud)

现在,我们可以尝试理解这个签名。我发现当括号作为这个等效表达式时更容易理解:

curry :: ((a, b) -> c) -> (a -> b -> c)
Run Code Online (Sandbox Code Playgroud)

您传入一个函数,该函数接受一个元组并返回一个值,并curry返回一个接受两个值并返回一个值的函数。从本质上讲,您正在解包参数:从一个接受包含 2 个值的单个元组的函数到一个接受 2 个值的函数。

在 的上下文中看这个curry fstfst\(a, b) -> a。因此,curry将解压缩参数,使其\a b -> a. 这基本上是 的定义const,返回第一个参数并忽略第二个参数。

现在我们可以看看 uncurry。使用与之前相同的推理,我们可以说参数a具有 type a,并且参数b具有 type b,并且由于f调用athen b,因此它必须具有某种类型a -> b -> c。所以,类型签名看起来像:

uncurry :: (a -> b -> c) -> (a, b) -> c
Run Code Online (Sandbox Code Playgroud)

或者,像以前一样用括号括起来:

uncurry :: (a -> b -> c) -> ((a, b) -> c)
Run Code Online (Sandbox Code Playgroud)

顾名思义,这与咖喱相反。它接受一个已经接受 2 个参数的函数,然后返回一个接受带有两个参数的元组的函数。看着uncurry一个用例:

?> f = uncurry (+)
?> :t f
f :: Num c => (c, c) -> c
?> f (1, 2)
3
?> (+) 1 2
3
Run Code Online (Sandbox Code Playgroud)

此外,对于接受两个或更多参数的任何参数,curry (uncurry f)应该是。ff