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

Aad*_*hah 10 javascript haskell lambda-calculus currying partial-application

curry在JavaScript中编写了一个简单的函数,可以在大多数情况下正常工作

const add = curry((a, b, c) => a + b + c);

const add2 = add(2);

const add5 = add2(3);

console.log(add5(5));
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

但是,它不适用于以下情况:

// 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);
Run Code Online (Sandbox Code Playgroud)

根据以下问题countWhere定义为(length .) . filter:

什么(f.).在Haskell中意味着什么?

因此我应该可以使用countWhere如下:

const odd = n => n % 2 === 1;

countWhere(odd, [1,2,3,4,5]);
Run Code Online (Sandbox Code Playgroud)

但是,它不返回3(数组的长度[1,3,5]),而是返回一个函数.我究竟做错了什么?

Tha*_*you 14

@Aadit,

我发布这个是因为你在我的答案中分享了一个评论,以功能方式在javascript中"组合"功能?我没有特别报道该帖子中的currying,因为这是一个非常有争议的话题,而不是我想在那里打开的一堆蠕虫.

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

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

无需再费周折...


澄清:

给定f是一个功能而且f.lengthn.我们curry(f)g.我们gm争论来称呼.应该怎么办?你说:

  1. 如果m === 0那么就回来了g.
  2. 如果m < n然后部分适用fm新的论据,并返回其接受余下的一个新的咖喱函数n - m的参数.
  3. 如果m === n然后适用fm参数.如果结果是一个函数,那么咖喱结果.最后,返回结果.
  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
Run Code Online (Sandbox Code Playgroud)

这里发生了两件事:

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

我不相信有很多的空间,在这里争论,但人们似乎错过了什么钻营实际上是

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

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

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

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

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

function id(x) {
  return x;
}
Run Code Online (Sandbox Code Playgroud)

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

  1. JavaScript的工作原理
  2. 维基百科如何说curry应该有效
  3. 我们应该如何实施自己的curry解决方案

在我们进一步讨论之前,我将使用ES6风格的箭头函数,但我还将在本文的底部包含ES5等效函数.(可能晚些时候今晚.)

curous技术ànanaomik

在这种方法中,我们编写一个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
Run Code Online (Sandbox Code Playgroud)

好的,所以我们已经看到了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
Run Code Online (Sandbox Code Playgroud)


咖喱或不咖喱?就是那个问题

我们将编写一些示例,其中我们的函数都是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
Run Code Online (Sandbox Code Playgroud)

你的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
Run Code Online (Sandbox Code Playgroud)


备注

咖喱或不咖喱?

// to curry
const add3 = curry((a, b, c) =>
  a + b + c
)

// not to curry
const add3 = a => b => c =>
 a + b + c
Run Code Online (Sandbox Code Playgroud)

由于ES6箭头功能是当今JavaScripter的首选,我认为手动调整功能的选择是不言而喻的.它实际上更短,并且只需要以咖喱形式写出它就可以减少开销.

也就是说,你仍然会与不提供他们所暴露的功能的curry形式的lib接口.对于这种情况,我建议


@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))
Run Code Online (Sandbox Code Playgroud)

  • @AaditMShah我很感激回应.Fwiw,计算机科学对currying的定义与数学没有区别.最好的祝福. (2认同)

Aad*_*hah 12

你的问题curry的功能(和大多数 功能在JavaScript)的是,它不能正确处理额外的参数.curry

是什么curry

假设f是一个功能而且f.lengthn.我们curry(f)g.我们gm争论来称呼.应该怎么办?

  1. 如果m === 0那么就回来了g.
  2. 如果m < n然后部分适用fm新的论据,并返回其接受余下的一个新的咖喱函数n - m的参数.
  3. 否则,适用fm参数和返回结果.

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

  1. 如果m === 0那么就回来了g.
  2. 如果m < n然后部分适用fm新的论据,并返回其接受余下的一个新的咖喱函数n - m的参数.
  3. 如果m === n然后适用fm参数.如果结果是函数,那么curry结果.最后,返回结果.
  4. 如果m > n那么适用f于第一个n参数.如果结果是函数,那么curry结果.最后,将结果应用于其余m - n参数并返回新结果.

大多数curry功能的问题

请考虑以下代码:

const countWhere = compose(compose(length), filter);

countWhere(odd, [1,2,3,4,5]);
Run Code Online (Sandbox Code Playgroud)

如果我们使用不正确的curry函数,那么这相当于:

compose(compose(length), filter, odd, [1,2,3,4,5]);
Run Code Online (Sandbox Code Playgroud)

但是,compose只接受三个参数.最后一个参数被删除:

const compose = curry((f, g, x) =>f(g(x)));
Run Code Online (Sandbox Code Playgroud)

因此,上面的表达式评估为:

compose(length)(filter(odd));
Run Code Online (Sandbox Code Playgroud)

这进一步评估为:

compose(length, filter(odd));
Run Code Online (Sandbox Code Playgroud)

compose函数需要一个参数,这就是它返回函数而不是返回的原因3.要获得正确的输出,您需要编写:

countWhere(odd)([1,2,3,4,5]);
Run Code Online (Sandbox Code Playgroud)

这就是为什么大多数curry功能都是错误的.

解决方案使用正确的curry功能

再次考虑以下代码:

const countWhere = compose(compose(length), filter);

countWhere(odd, [1,2,3,4,5]);
Run Code Online (Sandbox Code Playgroud)

如果我们使用正确的curry函数,那么这相当于:

compose(compose(length), filter, odd)([1,2,3,4,5]);
Run Code Online (Sandbox Code Playgroud)

评估结果为:

compose(length)(filter(odd))([1,2,3,4,5]);
Run Code Online (Sandbox Code Playgroud)

进一步评估(跳过中间步骤):

compose(length, filter(odd), [1,2,3,4,5]);
Run Code Online (Sandbox Code Playgroud)

结果如下:

length(filter(odd, [1,2,3,4,5]));
Run Code Online (Sandbox Code Playgroud)

产生正确的结果3.

执行正确的curry功能

请注意,我不是curry用来将curry对象转换为数组,因为这是V8中的优化杀手:

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 });
}
Run Code Online (Sandbox Code Playgroud)

我不确定这个实现的速度有多快curry.也许有人可以让它更快.

使用正确curry功能的含义

使用正确的id函数可以直接将Haskell代码转换为JavaScript.例如:

const id = curry(a => a);

const flip = curry((f, x, y) => f(y, x));
Run Code Online (Sandbox Code Playgroud)

flip函数很有用,因为它允许您轻松地部分应用非curried函数:

const add = (a, b) => a + b;

const add2 = id(add, 2);
Run Code Online (Sandbox Code Playgroud)

compose函数很有用,因为它允许您在JavaScript中轻松创建正确的部分:

const sub = (a, b) => a - b;

const sub2 = flip(sub, 2); // equivalent to (x - 2)
Run Code Online (Sandbox Code Playgroud)

这也意味着你不需要像这个扩展length功能的黑客:

这个扩展的'compose`函数有什么好名字?

你可以简单地写:

const project = compose(map, pick);
Run Code Online (Sandbox Code Playgroud)

如问题中所述,如果您想要撰写filter,(f .) . g然后使用该compose模式:

什么(f.).在Haskell中意味着什么?

另一种解决方案是创建更高阶的curry函数:

const compose2 = compose(compose, compose);

const countWhere = compose2(length, fitler);
Run Code Online (Sandbox Code Playgroud)

由于chain功能的正确实现,这一切都是可能的.

额外的食物供您考虑

curry当我想要组成一系列函数时,我通常使用以下函数:

const chain = compose((a, x) => {
    var length = a.length;
    while (length > 0) x = a[--length](x);
    return x;
});
Run Code Online (Sandbox Code Playgroud)

这允许您编写如下代码:

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]
Run Code Online (Sandbox Code Playgroud)

这相当于以下Haskell代码:

let foo = map (+1) . filter odd . take 5

foo [1,2,3,4,5,6,7,8,9,10]
Run Code Online (Sandbox Code Playgroud)

它还允许您编写如下代码:

chain([map(inc), filter(odd), take(5)], [1,2,3,4,5,6,7,8,9,10]); // [2,4,6]
Run Code Online (Sandbox Code Playgroud)

这相当于以下Haskell代码:

map (+1) . filter odd . take 5 $ [1,2,3,4,5,6,7,8,9,10]
Run Code Online (Sandbox Code Playgroud)

希望有所帮助.

  • 这个答案并不完全正确,请参阅我对该问题的评论.(我在这里发表评论,但你似乎正在和自己对话.) (2认同)