从EcmaScript角度进行功能编程。 组成,咖喱,部分涂抹

哈Ha!

今天,我们继续在EcmaScript上下文中进行函数式编程的研究,EcmaScript的规范基于JavaScript。 在上一篇文章中,我们研究了基本概念:纯函数,lambda和免疫性概念。 今天,我们将讨论稍微复杂一些的FP技术:合成,currying和纯函数。 该文章以“伪codreview”样式编写,即 我们将在研究相变和重构代码的过程中解决一个实际问题,以使后者近似于相变的理想状态。

因此,让我们开始吧!

假设我们有一个任务:创建一套用于处理回文的工具。
回文
男性性别
从左到右和从右到左以相同方式读取的单词或短语。
“ P. “我用法官的剑去”
该任务的可能实现之一可能是这样的:

function getPalindrom (str) { const regexp = /[\.,\/#!$%\^&\*;:{}=\-_`~()?\s]/g; str = str.replace(regexp, '').toLowerCase().split('').reverse().join(''); // -       ,       return str; } function isPalindrom (str) { const regexp = /[\.,\/#!$%\^&\*;:{}=\-_`~()?\s]/g; str = str.replace(regexp, '').toLowerCase(); return str === str.split('').reverse().join(''); } 

当然,此实现有效。 我们可以预期,如果api返回正确的数据,则getPalindrom将正常工作。 调用isPalindrom(“我将与剑士同行”)将返回true,而调用isPalindrom(“ not a palindrome”)将返回false。 就函数式编程的理想而言,此实现是否良好? 绝对不好!

根据本文纯函数的定义:
纯函数(PF)-始终返回预测结果。
PF特性:

PF执行的结果仅取决于传递的参数和实现PF的算法
不要使用全局值
不要修改外部值或传递的参数
不要将数据写入文件,数据库或其他任何地方
我们在回文示例中看到了什么?

首先,有重复的代码,即 违反了DRY的原理。 其次,getPalindrom函数访问数据库。 第三,函数修改其参数。 总计,我们的功能并不干净。

回顾一下定义:函数式编程是通过编译一组函数来编写代码的方式。

我们为此任务组合了一组功能:

 const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g;//(1) const replace = (regexp, replacement, str) => str.replace(regexp, replacement);//(2) const toLowerCase = str => str.toLowerCase();//(3) const stringReverse = str => str.split('').reverse().join('');//(4) const isStringsEqual = (strA, strB) => strA === strB;//(5) 

在第1行中,我们以函数形式声明了正则表达式常量。 FP中经常使用这种描述常数的方法。 在第2行中,我们将String.prototype.replace方法封装在功能替换抽象中,以便它(替换调用)符合功能编程约定。 在第3行上,以相同的方式创建了String.prototype.toLowerCase的抽象。 在第4种方法中,他们实现了一个函数,该函数从传递的字符串中创建一个新的扩展字符串。 第五次检查字符串是否相等。

请注意,我们的功能非常干净! 在上一篇文章中,我们讨论了纯函数的好处。

现在,我们需要执行检查以查看字符串是否是回文。 功能组合将对我们有所帮助。

函数的组成是将两个或多个函数合并为某个所得函数,该函数实现了按所需算法序列组合的函数的行为。

该定义可能看起来很复杂,但是从实际的角度来看,这是公平的。

我们可以这样做:

 isStringsEqual(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    ')), stringReverse(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    ')))); 

或像这样:

 const strA = toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    ')); const strB = stringReverse(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    '))); console.log(isStringsEqual(strA, strB)); 

或为已实施算法的每个步骤输入另一堆说明变量。 此类代码通常可以在项目上看到,这是组合的典型示例-将对一个函数的调用作为对另一个函数的参数传递。 但是,正如我们所看到的,在存在许多功能的情况下,这种方法是不好的,因为 此代码不可读! 那现在呢? 好吧,它的功能编程,我们不同意吗?

实际上,就像函数编程中通常那样,我们只需要编写另一个函数即可。

 const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); 

compose函数将可执行函数的列表作为参数,将它们转换为数组,将其存储在闭包中,并返回需要初始值的函数。 传递初始值后,将开始顺序执行fns数组中的所有功能。 第一个函数的参数将是传递的初始值x,所有后续函数的参数将是前一个参数的结果。 因此,我们可以创建任何数量的功能的组合。

创建功能组合时,监视输入参数的类型和每个函数的返回值非常重要,这样就不会出现意外错误,因为 我们将前一个函数的结果传递给下一个。

但是,现在我们已经看到将合成技术应用于代码的问题,因为该函数:

 const replace = (regexp, replacement, str) => str.replace(regexp, replacement); 

期望接受3个输入参数,而我们仅发送一个输入参数。 另一种FP技术Currying将帮助我们解决此问题。

固化是将一个函数从多个参数转换为一个函数从一个参数转换。

还记得我们第一篇文章中的add函数吗?

 const add = (x,y) => x+y; 

可以这样处理:

 const add = x => y => x+y; 

该函数采用x并返回期望y的lambda并执行操作。

咖喱的好处:

  • 代码看起来更好;
  • 咖喱函数总是干净的。

现在,我们对替换函数进行转换,以使其仅接受一个参数。 由于我们需要用以前已知的正则表达式替换字符串中字符的函数,因此我们可以创建部分应用的函数。

 const replaceAllNotWordSymbolsGlobal = replacement => str => replace(allNotWordSymbolsRegexpGlobal(), replacement, str); 

如您所见,我们用一个常数修复其中一个参数。 这是由于以下事实:咖喱实际上是部分使用的特殊情况。

一个部分应用程序使用一个包装器包装一个函数,该包装器接受的参数少于该函数本身;包装器应返回一个接受其余参数的函数。

在我们的例子中,我们创建了replaceAllNotWordSymbolsGlobal函数,这是部分应用的替换选项。 它接受替换,将其存储在一个闭包中,并期望有一个输入行被调用replace,并且我们使用常量进行正则表达式。

回到回文。 为回文计时创建功能组合:

 const processFormPalindrom = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, stringReverse ); 

以及我们将用来比较潜在回文的那条线的功能组成:

 const processFormTestString = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, ); 

现在,请记住我们上面说的话:
一个典型的合成示例是将对一个函数的调用作为对另一个函数的参数传递
并写:

 const testString = '    ';//          , .. ,    ,  ,   -   ,    const isPalindrom = isStringsEqual(processFormPalindrom(testString), processFormTestString(testString)); 

在这里,我们有一个可行且美观的解决方案:

 const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g; const replace = (regexp, replacement, str) => str.replace(regexp, replacement); const toLowerCase = str => str.toLowerCase(); const stringReverse = str => str.split('').reverse().join(''); const isStringsEqual = (strA, strB) => strA === strB; const replaceAllNotWordSymbolsGlobal = replacement => str => replace(allNotWordSymbolsRegexpGlobal(), replacement, str); const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); const processFormPalindrom = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, stringReverse ); const processFormTestString = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, ); const testString = '    '; const isPalindrom = isStringsEqual(processFormPalindrom(testString), processFormTestString(testString)); 

但是,我们不希望每次都花钱,也不想用手创建部分应用的函数。 当然我们不想要,程序员是懒惰的人。 因此,正如在FP中通常会发生的那样,我们将编写另外两个函数:

 const curry = fn => (...args) => { if (fn.length > args.length) { const f = fn.bind(null, ...args); return curry(f); } else { return fn(...args) } } 

咖喱函数接受一个要咖喱的函数,将其存储在一个闭包中,并返回一个lambda。 lambda期望该函数的其余参数。 每次收到参数时,它都会检查是否接受了所有已声明的参数。 如果被接受,则调用该函数并返回其结果。 如果不是,则再次调用该函数。

我们还可以创建部分应用的函数,以用空字符串替换所需的正则表达式:

 const replaceAllNotWordSymbolsToEmpltyGlobal = curry(replace)(allNotWordSymbolsRegexpGlobal(), ''); 

一切似乎都很好,但是我们是完美主义者,我们不希望括号太多,我们希望更好,所以我们将编写另一个或两个函数:

 const party = (fn, x) => (...args) => fn(x, ...args); 

