简体   繁体   English

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

[英]functional composition of a boolean 'not' function (not a boolean value)

I am working in TS but will show the tsc -> ES6 code below.我在 TS 工作,但会在下面显示 tsc -> ES6 代码。

I have a function 'isDigit' that returns true if the the character code is in the range of digits 0-9.我有一个函数“isDigit”,如果字符代码在数字 0-9 的范围内,它会返回 true。 This function (isDigit) must be passed as an argument into a higher order function.此函数 (isDigit) 必须作为参数传递给高阶函数。

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

As part of another higher order function, I need to know if a character is NOT a digit.作为另一个高阶函数的一部分,我需要知道一个字符是否不是数字。 Of course something like below would work...当然,像下面这样的东西会起作用......

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

BUT it would be more satisfying if I could compose isDigit with another function (I'll call notFun) to apply a not operator to the result of isDigit to make notDigit.但是,如果我可以将 isDigit 与另一个函数(我将调用 notFun)组合起来,将 not 运算符应用于 isDigit 的结果以生成 notDigit,那将会更令人满意。 In the code below 'boolCond' serves to control if the not operator is applied or not.在下面的代码中,'boolCond' 用于控制是否应用 not 运算符。 The code below 'almost' works, but it returns a boolean not a function which does not work when dealing with higher order functions. “几乎”下面的代码有效,但它返回一个布尔值,而不是一个在处理高阶函数时不起作用的函数。

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

As is usually the case, while preparing this question I wound up finding an answer, so I will share my answer and see what improvements come from the community.通常情况下,在准备这个问题时,我最终找到了答案,所以我将分享我的答案,看看社区有哪些改进。

The issue observed above (getting a boolean instead of a function) is an issue of 'functional composition, I found several optional approaches in the post of Eric Elliot, from which I selected the 'pipe' functional composition method.上面观察到的问题(获取布尔值而不是函数)是“功能组合”的问题,我在 Eric Elliot 的帖子中找到了几种可选方法,我从中选择了“管道”功能组合方法。

see Eric Elliot's excellent post 看到 Eric Elliot 的精彩帖子

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

The implementation of this pipe composition function looked like the below TS... For those following along at home, I have included the recursive count while 'recCountWhile' function that is the ultimate consumer of the composed (ie piped) function (please excuse the inverted order that these functions appear but this was done for clarity).这个管道组合函数的实现看起来像下面的 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);

The result is a composed function 'removeLeadingChars' that composes the 'isDigit' with the 'notFun' (using the pipe function ) into 'dummy' function that is passed to the recCountWhile function.结果是一个组合函数“removeLeadingChars”,它将“isDigit”和“notFun”(使用管道函数)组合成“dummy”函数,然后传递给 recCountWhile 函数。 This returns the count of 'not digits' (ie characters other than digits) that lead the string, these characters that are then 'sliced' from the head of the array, and the array is reduced back to a string.这将返回引导字符串的“非数字”(即数字以外的字符)的计数,这些字符然后从数组的头部“切片”,并将数组缩减回字符串。

I would be very keen to hear about any tweaks that may improve on this approach.我很想知道任何可以改进这种方法的调整。

Good on you to find your answer and still post the question.很高兴您找到答案并仍然发布问题。 I think this is a nice way to learn.我认为这是一种很好的学习方式。

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


Code review代码审查

Btw, this thing you're doing with the default parameters and leaked private API is pretty wonky顺便说一句,你用默认参数和泄露的私有 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 understand you're probably doing it so you don't have to write this我知道你可能正在这样做,所以你不必写这个

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

... but that function is actually a lot better because it doesn't leak private API (the charC var) to the external caller ...但该函数实际上要好得多,因为它不会将私有 API( charC var)泄漏给外部调用者

You'll notice the way I solved this in mine was to use my own isBetween combinator and currying which results in a pretty clean implementation, imo你会注意到我在我的解决这个问题的方法是使用我自己的isBetween组合器和柯里化,这导致了一个非常干净的实现,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)

More of your code that does this awful default parameters thing你的更多代码做了这个可怕的默认参数事情

// 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),'');

Try to avoid compromising the quality of your code for the sake of making everything a one-liner.尽量避免为了使所有内容成为单行代码而损害代码质量。 The above function is much worse than this one here上面的功能比这里的差很多

// 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), '')
}

Keep it simple把事情简单化

Instead of splitting the string into an array, then iterating over the array to count the leading non digits, then slicing the head of the array based on the count, then finally reassembling the array into an output string, you can... keep it simple不是将字符串拆分为数组,然后遍历数组以计算前导非数字,然后根据计数对数组的头部进行切片,最后将数组重新组装为输出字符串,您可以...保留它简单的

 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')) // ''

So yeah, I'm not sure if the code in your question was merely there for an exercise, but there's really a much simpler way to remove leading non digits from a string, if that's really the end goal.所以是的,我不确定您问题中的代码是否仅用于练习,但是如果这真的是最终目标,那么从字符串中删除前导非数字确实有更简单的方法。

The removeLeadningNonDigits function provided here is pure function, does not leak private variables, handles all inputs for its given domain (String), and maintains an easy-to-read style.这里提供的removeLeadningNonDigits函数是纯函数,不会泄漏私有变量,处理给定域(String)的所有输入,并保持易于阅读的风格。 I would suggest this (or something like this) compared to the proposed solution in your question.相比于你的问题提出的解决方案,我建议这个(或这样)。


Function Composition and "Pipe"函数组成和“管道”

Composing two functions is usually done in right-to-left order.组合两个函数通常按从右到左的顺序完成。 Some people find that hard to read/reason about, so they came up with a left-to-right function composer and most people seem to agree that pipe is a good name.有些人觉得这很难阅读/解释,所以他们想出了一个从左到右的函数作曲家,大多数人似乎都同意pipe是一个好名字。

There's nothing wrong with your pipe implementation, but I think it's nice to see how if you strive to keep things as simple as possible, the resulting code cleans up a bit.您的pipe实现没有任何问题,但我认为很高兴看到如果您努力使事情尽可能简单,结果代码会稍微清理一下。

const identity = x => x

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

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

Or if you'd like to work with my curried comp presented earlier in the post或者,如果您想使用我在帖子前面介绍的咖喱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)

Each of these functions have their own independent utility.这些功能中的每一个都有自己独立的效用。 So if you define compose in this way, you get the other 3 functions for free.因此,如果您以这种方式定义compose ,您就可以免费获得其他 3 个功能。

Contrast this to the pipe implementation provided in your question:将此与您的问题中提供的pipe实现进行对比:

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

Again, it's fine, but it mixes all of these things together in one function.同样,这很好,但它将所有这些东西混合在一个函数中。

  • (v,f) => f(v) is useful function on its own, why not define it separately and then use it by name? (v,f) => f(v)本身就是有用的函数,为什么不单独定义它然后按名称使用它呢?
  • the => x is merging the identity function which has countless uses => x正在合并具有无数用途的恒等函数

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM