javascript - bool 'not' 函数的函数组合(不是 bool 值)

标签 javascript functional-programming

我正在 TS 中工作,但会在下面显示 tsc -> ES6 代码。

我有一个函数“isDigit”,如果字符代码在数字 0-9 范围内,则该函数返回 true。该函数 (isDigit) 必须作为参数传递到高阶函数中。

const isDigit = (char, charC = char.charCodeAt(0)) => (charC > 47 && charC < 58);

作为另一个高阶函数的一部分,我需要知道一个字符是否不是数字。当然,像下面这样的东西会起作用......

const notDigit = (char, charC = char.charCodeAt(0)) => !isDigit(char);

但是如果我可以将 isDigit 与另一个函数(我将调用 notFun)组合起来,将 not 运算符应用于 isDigit 的结果来生成 notDigit,那就更令人满意了。在下面的代码中,“boolCond”用于控制是否应用 not 运算符。下面的代码“几乎”可以工作,但它返回一个 bool 值,而不是一个在处理高阶函数时不起作用的函数。

const notFun = (myFun, boolCond = Boolean(condition)) => (boolCond) ? !myFun : myFun;

与通常的情况一样,在准备这个问题时,我最终找到了答案,因此我将分享我的答案并看看社区有哪些改进。

上面观察到的问题(获取 bool 值而不是函数)是“函数组合”的问题,我在 Eric Elliot 的帖子中找到了几种可选方法,从中我选择了“管道”函数组合方法。

see Eric Elliot's excellent post

const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

这个管道组合函数的实现看起来像下面的 TS...对于那些在家中学习的人,我已经包含了递归计数 while 'recCountWhile' 函数,它是组合(即管道)函数的最终使用者(请原谅这些函数出现的顺序颠倒,但这样做是为了清楚起见)。

export const removeLeadingChars: (str: string) => string = 
  (str, arr = str.split(''), 
   dummy = pipe(isDigit, notFun), cnt = recCountWhile(arr, dummy, 0)) => 
          arr.slice(cnt).reduce((acc, e) => acc.concat(e),'');

export const recCountWhile: (arr: string[], fun: (char: string) => boolean, sum: number) => number = 
  (arr, fun, sum, test = (!(arr[0])) || !fun(arr[0]) ) => 
      (test) ? sum : recCountWhile(arr.slice(1), fun, sum + 1);

结果是一个组合函数“removeLeadingChars”,它将“isDigit”与“notFun”(使用管道函数)组合成“虚拟”函数,并传递给recCountWhile函数。这将返回引导字符串的“非数字”(即除数字之外的字符)的计数,然后将这些字符从数组的头部“切片”,并将数组缩减回字符串。

我非常希望听到任何可能改进这种方法的调整。

最佳答案

很高兴您找到答案并仍然发布问题。我认为这是一个很好的学习方式。

For the sake of a function composition exercise, here's how I might structure your functions.

see Keep it simple below for how I would handle this with practical code

const comp = f => g => x => f(g(x))

const ord = char => char.charCodeAt(0)

const isBetween = (min,max) => x => (x >= min && x <= max)

const isDigit = comp (isBetween(48,57)) (ord)

const not = x => !x

const notDigit = comp (not) (isDigit)

console.log(isDigit('0')) // true
console.log(isDigit('1')) // true
console.log(isDigit('2')) // true
console.log(isDigit('a')) // false

console.log(notDigit('0')) // false
console.log(notDigit('a')) // true


代码审查

顺便说一句,你使用默认参数和泄露的私有(private) API 所做的事情非常奇怪

// charC is leaked API
const isDigit = (char, charC = char.charCodeAt(0)) => (charC > 47 && charC < 58);

isDigit('9')     // true
isDigit('9', 1)  // false   wtf
isDigit('a', 50) // true    wtf

我知道您可能正在这样做,因此您不必写此内容

// I'm guessing you want to avoid this
const isDigit = char => {
  let charC = char.charCodeAt(0)
  return charC > 47 && charC < 58
}

...但该函数实际上要好得多,因为它不会将私有(private) API(charC var)泄漏给外部调用者

你会注意到我解决这个问题的方法是使用我自己的 isBetween 组合器和柯里化(Currying),这会导致一个非常干净的实现,我认为