这是用于创建部分应用功能的抽象实现。 它使用一个函数和第一个参数,返回一个需要其余部分的lambda并执行该函数。

现在我们重写party,以便我们可以创建一个包含多个参数的部分应用函数:

 const party = (fn, ...args) => (...rest) => fn(...args.concat(rest)); 

值得一提的是,可以用少于声明的数量(fn.length)的任意数量的参数调用以这种方式咖喱的函数。

 const sum = (a,b,c,d) => a+b+c+d; const fn = curry(sum); const r1 = fn(1,2,3,4);//,   const r2 = fn(1, 2, 3)(4);//       const r3 = fn(1, 2)(3)(4); const r4 = fn(1)(2)(3)(4); const r5 = fn(1)(2, 3, 4); const r6 = fn(1)(2)(3, 4); const r7 = fn(1, 2)(3, 4); 

让我们回到我们的回文。 我们可以重写我们的replaceAllNotWordSymbolsToEmpltyGlobal,而无需使用多余的括号:

 const replaceAllNotWordSymbolsToEmpltyGlobal = party(replace,allNotWordSymbolsRegexpGlobal(), ''); 

让我们看一下整个代码:

 //    -       const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g; const replace = (regexp, replacement, str) => str.replace(regexp, replacement); const toLowerCase = str => str.toLowerCase(); const stringReverse = str => str.split('').reverse().join(''); const isStringsEqual = (strA, strB) => strA === strB; //       const testString = '    '; //           -    rambda.js const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); const curry = fn => (...args) => { if (fn.length > args.length) { const f = fn.bind(null, ...args); return curry(f); } else { return fn(...args) } } const party = (fn, ...args) => (...rest) => fn(...args.concat(rest)); //       const replaceAllNotWordSymbolsToEmpltyGlobal = party(replace,allNotWordSymbolsRegexpGlobal(), ''); const processFormPalindrom = compose( replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase, stringReverse ); const processFormTestString = compose( replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase, ); const checkPalindrom = testString => isStringsEqual(processFormPalindrom(testString), processFormTestString(testString)); 

看起来不错,但是如果不是我们想要的字符串,而是数组,该怎么办? 因此,我们添加了另一个功能:

 const map = fn => (...args) => args.map(fn); 

现在,如果我们具有用于测试回文序列的数组,则:

 const palindroms = ['    ','   ','   '. ' '] map(checkPalindrom )(...palindroms ); // [true, true, true, false]   

这就是我们通过编写功能集解决任务的方式。 注意编写代码的毫无意义的风格-这是对功能纯度的试金石。

现在多一点理论。 使用currying时请不要忘记每次使用一个函数都会创建一个新的函数,即 为此选择一个存储单元。 对此进行监视以避免泄漏很重要。

诸如ramda.js之类的功能库具有compose和pipe函数。 compose从右到左实现合成算法,从左到右实现管道。 我们的compose函数类似于ramda的管道。 库中有两个不同的合成函数,因为 从右到左和从左到右的组合是功能编程的两个不同契约。 如果其中一位读者找到描述FP所有现有合同的文章,然后在评论中分享它,我将很高兴阅读并在评论中加上一个加号!

函数的形式参数的数量称为arity 。 从相变理论的角度来看,这也是一个重要的定义。

结论


在本文的框架中,我们研究了功能编程技术,例如合成,计算和部分应用。 当然,在实际项目中,您将使用具有这些工具的现成库,但作为本文的一部分,我在本机JS上实现了所有功能,以便对FP经验不多的读者可以了解这些技术是如何工作的。

为了说明我在代码中实现功能纯净的逻辑,我还特意选择了叙述方法-伪codreview。

顺便说一句,您可以继续开发与回文校验模块的模块并开发其思想,例如,通过api下载行,转换为字母集并将其发送到服务器,回文行将由回文校验管生成,以及更多...您可以自行决定。

在这些行的过程中消除重复也将是一件好事:

  replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase, 

通常,有可能并且有必要不断改进代码!

直到以后的文章。

Source: https://habr.com/ru/post/zh-CN475324/


All Articles