如何在JavaScript中正确地理解函数?

问题描述 投票:9回答:4

我在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]的长度),而是返回一个函数。我究竟做错了什么?

javascript haskell currying lambda-calculus partial-application
4个回答
14
投票

@审计,

我发布这个是因为你对我对To “combine” functions in javascript in a functional way?的回答发表评论我没有特别报道该帖子中的currying,因为这是一个非常有争议的话题,而不是我想在那里打开的一堆蠕虫。

当你似乎在你的实现中添加你自己的糖和便利时,我会谨慎使用“如何正确咖喱”的措辞。

无论如何,除此之外,我真的不打算将其作为议论/好斗的帖子。我希望能够就JavaScript的currying进行公开,友好的讨论,同时强调我们的方法之间的一些差异。

无需再费周折...


澄清:

鉴于f是一个函数,f.lengthn。让curry(f)成为g。我们用g论证称m。应该怎么办?你说:

  1. 如果m === 0然后返回g
  2. 如果m < n然后部分地将f应用于m新参数,并返回一个新的curried函数接受剩余的n - m参数。
  3. 如果m === n然后将f应用于m论点。如果结果是一个函数,那么咖喱结果。最后,返回结果。
  4. 如果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

这里发生了两件事:

  1. 您试图支持使用可变参数调用curried函数。
  2. 您将自动调整返回的函数

我不相信这里有很多辩论空间,但人们似乎错过了currying实际上是什么

via:维基百科 在数学和计算机科学中,currying是一种翻译对函数的评估的技术,该函数将多个参数(或参数元组)转换为评估一系列函数,每个函数都有一个参数...

我最后一点粗暴,因为它非常重要;序列中的每个函数只接受一个参数;不像你建议的变量(0,1或更多)参数。

你也在帖子中提到了haskell,所以我假设你知道Haskell没有带有多个参数的函数。 (注意:一个带元组的函数仍然只是一个带有一个参数,一个元组的函数)。其原因是深刻的,并为您提供了具有可变参数的函数无法提供给您的表现力的灵活性。

那么让我们再问一下这个原始问题:应该怎么办?

好吧,当每个函数只接受1个参数时,这很简单。在任何时候,如果给出超过1个参数,它们就会被丢弃。

function id(x) {
  return x;
}

当我们打电话给id(1,2,3,4)时会发生什么?当然,我们只得到1回来,2,3,4完全被忽视。这是:

  1. JavaScript的工作原理
  2. 维基百科如何说curry应该有效
  3. 我们应该如何实施自己的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接口。对于这种情况,我建议

  • currycurryN(定义见上文)
  • 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))

12
投票

您的curry函数(以及JavaScript中的most curry functions that people write)的问题在于它无法正确处理额外的参数。

什么curry

假设f是一个函数,f.lengthn。让curry(f)成为g。我们用g论证称m。应该怎么办?

  1. 如果m === 0然后返回g
  2. 如果m < n然后部分地将f应用于m新参数,并返回一个新的curried函数接受剩余的n - m参数。
  3. 否则将f应用于m参数并返回结果。

这是大多数curry功能所做的,这是错误的。前两种情况是正确的,但第三种情况是错误的。相反,它应该是:

  1. 如果m === 0然后返回g
  2. 如果m < n然后部分地将f应用于m新参数,并返回一个新的curried函数接受剩余的n - m参数。
  3. 如果m === n然后将f应用于m论点。如果结果是函数,那么curry结果。最后,返回结果。
  4. 如果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);

如问题所述,如果你想组成lengthfilter,那么你使用(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]

希望有所帮助。


1
投票

除了它的数学定义

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期待两个函数fg以及一个任意参数x。因此g必须是一元函数(一个只有一个形式参数的函数)和f,因为它用g的返回值。任何人都不会对comp(sqr)(inc)(1)的作品感到惊讶。 sqrinc都是一元的。

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))([]);

缺点:实现更难以阅读并且性能下降。


-1
投票
//---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);
© www.soinside.com 2019 - 2024. All rights reserved.