在阅读关于 Currying(柯里化) , Partial Application(偏函数应用) 和其他函数式编程技术之后,一些开发人员不知道应该什么时候使用这些方法;为什么要这样使用?
在接下来的三篇系列文章中,我们将尝试解决这个问题,我们会尝试,并向你展示如何在一个短小而现实的例子中用函数式编程的方式,解决这个问题。
在我们深入之前,让我们花一点时间来回顾一下一些实用的函数式编程概念。
函数式编程把 "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"
- },
- // ...
- ];
我们的需求:现在,假设我们想要显示最近的文章(不超过一个月),按标签分组,按发布日期排序。让我们思考一下我们需要做些什么
对文章进行分组(这可能意味着如果他们有多个标签,则会显示在两个分组中。)
- 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]
哇,可以!最初似乎是很多的工作; 但是我们从这个方法中得出的结论是:
,可以在许多情况下使用它来创建更具体的列表过滤器
- filterWith()
的同时,不使其立即对数据列表执行操作
- Array.prototype.filter
我们的
函数需要一个 predicate 断言函数,当给定列表中的某个元素时,它返回
- filterWith()
或
- 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
) 函数传递每个参数。如果在调用时比转换函数有更多的参数,那么剩下的参数将会以 "as is" 的形式传递。
- txnfn
让我们用一个小例子来帮助解释这个定义。简单地说,
让我们执行以下操作:
- 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)
我们可以使用
来修改现有的 predicate 断言函数来在对象类型上操作,而不是数值。首先,让我们再次使用 currying(柯里化) 来创建一个函数,该函数允许我们创建 偏应用的函数,这些函数可以通过属性名访问对象。
- useWith()
- // 用于访问对象属性的函数
- function get(obj, prop) { return obj[prop]; }
- // `get()` 的柯里化版本
- var getWith = rightCurry(get);
现在我们可以使用
作为变换函数,从每个对象获取
- getWith()
日期,传递给用于过滤器 (filter) 的一元断言函数。
- .published
- // 我们修改后的断言函数可以在
- // 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(偏函数应用) 以及可以使用它们的上下文。我们还发现,专注于构建小而有用的行数,与函数式技术相结合,可以合成高阶函数,并实现更好的重用。有了这些基础,接下来的两篇文章看起来就不那么令人生畏了。
在本系列的下一篇文章中,我们将结合到目前为止所进行的过滤,使用标签名称对文章数据进行分组,在这里我们将介绍更多与列表相关的函数和更灵活的函数合成。
JavaScript 函数式编程系列文章
来源: http://www.css88.com/archives/7794