准备充分了嘛就想学函数式编程?(第四部分)

本文讲的是准备充分了嘛就想学函数式编程?(第四部分),


想要理解函数式编程,第一步总是最重要,也是最困难的。但是只要有了正确的思维,其实也不是太难。

之前的部分: 第一部分第二部分第三部分

柯里化

如果你还记得第三部分内容的话,就会知道我们在组合 mult5 和 add 这两个函数时遇到问题的原因是:mult5 接收一个参数而 add 却接收两个。

其实只需要通过限制所有函数都只接收一个参数,就可以轻易地解决这个问题。

相信我,这并没有听起来那么糟糕。

我们只需要来写一个使用两个参数,但一次只接收一个参数的 add 函数。柯里函数允许我们这样做。

柯里函数是一种一次只接收单个参数的函数。

这就可以让我们在将 add 和 mult5 组合之前只传递第一个参数给 add。然后当调用(组合后的) mult5AfterAdd10 函数时,add 函数就将得到第二个参数。

在 JavaScript 里,可以通过改写 add 函数来实现这个功能:

var add = x => y => x + y

这个版本的 add 函数现在就只接收一个参数,之后再接收另外一个参数。。

详细来讲,这个 add 函数接收单参数 x,然后返回一个接收单参数 y 的函数,而这个函数最终就会返回 x + y 的结果。

现在我们就可以使用这个版本的 add 函数来构建一个 mult5AfterAdd10 函数的可运行版本:

var compose = (f, g) => x => f(g(x));
var mult5AfterAdd10 = compose(mult5, add(10));

这个组合函数接收两个参数,f 和 g,然后它返回一个接收单参数 x 的函数,这个函数在调用的时候就会将 g 函数作用于 x,然后再将 f 函数作用于上一步的结果。

实际上我们到底做了什么呢?好吧,我们其实是将旧的 add 函数进行了柯里化。这么做就让 add 函数变得更加灵活,因为我们可以先把10作为第一个参数传入,而最后的参数则可以在 mult5AfterAdd10 被调用的时候传入。

看到这里,你可能会想知道在 Elm 里怎么来改写这个 add 函数。答案是,不需要改写。在 Elm 和其他函数式(编程)语言里,所有的函数都会自动柯里化。

所以这个 add 函数看起来和之前是一样的:

add x y =
    x + y

mult5AfterAdd10 函数曾经在第三部分怎么写,也还是一样:

mult5AfterAdd10 =
    (mult5 << add 10)

语法上讲,Elm 其实打败了像 JavaScript 这样的命令式(编程)语言,因为它在函数式方面是做了优化的,就像柯里化和组合函数。

柯里化和重构

柯里化在重构的的时候也能发挥它闪亮的一面,当我们创建一个多参数通用版本的函数时,我们可以通过柯里化的方法用它来创建接收更少参数的特定版本的函数。

举个例子,当我们有下面两个方法,在一个字符串前后分别添加一对大括号和两对大括号。

bracket str =
    "{" ++ str ++ "}"

doubleBracket str =
    "{{" ++ str ++ "}}"

下面是如何使用它们:

bracketedJoe =
    bracket "Joe"

doubleBracketedJoe =
    doubleBracket "Joe"

我们可以通用化 bracket 和 doubleBracket 函数:

generalBracket prefix str suffix =
    prefix ++ str ++ suffix

但现在每当我们使用 generalBracket 时,都必须传入大括号:

bracketedJoe =
    generalBracket "{" "Joe" "}"

doubleBracketedJoe =
    generalBracket "{{" "Joe" "}}"

我们实际上想要的是两全其美。

如果我们重新对 generalBracket 函数的参数进行排序,就可以创建柯里化后的 bracket 和 doubleBracket 函数了。

generalBracket prefix suffix str =
    prefix ++ str ++ suffix

bracket =
    generalBracket "{" "}"

doubleBracket =
    generalBracket "{{" "}}"

注意到通常将静态参数放到前面,如 prefix 和 suffix,而可变参数尽量放到最后,如 str,这样,就可以简单地创建出generalBracket 函数的特定版本了。

参数顺序对全面柯里化来说非常重要。

还注意到 bracket 和 doubleBracket 函数都是免参数(point-free)写法,如 str 参数是隐式表明的。bracket 和doubleBracket 函数都在等待最后参数的传入。

现在就可以像之前那样使用了:

bracketedJoe =
    bracket "Joe"

doubleBracketedJoe =
    doubleBracket "Joe"

但这次我们使用的是通用化的柯里函数:generalBracket

常用的功能函数

让我们来看三个函数式(编程)语言里的常用函数。

但首先,来看看下面的 JavaScript 代码:

for (var i = 0; i < something.length; ++i) {
    // do stuff
}

这段代码有一个主要的错误,但并不是 bug。问题在于这个代码是一个模板代码,就是那些一遍又一遍重复写的代码。

如果你是使用像 Java、C#、JavaScript、PHP 和 Python 等这样的命令式(编程)语言。你就会发现相比其他语言你会写更多这样的模板代码。

这就是这段代码的问题所在。

所以让我们来解决它。将它放到一个函数里(或者几个函数),然后再也不写 for 循环了。好吧,几乎不写,至少直到我们移步使用一个函数式(编程)语言。

首先从修改一个 things 数组来开始:

var things = [1, 2, 3, 4];
for (var i = 0; i < things.length; ++i) {
    things[i] = things[i] * 10; // MUTATION ALERT !!!!
}
console.log(things); // [10, 20, 30, 40]

呃!!又是变量!

再试一次,这次不再去更改 things 数组了:

var things = [1, 2, 3, 4];
var newThings = [];

for (var i = 0; i < things.length; ++i) {
    newThings[i] = things[i] * 10;
}
console.log(newThings); // [10, 20, 30, 40]

好了,我们没有更改 things 数组但技术上来说我们更改了 newThings 数组。目前为止,我们将忽略这个问题。毕竟我们在使用 JavaScript,一旦我们移步使用一个函数式语言,就不可以更改了。

这里的重点是弄明白这些函数是怎么工作的,以及它们怎么来帮助我们减少代码噪音(冗余等)。

来把这段代码放到一个函数里。接下来将调用我们第一个常用函数 map,它会将旧数组里的每个值映射成新值放到一个新的数组里。

var map = (f, array) => {
    var newArray = [];

    for (var i = 0; i < array.length; ++i) {
        newArray[i] = f(array[i]);
    }
    return newArray;
};

注意到 f 函数,它作为参数传入,这样就可以让 map 函数对数组里的每一项进行任何我们想要的操作。

现在我们就可以使用 map 来改写之前的代码了:

var things = [1, 2, 3, 4];
var newThings = map(v => v * 10, things);

看看,没有 for 循环,而且更简单易读,这就是(关于之前的代码错误)原因。

好吧,技术上来说,map 函数里是有 for 循环的,但至少我们不必再写一大堆模板代码了。

现在来写另外一个常用函数,从一个数组当中过滤一些数据:

var filter = (pred, array) => {
    var newArray = [];

for (var i = 0; i  x % 2 !== 0;
var numbers = [1, 2, 3, 4, 5];
var oddNumbers = filter(isOdd, numbers);
console.log(oddNumbers); // [1, 3, 5]

使用新的 filter 函数比用 for 循环来手写实现简单太多了。

最后一个常用函数叫做 reduce。一般来说,它用来接收一个列表并将其减少到一个值,但实际上可以用它做更多的事情。

在函数式(编程)语言里这个函数通常叫做 fold

var reduce = (f, start, array) => {
    var acc = start;
    for (var i = 0; i < array.length; ++i)
        acc = f(array[i], acc); // f() takes 2 parameters
    return acc;
});

这个 reduce 函数接收一个(自定义)减少函数 f、一个初始 start 开始值和一个 array 数组。

注意到这个减少函数 f,接收两个参数,array 数组的当前项,以及累计器 acc。每次迭代,它都将使用这两个参数产生一个新的累计器,最后一次迭代得到的累计器将会被返回。

一个例子将帮助我们更好地来理解它如何工作:

var add = (x, y) => x + y;
var values = [1, 2, 3, 4, 5];
var sumOfValues = reduce(add, 0, values);
console.log(sumOfValues); // 15

注意到 add 函数接收两个参数并把它们相加。而 reduce 函数正是期望一个接收两个参数的函数,所以它们可以一起正常运行。

我们将初始 start 值设为0,并将 values 数组传入进行计算。reduce 函数内部,values 数组各项的总值作为累计器循环计算。最后的累计值返回为 sumOfValues

每个这些函数,mapfilter 和 reduce,都让我们可以在不必写 for 循环的情况下对数组进行常用操作。

但是在函数式(编程)语言里,它们甚至更有用,因为没有循环体只有递归。迭代函数不只是非常有用,它们是必要的。

我的脑子!!!

目前为止足够了.

在这个系列文章的随后部分,我将谈到有关引用完整性、执行顺序、类型以及其他更多的东西。





原文发布时间为:2016年11月17日


本文来自合作伙伴掘金,了解相关信息可以关注掘金网站。

时间: 2024-11-03 15:46:40

准备充分了嘛就想学函数式编程?(第四部分)的相关文章

准备充分了嘛就想学函数式编程?(第一部分)

本文讲的是准备充分了嘛就想学函数式编程?(第一部分), 迈出理解函数式编程概念的第一步是最重要的,有时也是最难的一步.但是不一定特别难.只要选对了思考方法就不难. 学开车 第一次学车时,我们也曾挣扎过.看别人学开车时觉得真的很简单.但事实上学车比我们想象的难多了. 我们借父母的车子练习,在家周围街道上开熟练之前甚至都不敢冒险开到公路上去. 但是通过不断的练习,在经历过一些父母想忘掉的担心令人的经历之后,我们学会了开车,最终拿到了驾照. 拿到驾照之后我们一有机会就会把车开出去.每次出行都会让我们的

准备充分了嘛就想学函数式编程?(第五部分)

本文讲的是准备充分了嘛就想学函数式编程?(第五部分), 迈出理解函数式编程概念的第一步是最重要的,有时也是最难的一步.但是不一定特别难.只要选对了思考方法就不难. 前几部分: 第一部分, 第二部分, 第三部分, 第四部分 引用透明 引用透明 是一个很酷炫的术语,它指的是一个纯函数能够安全地被它的表达式所替代.下面用一个例子来解释这个术语. 在代数中当你有以下这个公式时: y = x + 10 并且已知: x = 3 你可以将 x 代入方程来得到: y = 3 + 10 此时这个方程依旧成立.我们

准备充分了嘛就想学函数式编程?(Part 2)

本文讲的是准备充分了嘛就想学函数式编程?(Part 2), 想要理解函数式编程,第一步总是最重要,也是最困难的.但是只要有了正确的思维,其实也不是太难. 之前的部分: 第一部分 友情提示 请读仔细读代码,确保继续之前你已经理解.每一代码段落都基于它之前的代码. 如果你太急,可能会遗漏一些重要的细节. 重构 让我们先来重构一段 JavaScript 代码: function validateSsn(ssn) { if (/^\d{3}-\d{2}-\d{4}$/.exec(ssn)) consol

准备充分了嘛就想学函数式编程?(Part 6)

本文讲的是准备充分了嘛就想学函数式编程?(Part 6), 第一步,理解函数式编程概念是最重要的一步,同时也是最难的一步.如果你从正确的角度或方法来理解的话,它也未必会有那么难. 回顾之前的部分: Part 1, Part 2, Part 3, Part 4, Part 5 现在该做什么? 现在你已经学会了所有这些新东西了,你可能在想,"现在该干什么?我如何在日常编程中使用它?" 这得看情况.如果你会使用纯函数式语言(如 Elm 或 Haskell)编程,那么你可以尝试所有这些想法.这

想学jsp web编程,各位大虾有好意见么

问题描述 想学jspweb编程,各位大虾有好意见么 解决方案 解决方案二:多动手敲代码,多做项目解决方案三:做两个jsp+servlet+JavaBean的网站.熟悉mvc框架的结构.看下jsp,servlet相关书籍,struts,spring,hibernate相关书籍解决方案四:从jspservletjdbc整起解决方案五:我是学java方面的,jspweb我觉得是需要发一点时间的,现在学的话都是关于SSH编程的.建议下点视频看看.解决方案六:LZ基础是主要...先抓起基础.然后在学习框架

Python函数式编程指南(四):生成器详解

  这篇文章主要介绍了Python函数式编程指南(四):生成器详解,本文讲解了生成器简介.生成器函数.生成器函数的FAQ等内容,需要的朋友可以参考下 4. 生成器(generator) 4.1. 生成器简介 首先请确信,生成器就是一种迭代器.生成器拥有next方法并且行为与迭代器完全相同,这意味着生成器也可以用于Python的for循环中.另外,对于生成器的特殊语法支持使得编写一个生成器比自定义一个常规的迭代器要简单不少,所以生成器也是最常用到的特性之一. 从Python 2.5开始,[PEP

《JavaScript函数式编程》读后感_javascript技巧

本文章记录本人在学习 函数式 中理解到的一些东西,加深记忆和并且整理记录下来,方便之后的复习. 在近期看到了<JavaScript函数式编程>这本书预售的时候就定了下来.主要目的是个人目前还是不理解什么是函数式编程.在自己学习的过程中一直听到身边的人说面向过程编程和面向对象编程,而函数式就非常少.为了自己不要落后于其他同学的脚步,故想以写笔记的方式去分享和记录自己阅读中所汲取的知识. js 和函数式编程 书中用了一句简单的话来回答了什么是函数式编程: 函数式编程通过使用函数来将值转换为抽象单元

我想学计算机-想学计算机!从根本学起

问题描述 想学计算机!从根本学起 谁知道有什么书介绍了计算机的起源及发展和原理,还有汇编语言,c语言的原理 解决方案 要想学计算机,关键是要有一个系统的过程.大家都知道,计算机是美国人发明的,所以要学计算机需要看原版的高质量的书籍.看了不对的书,就要走冤枉路.像楼下的书,难度就比较大,不适合lz.姐姐有一些很好的入门的书,介绍各种原理的.都是金针度人的好书.lz如果采纳了姐姐的回答(方法是点击姐姐回答右边的采纳按钮),姐姐发给你.祝你好运. 解决方案二: 要想学计算机,关键是要系统的学习.1.硬

学做界面#-想学做界面的信息安全专业的会敲代码的色影丝小学渣

问题描述 想学做界面的信息安全专业的会敲代码的色影丝小学渣 自身具备的艺术素养对做出优质的界面有助推作用吗?我对别人做的界面的构图位置美观吧啦吧啦很敏感,脑中会形成一个自己感觉更舒服的界面版式,这对做出优质的界面有助推作用吗,还是小学生胡思乱想了,第一次提问,求指示,轻喷. 解决方案 有艺术细胞是好事.但是如果过于强调这个,而忽略了系统的学习.理性和理论,那么是没什么好处的. 一个靠直觉和自发得到的经验而进行界面设计的人,可能你能设计用户群体和你本人有着相同背景的简单的小软件. 但是一个前端交互