簡體   English   中英

布爾“非”函數的功能組合(不是布爾值)

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

我在 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 運算符。 “幾乎”下面的代碼有效,但它返回一個布爾值,而不是一個在處理高階函數時不起作用的函數。

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

通常情況下,在准備這個問題時,我最終找到了答案,所以我將分享我的答案,看看社區有哪些改進。

上面觀察到的問題(獲取布爾值而不是函數)是“功能組合”的問題,我在 Eric Elliot 的帖子中找到了幾種可選方法,我從中選擇了“管道”功能組合方法。

看到 Eric Elliot 的精彩帖子

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 函數。 這將返回引導字符串的“非數字”(即數字以外的字符)的計數,這些字符然后從數組的頭部“切片”,並將數組縮減回字符串。

我很想知道任何可以改進這種方法的調整。

很高興您找到答案並仍然發布問題。 我認為這是一種很好的學習方式。

為了進行函數組合練習,以下是我可能會如何構建您的函數。

請參閱下面的保持簡單,了解我將如何使用實用代碼處理此問題

 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組合器和柯里化,這導致了一個非常干凈的實現,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正在合並具有無數用途的恆等函數

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM