在阅读关于 Currying(柯里化) , Partial Application(偏函数应用) 和其他函数式编程技术之后,一些开发人员不知道应该什么时候使用这些方法;为什么要这样使用?
在接下来的三篇系列文章中,我们将尝试解决这个问题,我们会尝试,并向你展示如何在一个短小而现实的例子中用函数式编程的方式,解决这个问题。
什么是函数式编程(Functional Programming)
在我们深入之前,让我们花一点时间来回顾一下一些实用的函数式编程概念。
函数式编程把“function”作为重复使用的主要表达式。通过构建专注于某个特定任务的小函数,函数式编程使用合成(compose)来构建更复杂的函数 —— 这就是 Currying(柯里化) 和 Partial Application(偏函数应用) 这样的技术发挥作用的地方了。
函数式编程使用函数作为重复使用的声明表达式,避免对状态进行修改,消除了副作用,并使用合成来构建函数。
功能编程本质上是用功能编程的!额外需要考虑的是:如避免状态改变,无副作用的纯函数,消除循环支持递归是纯函数式编程方法的一部分,用 Haskell 语言是这样构建的。
我们将重点介绍函数式编程的实用部分,以便我们可以在本系列博客文章中立即使用 Javascript 。
高阶函数(Higher Order Functions) – JavaScript 中函数是“一等公民(first-class)”,这意味着我们可以将函数作为参数传递给其他函数;也可以将函数作为其他函数的值返回。愚人码头注:以函数为参数或返回值的函数称为“高阶函数”。
装饰器(Decorators) – 因为 JavaScript 中函数可以是高阶函数,所以我们可以创建函数来增加其他函数的行为 和/或 作为其他函数的参数。
合成(Composition) – 我们还可以创建由多个函数合成的函数,创建链式的输入处理。
我们将介绍我们要使用的技术,以便在需要时利用这些特性。这让我们可以在上下文环境中引入它们,并使概念易于消化和理解。
让我们开始吧
OK,那我们打算怎么办呢?
我们来看一个典型的例子,它需要处理从异步请求中获取的一些数据。 在这种情况下,异步获取数据采用了JSON格式,并包含了一个博客文章的摘要列表。
以下是我们将使用的异步获取数据:查看 Gist中的完整数据 和 示例数据。
// 异步获取 JSON 数据的一条示例数据 var records = [ { "id": 1, "title": "Currying Things", "author": "Dave", "selfurl": "/posts/1", "published": 1437847125528, "tags": [ "functional programming" ], "displayDate": "2015-07-25" }, // ... ];
我们的需求:现在,假设我们想要显示最近的文章(不超过一个月),按标签分组,按发布日期排序。让我们思考一下我们需要做些什么
- 过滤掉一个月以前的文章(比如30天)。
- 通过他们的tags对文章进行分组(这可能意味着如果他们有多个标签,则会显示在两个分组中。)
- 按发布日期排序每个标签列表,降序。
我们将在本系列文章中涵盖上述每个需求,这篇文章从过滤开始。
过滤数据
我们的第一步是过滤掉发布日期超过 30 天的文章记录。由于函数式编程都是作为重用的主要表达式的函数,所以让我们构建一个函数来封装过滤列表的行为。
function filter(list, fn) { return list.filter(fn); }
有些朋友可能会问,“真的吗?就这样好了吗?”
嗯,是的,没有更多要写的了。
这个函数使用 predicate 断言函数(fn) 来过滤一个数组(list),或许你会说,这可以通过直接调用list.filter(fn)来轻松实现。那么为什么不这样做呢?
因为当我们将操作抽象成一个函数时,我们就可以使用 Currying(柯里化) 来构建一个更有用的函数。
Currying(柯里化) 是使用 N 个参数的函数,返回一个 N 个函数的嵌套系列,每个函数都采用 1 个参数。
有关 Currying(柯里化) 概念的更多信息,请阅读我以前的文章,并实现 left -> right 的 currying(柯里化) 。
在这种情况下,我们将使用一个名为rightCurry()的函数,该函数将函数的参数从右向左进行柯里化。通常,一个普通curry()函数会将参数从左到右进行柯里化。
这是我们的实现,以及它在内部使用的另一个实用函数flip()。
// 返回一个函数, // 该函数在调用时将参数的顺序颠倒过来。 function flip(fn) { return function() { var args = [].slice.call(arguments); return fn.apply(this, args.reverse()); }; } // 返回一个新函数, // 从右到左柯里化原始函数的参数。 function rightCurry(fn, n) { var arity = n || fn.length, fn = flip(fn); return function curried() { var args = [].slice.call(arguments), context = this; return args.length >= arity ? fn.apply(context, args.slice(0, arity)) : function () { var rest = [].slice.call(arguments); return curried.apply(context, args.concat(rest)); }; }; }
通过 currying(柯里化) ,我们可以创建一些函数,允许我们创建新的、偏应用的函数,我们可以重用这些函数。 在我们这个例子中,我们将使用它来创建一个函数,该函数部分应用 predicate 断言函数(fn)来进行过滤列表的操作。
// 一个函数,使用给定 predicate 断言函数 过滤列表 var filterWith = rightCurry(filter);
这基本上与手动调用二元的filter(list, fn)函数一样,进行相同的操作。
function filterWith(fn) { return function(list) { return filter(list, fn); } }
我们可以如下使用它吗?
var list = [1,2,3,4,5,6,7,8,9,10]; // 创建一个偏应用过滤器,获取列表中的偶数 var justEvens = filterWith(function(n) { return n%2 == 0; }); justEvens(list); // [2,4,6,8,10]
哇,可以!最初似乎是很多的工作; 但是我们从这个方法中得出的结论是:
- 使用 currying(柯里化) 创建一个通用的,可重用的函数,filterWith(),可以在许多情况下使用它来创建更具体的列表过滤器
- 每当我们得到一些数据时,都可以懒惰地执行这个新的过滤器。我们不能做到调用Array.prototype.filter的同时,不使其立即对数据列表执行操作
- 一个更具声明性的API,有助于可读性和理解
关于 predicate 断言函数
我们的filterWith()函数需要一个 predicate 断言函数,当给定列表中的某个元素时,它返回true或false,以确定是否应该在新过滤的列表中返回该元素。
让我们从一个更通用的比较函数开始,它可以告诉我们一个给定的数是否大于或等于另一个数。
// 简单的使用 '>=' 比较 function greaterThanOrEqual(a, b) { return a >= b; }
我们文章的发布日期可以转换成数字,时间戳格式(自Epoch以来的毫秒数)这应该可以正常工作。但是,用于过滤数组的断言函数只能传递一个参数来检查,而不是两个。
那么,在需要一元函数的情况下,如何使我们的二元比较函数工作呢?
Currying(柯里化) 可以再次拯救我们!我们将使用它来创建一个函数,该函数可以创建一元比较函数。
var greaterThanOrEqualTo = rightCurry(greaterThanOrEqual);
我们现在可以使用这个柯里化版本来创建一个 predicate 断言函数,可以用于列表过滤,例如:
var list = [5,3,6,2,8,1,9,4,7], // a unary comparison function to see if a value is >= 5 fiveOrMore = greaterThanOrEqualTo(5); filterWith(fiveOrMore)(list); // [5,6,8,9,7]
棒极了! 现在我们回到我们的示例,创建一个 predicate 断言函数,具体解决我们原先的过滤掉发布在30天以前的文章了:
var thirtyDaysAgo = (new Date()).getTime() - (86400000 * 30), within30Days = greaterThanOrEqualTo(thirtyDaysAgo); var dates = [ (new Date('2015-07-29')).getTime(), (new Date('2015-05-01')).getTime() ]; filterWith(within30Days)(dates); // [1438128000000] - July 29th, 2015
到现在为止还挺好!
我们创建了一个可以轻松重用的 过滤 断言函数。另外,因为我们使用的是函数式方法,所以我们的代码更具声明性,易于遵循 – 它的读取方式与工作原理完全相同。可读性和维护是编写任何代码时需要考虑的重要事情!
类型问题…
呃,我们还有另一个问题!我们的程序需要过滤的是一个对象列表,所以我们的 predicate 断言函数将需要访问传入的每一项的published属性。
我们目前的 predicate 断言函数,within30Days()不能处理对象类型的参数,只能处理具体的数值!让我们用另一个函数来解决这个问题吧!(你在这里看到一个模式了吗?)
我们想重用我们现有的断言函数;但修改其参数,以便它可以与我们的特定对象类型一起使用。这是一个新的实用函数,让我们通过修改其参数来扩展现有的函数。
function useWith(fn /*, txfn, ... */) { var transforms = [].slice.call(arguments, 1), _transform = function(args) { return args.map(function(arg, i) { return transforms[i](arg); }); }; return function() { var args = [].slice.call(arguments), targs = args.slice(0, transforms.length), remaining = args.slice(transforms.length); return fn.apply(this, _transform(targs).concat(remaining)); } }
这是迄今为止最有趣的函数式实用工具函数,并且几乎与 Ramda.js 库 中相同名称的函数相同。
useWith()返回一个修改原来函数(fn)的函数,所以当被调用时,它将通过相应的变换(txnfn)函数传递每个参数。如果在调用时比转换函数有更多的参数,那么剩下的参数将会以 “as is” 的形式传递。
让我们用一个小例子来帮助解释这个定义。简单地说,useWith()让我们执行以下操作:
function sum(a,b) { return a + b; } function add1(v) { return v+1; } var additiveSum = useWith(sum, add1, add1); // 在总和接收 4 & 5 之前, // 它们都首先通过 'add1()' 函数进行转换 additiveSum(4,5); // 11
当我们调用additiveSum(4,5)时,我们基本上可以得到以下调用栈:
-
additiveSum(4,5)
- add1(4) => 5
- add1(5) => 6
- sum(5, 6) => 11
我们可以使用useWith()来修改现有的 predicate 断言函数来在对象类型上操作,而不是数值。首先,让我们再次使用 currying(柯里化) 来创建一个函数,该函数允许我们创建 偏应用的函数,这些函数可以通过属性名访问对象。
// 用于访问对象属性的函数 function get(obj, prop) { return obj[prop]; } // `get()` 的柯里化版本 var getWith = rightCurry(get);
现在我们可以使用getWith()作为变换函数,从每个对象获取.published日期,传递给用于过滤器(filter)的一元断言函数。
// 我们修改后的断言函数可以在 // record 对象的 `.published` 属性上工作 var within30Days = useWith(greaterThanOrEqualTo(thirtyDaysAgo), getWith('published'));
我们来试试看一下测试数据:
// 简单的对象数组 var dates = [ { id: 1, published: (new Date('2015-07-29')).getTime() }, { id: 2, published: (new Date('2015-05-01')).getTime() } ], within30Days = useWith(greaterThanOrEqualTo(thirtyDaysAgo), getWith('published')); // 获取数组中 published(发布日期)在30天内的任何对象 filterWith(within30Days)(dates); // { id: 1, published: 1438128000000 }
准备过滤!
好的,鉴于我们的第一个需求是保留最近30天内的文章记录,那么用我们的响应数据来提供一个完整的实现。
filterWith(within30Days)(records); // [ // { id: 1, title: "Currying Things", displayDate: "2015-07-25", ... }, // { id: 2, title: "ES6 Promises", displayDate: "2015-07-26", ... }, // { id: 7, title: "Common Promise Idioms", displayDate: "2015-08-06", ... }, // { id: 9, title: "Default Function Parameters in ES6", displayDate: "2015-07-06", ... }, // { id: 10, title: "Use More Parenthesis!", displayDate: "2015-08-26", ... }, // ]
在过去的30天里,我们现在有了一个新的文章列表。看来我们已经满足了第一个需求,并且有了一个良好的开端。随着我们的继续,我们将把函数式实用工具函数放在一个可以重用的库中。
获取源代码:您可以看到这篇文章中 所写的源代码 ,在单独的functional.js文件中包含了我们所有的函数式实用工具函数,在app.js文件中包含了我们的主应用程序的逻辑。我们将后续的本系列的翁中添加这些代码。
小结
我们已经发现了一些函数式编程中的关键技术,如 Currying(柯里化) 和 Partial Application(偏函数应用) 以及可以使用它们的上下文。我们还发现,专注于构建小而有用的行数,与函数式技术相结合,可以合成高阶函数,并实现更好的重用。有了这些基础,接下来的两篇文章看起来就不那么令人生畏了。
哈尔滨品用软件有限公司致力于为哈尔滨的中小企业制作大气、美观的优秀网站,并且能够搭建符合百度排名规范的网站基底,使您的网站无需额外费用,即可稳步提升排名至首页。欢迎体验最佳的哈尔滨网站建设。