首页 > 解决方案 > 解释 Haskell 中的柯里化实现

问题描述

我是一名学习 Haskell 的 Scala 程序员,通过“Haskell Programming from First Principles”来为我在 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

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

(第 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)

作为参考,Scala 等价物:

def curry[A,B,C](f: (A, B) => C): A => (B => C) = a => b => f(a, b)

显然,这不是在 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

标签: scalahaskell

解决方案


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

curry f a b = f (a, b)

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

因此,当前签名看起来像(滥用符号):

curry :: (type of f) -> a -> b -> (type of f (a, b))

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

curry :: ((a, b) -> c) -> a -> b -> c

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

curry :: ((a, b) -> c) -> (a -> b -> c)

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

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

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

uncurry :: (a -> b -> c) -> (a, b) -> c

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

uncurry :: (a -> b -> c) -> ((a, b) -> c)

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

λ> f = uncurry (+)
λ> :t f
f :: Num c => (c, c) -> c
λ> f (1, 2)
3
λ> (+) 1 2
3

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


推荐阅读