const comp = f => g => x => f(g(x))

const ord = char => char.charCodeAt(0)

const isBetween = (min,max) => x => (x >= min && x <= max)

const isDigit = comp (isBetween(48,57)) (ord)

更多的代码会执行这种可怕的默认参数操作

// is suspect you think this is somehow better because it's a one-liner
// abusing default parameters like this is bad, bad, bad
const removeLeadingChars: (str: string) => string = 
  (str, arr = str.split(''), 
   dummy = pipe(isDigit, notFun), cnt = recCountWhile(arr, dummy, 0)) => 
          arr.slice(cnt).reduce((acc, e) => acc.concat(e),'');

尽量避免为了让一切变得简单而牺牲代码的质量。上面的功能比这里的差很多

// huge improvement in readability and reliability
// no leaked variables!
const removeLeadingChars: (str: string) => string = (str) => {
  let arr = str.split('')
  let dummy = pipe(isDigit, notFun)
  let count = recCountWhile(arr, dummy, 0)
  return arr.slice(count).reduce((acc, e) => acc.concat(e), '')
}

保持简单

不必将字符串拆分为数组,然后迭代数组以计算前导非数字,然后根据计数分割数组的头部,最后将数组重新组装为输出字符串,您可以.. .保持简单

const isDigit = x => ! Number.isNaN (Number (x))

const removeLeadingNonDigits = str => {
  if (str.length === 0)
    return ''
  else if (isDigit(str[0]))
    return str
  else
    return removeLeadingNonDigits(str.substr(1))
}

console.log(removeLeadingNonDigits('hello123abc')) // '123abc'
console.log(removeLeadingNonDigits('123abc'))      // '123abc'
console.log(removeLeadingNonDigits('abc'))         // ''

所以,是的,我不确定您问题中的代码是否只是为了练习,但如果这确实是最终目标,那么确实有一种更简单的方法可以从字符串中删除前导非数字。

这里提供的removeLeadningNonDigits函数是纯函数,不会泄漏私有(private)变量,处理其给定域(字符串)的所有输入,并保持易于阅读的风格。与您问题中建议的解决方案相比,我会建议这个(或类似的东西)。


函数组合和“管道”

组合两个函数通常按从右到左的顺序完成。有些人发现这很难阅读/推理,因此他们想出了一个从左到右的函数编辑器,大多数人似乎都同意 pipe 是一个好名字。

您的 pipe 实现没有任何问题,但我认为很高兴看到如果您努力使事情尽可能简单,生成的代码会变得干净一些。

const identity = x => x

const comp = (f,g) => x => f(g(x))

const compose = (...fs) => fs.reduce(comp, identity)

或者,如果您想使用本文前面介绍的我的柯里化(Currying)comp

const identity = x => x

const comp = f => g => x => f(g(x))

const uncurry = f => (x,y) => f(x)(y)

const compose = (...fs) => fs.reduce(uncurry(comp), identity)

每个函数都有自己独立的实用程序。因此,如果您以这种方式定义 compose,您就可以免费获得其他 3 个函数。

将此与您问题中提供的 pipe 实现进行对比:

const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

同样,这很好,但它将所有这些东西混合在一个函数中。

  • (v,f) => f(v) 本身就是一个有用的函数,为什么不单独定义它,然后通过名称使用它?
  • => x 正在合并具有无数用途的恒等函数

关于javascript - bool 'not' 函数的函数组合(不是 bool 值),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42098298/

相关文章:

javascript - 如何将非常大的内存对象保存到文件中?

javascript - 基于鼠标输入方向的JS动画

javascript - JavaScript 中的对象是按引用还是按值传递的?

javascript - 纯函数能否返回一个在随机时间后解决的 promise ?

javascript - 使用 Lodash 将 JavaScript 数组分成 block

java - 如何使用 Collectors.summarizingInt 和 flatMap 运算符的用法来总结歌曲的投票

javascript - 在 JavaScript 函数中运行而不每次都重新声明它

javascript - 如何在 componentDidMount 中模拟类函数

javascript - 如何使用 jQuery 或 javascript 保留 cookie 而不更改浏览器设置?

.net - F# 短路模式匹配