我在JavaScript中编写了一个简单的curry
函数,它在大多数情况下都能正常工作:
const add = curry((a, b, c) => a + b + c);
const add2 = add(2);
const add5 = add2(3);
console.log(add5(5));
<script>
const curried = Symbol("curried");
Object.defineProperty(curry, curried, { value: true });
function curry(functor, ...initArgs) {
if (arguments.length === 0) return curry;
if (typeof functor !== "function") {
const value = JSON.stringify(functor);
throw new TypeError(`${value} is not a function`);
}
if (functor[curried] || initArgs.length >= functor.length)
return functor(...initArgs);
const result = (...restArgs) => curry(functor, ...initArgs, ...restArgs);
return Object.defineProperty(result, curried, { value: true });
}
</script>
但是,它不适用于以下情况:
// length :: [a] -> Number
const length = a => a.length;
// filter :: (a -> Bool) -> [a] -> [a]
const filter = curry((f, a) => a.filter(f));
// compose :: (b -> c) -> (a -> b) -> a -> c
const compose = curry((f, g, x) => f(g(x)));
// countWhere :: (a -> Bool) -> [a] -> Number
const countWhere = compose(compose(length), filter);
根据以下问题,countWhere
被定义为(length .) . filter
:
What does (f .) . g mean in Haskell?
因此我应该能够使用countWhere
如下:
const odd = n => n % 2 === 1;
countWhere(odd, [1,2,3,4,5]);
但是,它不返回3
(数组[1,3,5]
的长度),而是返回一个函数。我究竟做错了什么?
@审计,
我发布这个是因为你对我对To “combine” functions in javascript in a functional way?的回答发表评论我没有特别报道该帖子中的currying,因为这是一个非常有争议的话题,而不是我想在那里打开的一堆蠕虫。
当你似乎在你的实现中添加你自己的糖和便利时,我会谨慎使用“如何正确咖喱”的措辞。
无论如何,除此之外,我真的不打算将其作为议论/好斗的帖子。我希望能够就JavaScript的currying进行公开,友好的讨论,同时强调我们的方法之间的一些差异。
无需再费周折...
澄清:
鉴于f
是一个函数,f.length
是n
。让curry(f)
成为g
。我们用g
论证称m
。应该怎么办?你说:
- 如果
m === 0
然后返回g
。- 如果
m < n
然后部分地将f
应用于m
新参数,并返回一个新的curried函数接受剩余的n - m
参数。- 如果
m === n
然后将f
应用于m
论点。如果结果是一个函数,那么咖喱结果。最后,返回结果。- 如果
m > n
然后将f
应用于第一个n
论点。如果结果是一个函数,那么咖喱结果。最后,将结果应用于剩余的m - n
参数并返回新结果。
让我们看一下@Aadit M Shah代码实际上做的代码示例
var add = curry(function(x, y) {
return function(a, b) {
return x + y + a + b;
}
});
var z = add(1, 2, 3);
console.log(z(4)); // 10
这里发生了两件事:
我不相信这里有很多辩论空间,但人们似乎错过了currying实际上是什么
via:维基百科 在数学和计算机科学中,currying是一种翻译对函数的评估的技术,该函数将多个参数(或参数元组)转换为评估一系列函数,每个函数都有一个参数...
我最后一点粗暴,因为它非常重要;序列中的每个函数只接受一个参数;不像你建议的变量(0,1或更多)参数。
你也在帖子中提到了haskell,所以我假设你知道Haskell没有带有多个参数的函数。 (注意:一个带元组的函数仍然只是一个带有一个参数,一个元组的函数)。其原因是深刻的,并为您提供了具有可变参数的函数无法提供给您的表现力的灵活性。
那么让我们再问一下这个原始问题:应该怎么办?
好吧,当每个函数只接受1个参数时,这很简单。在任何时候,如果给出超过1个参数,它们就会被丢弃。
function id(x) {
return x;
}
当我们打电话给id(1,2,3,4)
时会发生什么?当然,我们只得到1
回来,2,3,4
完全被忽视。这是:
curry
解决方案在我们走得更远之前,我将使用ES6风格的arrow functions,但我也将在这篇文章的底部包含ES5等价物。 (可能晚些时候今晚。)
在naomik技术currying
在这种方法中,我们编写一个curry函数,它不断返回单参数函数,直到指定了所有参数
由于这个实现,我们有6个多功能功能。
// no nonsense curry
const curry = f => {
const aux = (n, xs) =>
n === 0 ? f (...xs) : x => aux (n - 1, [...xs, x])
return aux (f.length, [])
}
// demo
let sum3 = curry(function(x,y,z) {
return x + y + z;
});
console.log (sum3 (3) (5) (-1)); // 7
好的,所以我们已经看到了使用简单的辅助循环实现的curry
技术。它没有依赖关系和一个低于5行代码的声明性定义。它允许部分应用函数,一次一个参数,就像一个curried函数应该工作一样。
没有魔法,没有不可预见的自动调整,没有其他不可预见的后果。
但无论如何,究竟是什么意思呢?
好吧,事实证明,我并没有真正编写的curry
函数。正如您在下面看到的,我通常以curry形式定义所有可重用的函数。所以,当你想要与一些你无法控制的函数接口时,你只需要curry
,也许来自lib或者其他东西;其中一些可能有可变接口!
我介绍curryN
// the more versatile, curryN
const curryN = n => f => {
const aux = (n, xs) =>
n === 0 ? f (...xs) : x => aux (n - 1, [...xs, x])
return aux (n, [])
};
// curry derived from curryN
const curry = f => curryN (f.length) (f);
// some caveman function
let sumN = function() {
return [].slice.call(arguments).reduce(function(a, b) {
return a + b;
});
};
// curry a fixed number of arguments
let g = curryN (5) (sumN);
console.log (g (1) (2) (3) (4) (5)); // 15
咖喱或不咖喱?就是那个问题
我们将编写一些示例,其中我们的函数都是curry形式。功能将非常简单。每个都带有1
参数,每个参数都有一个返回表达式。
// composing two functions
const comp = f => g => x => f (g (x))
const mod = y => x => x % y
const eq = y => x => x === y
const odd = comp (eq (1)) (mod (2))
console.log (odd(1)) // true
console.log (odd(2)) // false
你的countWhere
功能
// comp :: (b -> c) -> (a -> b) -> (a -> c)
const comp = f => g => x =>
f(g(x))
// mod :: Int -> Int -> Int
const mod = x => y =>
y % x
// type Comparable = Number | String
// eq :: Comparable -> Comparable -> Boolean
const eq = x => y =>
y === x
// odd :: Int -> Boolean
const odd =
comp (eq(1)) (mod(2))
// reduce :: (b -> a -> b) -> b -> ([a]) -> b
const reduce = f => y => ([x,...xs]) =>
x === undefined ? y : reduce (f) (f(y)(x)) (xs)
// filter :: (a -> Boolean) -> [a] -> [a]
const filter = f =>
reduce (acc => x => f (x) ? [...acc,x] : acc) ([])
// length :: [a] -> Int
const length = x =>
x.length
// countWhere :: (a -> Boolean) -> [a] -> Int
const countWhere = f =>
comp (length) (filter(f));
console.log (countWhere (odd) ([1,2,3,4,5]))
// 3
备注
咖喱或不咖喱?
// to curry
const add3 = curry((a, b, c) =>
a + b + c
)
// not to curry
const add3 = a => b => c =>
a + b + c
由于ES6箭头功能是当今JavaScripter的首选,我认为手动调整功能的选择是不言而喻的。它实际上更短,并且只需要以咖喱形式写出它就可以减少开销。
也就是说,你仍然会与不提供他们所暴露的功能的curry形式的lib接口。对于这种情况,我建议
curry
和curryN
(定义见上文)partial
(作为defined here)@Iven,
你的curryN
实现非常好。本节仅供您使用。
const U = f=> f (f)
const Y = U (h=> f=> f(x=> h (h) (f) (x)))
const curryN = Y (h=> xs=> n=> f=>
n === 0 ? f(...xs) : x=> h ([...xs, x]) (n-1) (f)
) ([])
const curry = f=> curryN (f.length) (f)
const add3 = curry ((x,y,z)=> x + y + z)
console .log (add3 (3) (6) (9))
您的curry
函数(以及JavaScript中的most curry
functions that people write)的问题在于它无法正确处理额外的参数。
什么curry
做
假设f
是一个函数,f.length
是n
。让curry(f)
成为g
。我们用g
论证称m
。应该怎么办?
m === 0
然后返回g
。m < n
然后部分地将f
应用于m
新参数,并返回一个新的curried函数接受剩余的n - m
参数。f
应用于m
参数并返回结果。这是大多数curry
功能所做的,这是错误的。前两种情况是正确的,但第三种情况是错误的。相反,它应该是:
m === 0
然后返回g
。m < n
然后部分地将f
应用于m
新参数,并返回一个新的curried函数接受剩余的n - m
参数。m === n
然后将f
应用于m
论点。如果结果是函数,那么curry
结果。最后,返回结果。m > n
然后将f
应用于第一个n
论点。如果结果是函数,那么curry
结果。最后,将结果应用于剩余的m - n
参数并返回新结果。大多数curry
功能的问题
请考虑以下代码:
const countWhere = compose(compose(length), filter);
countWhere(odd, [1,2,3,4,5]);
如果我们使用不正确的curry
函数,那么这相当于:
compose(compose(length), filter, odd, [1,2,3,4,5]);
但是,compose
只接受三个论点。最后一个参数被删除:
const compose = curry((f, g, x) =>f(g(x)));
因此,上面的表达式评估为:
compose(length)(filter(odd));
这进一步评估为:
compose(length, filter(odd));
compose
函数需要一个参数,这就是为什么它返回一个函数而不是返回3
。要获得正确的输出,您需要编写:
countWhere(odd)([1,2,3,4,5]);
这就是大多数curry
功能错误的原因。
使用正确的curry
函数的解决方案
再次考虑以下代码:
const countWhere = compose(compose(length), filter);
countWhere(odd, [1,2,3,4,5]);
如果我们使用正确的curry
函数,那么这相当于:
compose(compose(length), filter, odd)([1,2,3,4,5]);
评估结果为:
compose(length)(filter(odd))([1,2,3,4,5]);
进一步评估(跳过中间步骤):
compose(length, filter(odd), [1,2,3,4,5]);
结果如下:
length(filter(odd, [1,2,3,4,5]));
产生正确的结果3
。
执行正确的curry
函数
在ES6中实现正确的curry
函数非常简单:
const curried = Symbol("curried");
Object.defineProperty(curry, curried, { value: true });
function curry(functor, ...initArgs) {
if (arguments.length === 0) return curry;
if (typeof functor !== "function") {
const value = JSON.stringify(functor);
throw new TypeError(`${value} is not a function`);
}
if (functor[curried]) return functor(...initArgs);
const arity = functor.length;
const args = initArgs.length;
if (args >= arity) {
const result = functor(...initArgs.slice(0, arity));
return typeof result === "function" || args > arity ?
curry(result, ...initArgs.slice(arity)) : result;
}
const result = (...restArgs) => curry(functor, ...initArgs, ...restArgs);
return Object.defineProperty(result, curried, { value: true });
}
我不确定curry
的实施速度有多快。也许有人可以让它更快。
使用正确的curry
函数的含义
使用正确的curry
函数可以直接将Haskell代码转换为JavaScript。例如:
const id = curry(a => a);
const flip = curry((f, x, y) => f(y, x));
id
函数很有用,因为它允许您轻松地部分应用非curried函数:
const add = (a, b) => a + b;
const add2 = id(add, 2);
flip
函数很有用,因为它允许您在JavaScript中轻松创建right sections:
const sub = (a, b) => a - b;
const sub2 = flip(sub, 2); // equivalent to (x - 2)
这也意味着你不需要像这样的extended compose
function这样的黑客:
What's a Good Name for this extended `compose` function?
你可以简单地写:
const project = compose(map, pick);
如问题所述,如果你想组成length
和filter
,那么你使用(f .) . g
模式:
What does (f .) . g mean in Haskell?
另一种解决方案是创建更高阶的compose
函数:
const compose2 = compose(compose, compose);
const countWhere = compose2(length, fitler);
这是可能的,因为curry
函数的正确实现。
额外的食物供您考虑
当我想编写一系列函数时,我通常使用以下chain
函数:
const chain = compose((a, x) => {
var length = a.length;
while (length > 0) x = a[--length](x);
return x;
});
这允许您编写如下代码:
const inc = add(1);
const foo = chain([map(inc), filter(odd), take(5)]);
foo([1,2,3,4,5,6,7,8,9,10]); // [2,4,6]
这相当于以下Haskell代码:
let foo = map (+1) . filter odd . take 5
foo [1,2,3,4,5,6,7,8,9,10]
它还允许您编写如下代码:
chain([map(inc), filter(odd), take(5)], [1,2,3,4,5,6,7,8,9,10]); // [2,4,6]
这相当于以下Haskell代码:
map (+1) . filter odd . take 5 $ [1,2,3,4,5,6,7,8,9,10]
希望有所帮助。
除了它的数学定义
currying是将
n
参数函数转换为n
函数序列,每个函数接受一个参数。因此,arity从n-ary
转变为n * 1-ary
影响编程的影响是什么? arity抽象!
const comp = f => g => x => f(g(x));
const inc = x => x + 1;
const mul = y => x => x * y;
const sqr = x => mul(x)(x);
comp(sqr)(inc)(1); // 4
comp(mul)(inc)(1)(2); // 4
comp
期待两个函数f
和g
以及一个任意参数x
。因此g
必须是一元函数(一个只有一个形式参数的函数)和f
,因为它用g
的返回值。任何人都不会对comp(sqr)(inc)(1)
的作品感到惊讶。 sqr
和inc
都是一元的。
但mul
显然是一个二元函数。这怎么样才能起作用?因为currying抽象了mul
的arity。你现在可以想象一下强大的功能是什么。
在ES2015中,我们可以简洁地使用箭头功能预先调整我们的功能:
const map = (f, acc = []) => xs => xs.length > 0
? map(f, [...acc, f(xs[0])])(xs.slice(1))
: acc;
map(x => x + 1)([1,2,3]); // [2,3,4]
然而,我们需要一个程序化的咖喱功能来控制我们无法控制的所有功能。由于我们了解到currying主要意味着对arity的抽象,我们的实现不能依赖于Function.length
:
const curryN = (n, acc = []) => f => x => n > 1
? curryN(n - 1, [...acc, x])(f)
: f(...acc, x);
const map = (f, xs) => xs.map(x => f(x));
curryN(2)(map)(x => x + 1)([1,2,3]); // [2,3,4]
明确地将arity传递给curryN
具有很好的副作用,我们也可以调整可变参数函数:
const sum = (...args) => args.reduce((acc, x) => acc + x, 0);
curryN(3)(sum)(1)(2)(3); // 6
一个问题仍然存在:我们的咖喱解决方案无法处理方法。好的,我们可以轻松地重新定义我们需要的方法:
const concat = ys => xs => xs.concat(ys);
const append = x => concat([x]);
concat([4])([1,2,3]); // [1,2,3,4]
append([4])([1,2,3]); // [1,2,3,[4]]
另一种方法是以能够处理多参数函数和方法的方式调整curryN
:
const curryN = (n, acc = []) => f => x => n > 1
? curryN(n - 1, [...acc, x])(f)
: typeof f === "function"
? f(...acc, x)
: x[f](...acc);
curryN(2)("concat")(4)([1,2,3]); // [1,2,3,4]
我不知道这是否是正确的方法来解决Javascript中的函数(和方法)。这是一种可能的方式。
编辑:
naomik指出,通过使用默认值,咖喱功能的内部API部分暴露。因此,实现的咖喱功能的简化以其稳定性为代价。为了避免API泄漏,我们需要一个包装函数。我们可以使用U组合器(类似于naomik的Y解决方案):
const U = f => f(f);
const curryN = U(h => acc => n => f => x => n > 1
? h(h)([...acc, x])(n-1)(f)
: f(...acc, x))([]);
缺点:实现更难以阅读并且性能下降。
//---Currying refers to copying a function but with preset parameters
function multiply(a,b){return a*b};
var productOfSixNFiveSix = multiply.bind(this,6,5);
console.log(productOfSixNFive());
//The same can be done using apply() and call()
var productOfSixNFiveSix = multiply.call(this,6,5);
console.log(productOfSixNFive);
var productOfSixNFiveSix = multiply.apply(this,[6,5]);
console.log(productOfSixNFive);