布尔型“非”函数的功能组合(不是布尔值)

时间:2017-02-07 19:28:15

标签: 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);

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

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

通常情况下,在准备这个问题时,我最终找到答案,所以我将分享我的答案,看看社区有哪些改进。

上面观察到的问题(得到一个布尔而不是一个函数)是一个'功能组合问题',我在Eric Elliot的帖子中找到了几个可选方法,我选择了'管道'功能组合方法。

see Eric Elliot's excellent post

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

这个管道组合函数的实现看起来像下面的TS ...对于那些在家里跟随的人,我已经包含递归计数而'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'(使用管道函数)组合成'dummy'函数,该函数传递给recCountWhile函数。这将返回引导字符串的'not digits'(即数字以外的字符)的计数,这些字符然后从数组的头部“切片”,然后数组缩减回字符串。

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

1 个答案:

答案 0 :(得分:2)

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

  

为了进行功能组合练习,以下是我如何构建你的功能。

     

请参阅保持简单,了解如何使用实用代码处理此问题

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

代码审核

顺便说一下,你用默认参数和泄露的私有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
}

...但是该功能实际上要好得多,因为它不会将私有API(charC var)泄露给外部调用者

你会注意到我解决这个问题的方法是使用我自己的isBetween组合器和currying,这会产生一个非常干净的实现,imo

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函数是纯函数,不会泄漏私有变量,处理其给定域(String)的所有输入,并保持易于阅读的样式。与你提出的解决方案相比,我会建议这个(或喜欢这个)。

功能组合和“管道”

组合两个函数通常按从右到左的顺序进行。有些人发现难以阅读/推理,所以他们想出了一个从左到右的函数作曲家,大多数人似乎都同意pipe是一个好名字。

您的pipe实现没有任何问题,但我认为很高兴看到您如何尽可能地保持简单,最终的代码会清理一下。

const identity = x => x

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

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

或者,如果您想使用帖子中前面提到的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正在合并具有无数用途的身份功能