[译]A Gentle Introduction to Functional Javascript part 4

本人声明

  1. 本栏仅为归档自己看到的优秀文章; 
  2. 文章版权归原作者所有; 
  3. 因为个人水平有限,翻译难免有错误,请多多包涵。
  4. 第一部分已被翻译,详见Functional Javascript教程(一),全系列共四篇。

原文地址

https://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-style/

文章正文

    这是JavaScript函数式编程入门四部分的第四篇。在上一篇文章我们见识到了高阶函数——返回函数的函数。这篇文章我们将讨论怎样以函数式编程使用它们。

函数式编程

    再上一篇文章我们见识了partial,compose,curry和pipe以及怎样使用它们把简单的函数串联在一起形成更复杂的函数。但是这么做有什么用?当我们已经能写出十分正确的代码时这是否值得我们去学习?

    一个理由是有更多完成工作的可用工具总是很有用的——只要你知道怎么使用它们——函数式编程确实为JavaScript带来了一套非常有用的工具。但是我认为理由远不止此。函数式编程打开了一种不同的编程风格。这将使我们以一种不同的方法抽象问题及其解决办法。

    函数式编程有两个关键:

1.使用纯函数,如果你想试一试函数式编程这很重要。

2.Pointfree编程风格,这不算重要但是值得理解。

纯函数

    如果你去了解函数式编程,肯定会遇到纯函数和非纯函数的概念。纯函数要满足一下两个标准:

1.以同样参数调用函数返回一样的结果。

2.调用函数不产生副作用:没有网络调用,没有文件读写,没有数据库查询,没有DOM元素修改,没有全局变量修改,也没有控制台输出,都不行。

    非纯函数让函数式程序员很不舒服以至于他们会尽可能避免非纯函数。不过问题是现代计算机程序的重点都是副作用。调用网络连接和渲染DOM元素都是web应用的核心,也是JavaScript被创造的原因。

    这时候一个有抱负的函数式程序员会怎么做?嗯i解决办法就是我们不完全避免非纯函数,但是把他们推迟到必须处理的时候才处理。在尝试写代码之前我们要想出一个清晰经得起推敲的方案。正如Eric Elliot在 The Dao of Immutability所言:

Logic is thought. Effects are action. Therefore the wise think before acting, and act only when the thinking is done.

If you try to perform effects and logic at the same time, you may create hidden side effects which cause bugs in the logic. Keep functions small. Do one thing at a time, and do it well.

三思而后行。

     换句话说,用函数式编程,在做任何有潜在副作用的工作时,我们通常会试着想出要达成目的的逻辑。

     另一种比喻是,这就好像是使用机枪和来福的不同。使用机枪你要不断射击,这样你最终会击中某个目标。但是可能会击中你不是你想要的目标。用来福的话就很不一样,你选择最有可能的点,和枪两点成线,并考虑风速和目标的距离。耐心,有方法,仔细地准备好在对的时间扣动扳机。更少的子弹,但是更准确的击中目标。

    所以怎样让我们的函数更纯?来看个例子:

var myGobalMessage = '{{verb}} me';

var impureInstruction = function(verb){
    return myGlobalMessage.replace('{{verb}}', verb);
}

var eatMe = impureInstruction('Eat');
//=> 'Eat me'

var drinkMe = impureInstruction('Drink');
//=> 'Drink me'

    这个函数不纯因为它依赖一个全局变量myGlobalMessage。如果这个变量改变的话,就很难辨别impureInstruction做了什么。所以一个使它变纯的方法是把它放进函数中:

var pureInstrction = function(verb){
    var message = '{{verb}} me';
    return message.replace('{{verb}}', verb);
}

    现在给定同样的参数这个函数总是返回相同的结果。但是有时候我们用不了这种方法。举个例子:

var getHTMLImpure = function(id){
    var el = document.getElementById(id);
    return el.innerHTML;
}

    这个函数不纯因为它依赖document对象得到DOM。如果DOM变化了它可能会产生不同的结果。现在,我们确定函数里面的document因为它只是一个浏览器的API,但是我们可以把它作为参数传给函数:

var getHTML = function(doc, id){
    var el = doc.getElementById(id);
    return el.innerHTML;
}

这可能看上去有点繁琐无用,但是这是个很方便的方法。假设你正准备单元测试这个函数。通常我们必须准备某个浏览器获得document对象来测试它。但是因为我们把doc作为一个参数传递,就可以简单的传入一个根存对象。

var stubDoc = {
    getElementById: function(id){
        if(id === 'jabberwocky'){
            return {
                innerHTML: '<p>Twas brillig...'
            };
        }
    }
};

assert.equal(getHTML('javverwocky'), '<p>Twas brillig...');

    写出这样的根存对象看上去有有点复杂,但是我们现在不用浏览器就可以测试这个函数。只有我们愿意,我们就可以在命令行中运行它而不用配置一个无头浏览器(无界面浏览器)。还有一个额外好处就是这会比在一个完整document对象环境中快很多倍。

    另一个使函数变纯的方法是使其返回另一个函数,调用这个返回函数会最终完成那些不纯的步骤。这听上去就像一个脏技巧,但是这完全合法。举个例子:

var htmlGetter = function(id){
    return function(){
        var el = document.getElementById(id);
        return el.innerHTML;
    };
}

    htmlGetter函数是纯函数因为它不会访问任何全局变量而且它总是返回同样的函数。

    这么做对单元测试没什么用,这样没有完全移除非纯部分而是推迟非纯部分。这不一定是一件坏事。记住,在处理副作用前,我们要首先使用纯函数理清逻辑。

Pointfree

    Pointfree或者隐性编程,高阶函数像curry和compose使之成为可能。为了方便解释,我们再看一看上一文中的诗的例子:

var poem = 'Twas brillig, and the slithy toves\n' + 
    'Did gyre and gimble in the wabe;\n' +
    'All mimsy were the borogoves,\n' +
    'And the mome raths outgrabe.';

var replace = curry(function(find, replacement, str) {
    var regex = new RegExp(find, 'g');
    return str.replace(regex, replacement);
});

var wrapWith = curry(function(tag, str) {
    return '<' + tag + '>' + str + '</' + tag + '>'; 
});

var addBreaks      = replace('\n', '<br/>\n');
var replaceBrillig = replace('brillig', wrapWith('em', 'four o’clock in the afternoon'));
var wrapP          = wrapWith('p');
var wrapBlockquote = wrapWith('blockquote');

var modifyPoem = compose(wrapBlockquote, wrapP, addBreaks, replaceBrillig);

    主意compose期望每个函数只接受一个参数。所以我们使用curry让多参数函数replace和wrapWith编程单参数函数。同时注意我们对于函数顺序的熟虑。比如wrapWith读入tag作为它的第一个参数而不是他要包裹的字符串。如果我们像这样仔细的安排我们的函数就会使得通过组合创建函数更容易。

    实际上这就简单到我们可以用这种方法写所有的代码。但是注意一点副作用:当我们定义最终的modifyPoem函数时,我们没有在任何地方提到它接受一个单个的字符串参数。如果你仔细观察这些柯里化函数addBreaks,replaceBrillig,wrap和wrapBlockquote就会发现也都没有提到他们只接受一个字符串变量。这就是pointfree编程:以一套基础实用工具函数开始(像是Ramda或者functional.js)并以一种你绝对不会提及输入变量的方式写代码。

   这又能带给我们什么?仅依据代码本身这并没有什么特别的地方。pointfree风格聪明的地方是它迫使你使用compose,curry和pipe等函数。这样就会大大鼓励你继续以灵活的方式把小而简单的函数串联在一起。换句话说,这是一种自愿强加的限制,就好象俳句或者十四行诗一样。不是所有的诗都必须以这种方式写——遵循这些规则并不保证是首好诗——但是一些以这些方式写的诗非常的有文采。

   任何代码都以pointfree风格并不总是很实用。有时候,这会对简单函数增加不必要的复杂。但是,尝试一下和试着用pointfree写你的所有函数是理解函数式编程的很好的方法。

Hindley-Milner类型签名

    一旦你习惯了pointfree风格就会有一个问题,怎么和其它程序员交流你的函数应该用那种类型的参数。为了解决这个问题,函数式程序员开发了一种特殊的记号来指定一个函数接受什么类型的参数以及它返回什么。这种记号被称为Hindley-Milner类型签名。我们在函数定义的地方以注释写出。来看看例子:

// instruction :: String -> String
var instruction = function(verb){
    return verb + ' me';
}

    这个签名指明了instruction读入一个String作为输入并返回另一个String。目前看来还不错。但是如果我们的函数有两个参数怎么办?

// wrapWith :: String -> (String -> String)
var wrapWith = curry(function(tag, str){
    return '<' + tag + '>' + str + '</' + tag + '>';
});

    这复杂了一点,但也没有很难。这个签名说明wrapWith函数读入一个String并返回一个函数,这个返回函数接受一个String并返回一个String。注意这是正确的是因为我们柯里化了那个函数。当我们使用这种风格时,默认你总是柯里化所有函数。

    那三个参数的函数怎么办?一种方法是这样写:

//replace :: String -> (String -> (String -> String))
var replace = curry(function(find, replacement, str){
    var regex = new RegExp(find, 'g');
    return str.replace(regex, replacement);
});

    现在我们有一个返回 返回 返回string的函数的函数的函数。这依然讲的通,但是我们总是所有都是柯里化的,所以我们倾向于去掉括号:

// replace :: String -> String -> String -> String

    如果我们有一个不同类型的输入参数怎么办?

//formatDollars :: Number -> String
vaar formatDollars = replace('${{number}}', '{{number}}');

formatDollars(100);
// => $100

    这是个pointfree函数,现在为什么签名类型很有用变得很清晰了。这个函数读入一个数字(number)返回一个字符串(string)。

    假如我们有一个数组怎么办?

// sum :: [Number] -> Number
var sum = reduce(add, 0);

    这个函数读入一组数字(number)并返回一个数字(number)(假设我们从第二篇文章中柯里化了我们的reduce函数)。

    最后两个例子:

//identity :: a -> a
var identity = function(x) { return x };

//map :: (a->b) -> [a] -> [b]
var map = curry(function(callback, array){
    return array.map(callback);
});

    上述identity函数读入任何类型并返回一个相同类型的变量。另一方面map函数读入一个接受a类型并返回b类型的函数。然后接受一组a类型的数组并返回一组b类型的数组。

    你可能会发现一些库比如Ramda对采用这种记号记录库中的所有函数。

继续深入

    我们仅仅见识到了函数式编程的表面。但是理解一等函数(函数和变量一样),偏函数应用和组合带给了我们基本的构建块的能力并加深了这种能力。如果你有兴趣深入阅读,下面这有一些有用的资源:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值