彩世界开奖app官网-彩世界平台官方网址(彩票平台)
做最好的网站
来自 前端技术 2019-11-28 11:54 的文章
当前位置: 彩世界开奖app官网 > 前端技术 > 正文

初入函数式编程

pipeline

compose的区别就是换个方向,compose用的是reduceRightpipeline用的是reduce

const pipeline = (...fns) => x => fns.reduce((acc, cur) => cur(acc), x);
const amaze = pipeline(holify, exclaim, upperCase)
console.log(amaze('functional programing'));

实现循环

命令式:

for (var i = 0; i < arr.length; i  ) {
    // ...
}

这个太常见了, 也太简单了,第一天学编程就是这个玩意儿吧.

题外话: 关于这个例子 var i = 0 部分在 javascript 这个 var 写了和没写没啥区别.
使用 ES2015 改成 let 吧!

好! 来直接看 函数式 怎么写:

const two_steps = step1 => step2 => param =>
  step2(step1(param))

const loop_on_array = arr => body => i =>
    i < arr.length
    ? two_steps(body)(_ => loop_on_array(arr)(body)(i   1))(arr[i])
    : undefined

别灰心,我第一次看也看不懂.

看个过度版本的例子:

function loop_on_array(arr, body, i) {
    if (i < arr.length) {
        body(arr[i])
        loop_on_array(arr, body, i   1)
    }
}

这个都能看懂, 就是回调自己, 递归嘛.

但是根据 之前的 禁令, 貌似写了的都违背了.

回到之前的函数式写法, 再复制一遍

const two_steps = step1 => step2 => param =>
  step2(step1(param))

const loop_on_array = arr => body => i =>
  i < arr.length
  ? two_steps (body)(_ => loop_on_array(arr)(body)(i   1))(arr[i])
  : undefined

现在是不是稍微能理解一点了,
就是用三目运算符代替 if/else
写了个 two_step 用作递归.

嗯,没错 two_step 就是用来递归的, 看懂这里你就可以 × 掉本页面了!

再来看下如何使用这个函数式的循环

loop_on_array([1, 2, 3, 4, 5])(item => console.log(item))(0)

对的, 没错我们写了个蛋疼的 0 (暂时先别管这个)

咱们遍历这个数组的时候, 第一个curry参数放的数组, 第二个放的一个函数, 第三个是个蛋疼 0
好大家简单做个映射, 等下要用的.

arr  = [1,2,3,4,5]
body = [item => console.log]
i    = 0

啥是curry化? 简而言之就是多个参数变成一个参数的写法, 相关的lambda演算.

上面参数的顺序可以通过调整对应 curry 参数位置调整的, 因为函数式不是按顺序执行的

咱反过来推倒, 这个例子的效果其实很简单, 就是打印数组里的所有内容.

  • 0 必然是作为第一次使用的下标值
  • item => console.log(item) 接受的参数 item 也肯定是[1,2,3,4,5]里的单个元素
  • 怎么样使它回调自己呢? 那就是使用 two_steps 方法了

好看看在 loop_on_array 中是如何使用 two_steps

回想下之前的映射关系.

two_steps (body)(_ => loop_on_array(arr)(body)(i   1))(arr[i])

那么上式中:

arr[i] = arr[0] = 1
body   = item => console.log(item)

啥都不管, 我貌似看到可以先打印出一个 1, 只要吧 arr[0] 当作变量传给 body 就可以了
打印完第一个, 我肯定要接着打第二个,
这么看来使用 loop_on_array([1, 2, 3, 4, 5])(item => console.log(item))(0)
就打印了下标为 0arr 数组
那么 使用 loop_on_array([1, 2, 3, 4, 5])(item => console.log(item))(1)
就可以打印下标为 1arr 数组了

(_ => loop_on_array(arr)(body)(i 1)) 这里不就是这么用的吗.

等等, 在 loop_on_array 的三目表达式中 只要 i < arr.length 就会执行 two_steps
也就是说 这会递归调用自己.

这时候看 two_steps 就能看懂了吧.

const two_steps = step1 => step2 => param => step2(step1(param))

two_steps 接受3个 curry化参数:

  1. step1 也就是第一次要执行的方法, 例子中的 item => console.log(item)
  2. step2 也就是递归调用的, _ => loop_on_array(arr)(body)(i 1)的写法也能看出他根本不在乎传入的参数, 他只是用来回调 loop_on_array的 所以使用了_来忽略传入参数
  3. paramsstep1 中是当前的下标, 1 之后就会是step2parmas
// 这里的3个参数顺序调换对应的只要吧 `step2(step1(param))` 就可以了
// 比如我写成 `const two_steps = step1 => param => step2 => step2(step1(param))`
? two_steps(body)(_ => loop_on_array(arr)(body)(i   1))(arr[i]) 
// 改成
? two_steps(body)(arr[i])(_ => loop_on_array(arr)(body)(i   1))
// (body)(arr[i])这样放近一点我更容易理解 arr[i] 是作为 body 的参数
// 然后再回调 (_ => loop_on_array(arr)(body)(i   1)) 这个方法

刚了解函数式可能觉得, OMG 写个破循环这么费劲.
是的, 我也是这么认为的...
那是因为咱们还没见识到她的魅力~

关于这个蛋疼的0其实很好解决:

const new_loop = arr => body => loop_on_array(arr)(body)(0)

能看出一点他的魅力吗?

 

WHY? 函数式有什么好处?

  • 易写易读 聚焦重要逻辑,摆脱例如循环之类的底层工作
  • 易复用 面向对象可复用的单位是类,函数式可复用的是函数,更小更灵活
  • 易测 纯函数【后面会讲】不依赖外部环境,测试起来准备工作少
  • 看起来很厉害 被人夸奖能增强信心和动力,所以这点也很重要

前端正在革命, 我们不要做切图仔!

此文写给不愿意做切图仔的你我.
参考: http://web.jobbole.com/84882/

  • 比如函数作为一等公民,能够被赋值,被传递
  • 支持闭包(Closure)
  • 支持 Currying

small pure function

纯函数有两点要求:

  1. 相同的传参,返回值一定相同
  2. 函数调用不会对外界造成影响,如不会修改外部对象

看个例子

let name = 'apolis';
const greet = () => console.log('Hello '   name);

greet();
name = 'kzhang';
greet();

greet函数依赖外部变量name,相同的传参【都不传参也算相同的传参】屏幕输出的内容却不一样,所以它不纯,鉴定完毕。

const greet = name => console.log('Hello '   name);

这样就好多了,不受外部变量的影响了。

不过更严格的认为,调用这个函数造影响了控制台console,所以还不算纯。

const greet = name => 'Hello '   name;

这样才够纯,同时greet也摆脱了对控制台的依赖,可以适用的范围更广了。

我们要学会把纯的留给自己,把不纯的甩给别人......咳咳,关在函数外面。

由于它的纯,同样的传参,返回值一定相同。
我们可以把算过的结果保存下来,下次调用传的参数发现算过了,直接返回之前计算的结果,提升效率。

const memorize = fn => {
    let cache = {};
    return x => {
        if (cache.hasOwnProperty(x)) return cache[x];
        else {
            const result = fn(x);
            cache[x] = result;
            return result;
        }
    }
};

利用上面的工具函数,我们可以缓存纯函数的计算结果,三板斧的例子filter改一下就可以了。

const sum = arr.filter(memorize(isPrimeNumber))
    .map(x => x * x)
    .reduce((acc, cur) => acc   cur, 0);

console.log(sum);

如果数组中包含重复元素,这样就能减少计算次数了。
命令式写法要达到这个效果,改动就大的多了。

真·函数式编程 学习函数式编程的学习笔记

  • 禁用var/let,所有东西都用const定义,也就是说无变量,强制immutable。
  • 禁用分号,也就是不让“顺序执行”,解除过程式编程范式
  • 禁用if/else,但允许用条件表达式condition ? expr1 : expr2。
  • 禁用for/while/do-while。
  • 禁用prototype和this来解除JS中的面向对象编程范式
  • 禁用function和return关键字,仅用lambda表达式来编程(JS里叫箭头函数)。
  • 禁用多参数函数,只允许使用单个参数,相当于强行curry化

根据作者的提示, 先自宫一刀!
啊啊啊~ 好疼~
*不过蛮爽的, 嘿嘿嘿!*

ES 基本的箭头函数啥的略过

一、引言

说到函数式编程,大家可能第一印象都是学院派的那些晦涩难懂的代码,充满了一大堆抽象的不知所云的符号,似乎只有大学里的计算机教授才会使用这些东西。在曾经的某个时代可能确实如此,但是近年来随着技术的发展,函数式编程已经在实际生产中发挥巨大的作用了,越来越多的语言开始加入闭包,匿名函数等非常典型的函数式编程的特性,从某种程度上来讲,函数式编程正在逐步“同化”命令式编程。

JavaScript 作为一种典型的多范式编程语言,这两年随着React的火热,函数式编程的概念也开始流行起来,RxJS、cycleJS、lodashJS、underscoreJS等多种开源库都使用了函数式的特性。所以下面介绍一些函数式编程的知识和概念。

二、纯函数

如果你还记得一些初中的数学知识的话,函数 f 的概念就是,对于输入 x 产生一个输出 y

f(x)。这便是一种最简单的纯函数。纯函数的定义是,对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。

下面来举个栗子,比如在Javascript中对于数组的操作,有些是纯的,有些就不是纯的:

var arr = [1,2,3,4,5];


// Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的 // 可以,这很函数式 xs.slice(0,3); //=> [1,2,3] xs.slice(0,3); //=> [1,2,3]
// Array.splice是不纯的,它有副作用,对于固定的输入,输出不是固定的 // 这不函数式 xs.splice(0,3); //=> [1,2,3] xs.splice(0,3); //=> [4,5] xs.splice(0,3); //=> []

在函数式编程中,我们想要的是 slice 这样的纯函数,而不是 splice这种每次调用后都会把数据弄得一团乱的函数。

为什么函数式编程会排斥不纯的函数呢?下面再看一个例子:

//不纯的
var min = 18;
var checkage = age => age > min;


//纯的,这很函数式 var checkage = age => age > 18;

在不纯的版本中,checkage 这个函数的行为不仅取决于输入的参数 age,还取决于一个外部的变量 min,换句话说,这个函数的行为需要由外部的系统环境决定。对于大型系统来说,这种对于外部状态的依赖是造成系统复杂性大大提高的主要原因。

可以注意到,纯的 checkage 把关键数字 18 硬编码在函数内部,扩展性比较差,我们可以在后面的柯里化中看到如何用优雅的函数式解决这种问题。

纯函数不仅可以有效降低系统的复杂度,还有很多很棒的特性,比如可缓存性:

import _ from 'lodash';
var sin = _.memorize(x => Math.sin(x));


//第一次计算的时候会稍慢一点 var a = sin(1);
//第二次有了缓存,速度极快 var b = sin(1);

 

三、函数的柯里化

函数柯里化(curry)的定义很简单:传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

比如对于加法函数 var add = (x, y) => x y ,我们可以这样进行柯里化:

//比较容易读懂的ES5写法
var add = function(x){
    return function(y){
        return x   y
    }
}


//ES6写法,也是比较正统的函数式写法 var add = x => (y => x y);
//试试看 var add2 = add(2); var add200 = add(200);
add2(2); // =>4 add200(50); // =>250

对于加法这种极其简单的函数来说,柯里化并没有什么大用处。

还记得上面那个 checkage 的函数吗?我们可以这样柯里化它:

var checkage = min => (age => age > min);
var checkage18 = checkage(18);
checkage18(20);
// =>true

事实上柯里化是一种“预加载”函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的编写函数的方法:

import { curry } from 'lodash';


//首先柯里化两个纯函数 var match = curry((reg, str) => str.match(reg)); var filter = curry((f, arr) => arr.filter(f));
//判断字符串里有没有空格 var haveSpace = match(/s /g);
haveSpace("ffffffff"); //=>null
haveSpace("a b"); //=>[" "]
filter(haveSpace, ["abcdefg", "Hello World"]); //=>["Hello world"]

 

四、函数组合

学会了使用纯函数以及如何把它柯里化之后,我们会很容易写出这样的“包菜式”代码:

h(g(f(x)));

虽然这也是函数式的代码,但它依然存在某种意义上的“不优雅”。为了解决函数嵌套的问题,我们需要用到“函数组合”:

//两个函数的组合
var compose = function(f, g) {
    return function(x) {
        return f(g(x));
    };
};


//或者 var compose = (f, g) => (x => f(g(x)));
var add1 = x => x 1; var mul5 = x => x * 5;
compose(mul5, add1)(2); // =>15

我们定义的compose就像双面胶一样,可以把任何两个纯函数结合到一起。当然你也可以扩展出组合三个函数的“三面胶”,甚至“四面胶”“N面胶”。

这种灵活的组合可以让我们像拼积木一样来组合函数式的代码:

var first = arr => arr[0];
var reverse = arr => arr.reverse();


var last = compose(first, reverse);
last([1,2,3,4,5]); // =>5

 

五、Point Free

有了柯里化和函数组合的基础知识,下面介绍一下Point Free这种代码风格。

细心的话你可能会注意到,之前的代码中我们总是喜欢把一些对象自带的方法转化成纯函数:

var map = (f, arr) => arr.map(f);


var toUpperCase = word => word.toUpperCase();

这种做法是有原因的。

Point Free这种模式现在还暂且没有中文的翻译,有兴趣的话可以看看这里的英文解释:

https:// class="visible">en.wikipedia.org/wiki/T class="invisible">acit_programming

用中文解释的话大概就是,不要命名转瞬即逝的中间变量,比如:

//这不Piont free
var f = str => str.toUpperCase().split(' ');

这个函数中,我们使用了 str 作为我们的中间变量,但这个中间变量除了让代码变得长了一点以外是毫无意义的。下面改造一下这段代码:

var toUpperCase = word => word.toUpperCase();
var split = x => (str => str.split(x));


var f = compose(split(' '), toUpperCase);
f("abcd efgh"); // =>["ABCD", "EFGH"]

这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用。当然,为了在一些函数中写出Point Free的风格,在代码的其它地方必然是不那么Point Free的,这个地方需要自己取舍。

六、声明式与命令式代码

命令式代码的意思就是,我们通过编写一条又一条指令去让计算机执行一些动作,这其中一般都会涉及到很多繁杂的细节。

而声明式就要优雅很多了,我们通过写表达式的方式来声明我们想干什么,而不是通过一步一步的指示。

//命令式
var CEOs = [];
for(var i = 0; i < companies.length; i  ){
    CEOs.push(companies[i].CEO)
}


//声明式 var CEOs = companies.map(c => c.CEO);

命令式的写法要先实例化一个数组,然后再对 companies 数组进行for循环遍历,手动命名、判断、增加计数器,就好像你开了一辆零件全部暴露在外的汽车一样,虽然很机械朋克风,但这并不是优雅的程序员应该做的。

声明式的写法是一个表达式,如何进行计数器迭代,返回的数组如何收集,这些细节都隐藏了起来。它指明的是做什么,而不是怎么做。除了更加清晰和简洁之外,map 函数还可以进一步独立优化,甚至用解释器内置的速度极快的 map 函数,这么一来我们主要的业务代码就无须改动了。

函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。

相反,不纯的不函数式的代码会产生副作用或者依赖外部系统环境,使用它们的时候总是要考虑这些不干净的副作用。在复杂的系统中,这对于程序员的心智来说是极大的负担。

七、尾声

任何代码都是要有实际用处才有意义,对于JS来说也是如此。然而现实的编程世界显然不如范例中的函数式世界那么美好,实际应用中的JS是要接触到ajax、DOM操作,NodeJS环境中读写文件、网络操作这些对于外部环境强依赖,有明显副作用的“很脏”的工作。

这对于函数式编程来说也是很大的挑战,所以我们也需要更强大的技术去解决这些“脏问题”。我会在下一篇文章中介绍函数式编程的更加高阶一些的知识,例如Functor、Monad等等概念。

八、参考

1、https:// class="visible">github.com/MostlyAdequa class="invisible">te/mostly-adequate-guide

2、http://www. class="visible">ibm.com/developerworks/ class="invisible">cn/web/1006_qiujt_jsfunctional/

3、《JavaScript函数式编程》【美】迈克尔·佛格斯

JavaScript函数式编程(一)

独孤九剑之命令式

const isPrimeNumber = x => {
    if (x <= 1) return false;

    let testRangStart = 2,
        testRangeEnd = Math.floor(Math.sqrt(x));

    let i = testRangStart;
    while (i <= testRangeEnd) {
        if (x % i == 0) return false;
        i  ;
    }

    return true;
};

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

let sum = 0;

for (let i = 0; i < arr.length; i  ) {
    if (isPrimeNumber(arr[i])) {
        sum  = arr[i] * arr[i];
    }
}

console.log(sum);

下面转载一篇JavaScript的函数式编程,很好的说明了JS是如何通过参数化函数和函数组合实现Point Free风格,进而消除不必要的中间变量让代码变得更简洁。函数式编程声名式实现,让程序更加清晰和简洁,让开发专注于编写业务代码,优化代码时,目光只需要集中在函数内部即可。

compose

举个例子。

const upperCase = str => str.toUpperCase();
const exclaim = str => str   '!';
const holify = str => 'Holy '   str;

现在需要一个amaze方法,字符串前面添加Holy,后面添加叹号,全部转为大写。

const amaze = str => upperCase(exclaim(holify(str)));

很不优雅对不对?

看看compose怎么帮我们解决这个问题。

const compose = (...fns) => x => fns.reduceRight((acc, cur) => cur(acc), x);
const amaze = compose(upperCase, exclaim, holify)
console.log(amaze('functional programing'));

这里用到了reduceRight,和reduce的区别就是数组是从后往前遍历的。
compose内的函数是从右往左运行的,也就是先holifyexclaimupperCase

有人可能看不惯从右往左运行,于是又有了一个pipeline

虽然标题是NodeJS函数式编程,但实际上NodeJS 是一个框架,不是一种语言,其采用的语言是 JavaScript。而JavaScript是一种典型的多范式编程语言,算不上是函数式语言,但它有函数式编程的一些特性:

filter map reduce 三板斧

来个例子:找出集合中的素数,算出它们平方的和。

curry

上面compose pipeline里的函数参数都只是一个,如果函数要传多个参数怎么办?

解决办法就是用curry【柯里化】,把函数变成一个参数的。

const add = (x, y) => x   y;
const multiply = (x, y) => x * y;

这两个函数都是需要传两个参数的,现在我需要一个函数,把数字先加5再乘2。

const add5ThenMultiplyBy2 = x => multiply(add(x, 5), 2)

很不好看,我们来curry一下再compose看看。

怎么curry
把括号去掉,逗号变箭头就可以了。
这样传入一个参数x的时候,返回了一个新函数,等待着接收参数y

const add = x => y => x   y;
const multiply = x => y => x * y;

接下来,我们又可以用compose

const add5ThenMultiplyBy2 = x => compose(multiply(2), add(5));

不过curry之后的add方法要这么调用了

add(2)(3)

原先的调用方式add(2, 3)都得改掉了。不喜欢这个副作用?再奉上一个工具函数curry

const curry = fn => {
    const inner = (...args) => {
        if (args.length >= fn.length) return fn(...args);
        else return (...newArgs) => inner(...args, ...newArgs);
    }
    return inner;
};

传入fn返回一个新函数,新函数调用时判断传入的参数个数有没有达到fn的要求,达到了,直接返回fn调用的结果;没达到,继续返回一个新新函数,记录着之前已传入的参数。

const add = (x, y) => x   y;
const curriedAdd = curry(add);

这样两种调用方式都支持了。

curriedAdd(2)(3);
curriedAdd(2, 3);

compose pipeline curry

写了一堆small pure function,怎么把他们组合成更强大的功能呢?

compose pipeline curry这三位该出场了。

WHAT? 什么是函数式编程?

函数式编程是一种编程范式。

编程范式又是什么?
编程范式是一种解决问题的思路。
我们熟悉的命令式编程把程序看作一系列改变状态的指令;而函数式编程把程序看作一系列数学函数映射的组合
编程范式和编程语言无关,任何编程语言都可以按照函数式的思维来组织代码。

i  ; // 命令式 关心指令步骤
[i].map(x => x   1); // 函数式 关心映射关系

破——剑——嗯....函数式

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const sum = arr.filter(isPrimeNumber)
    .map(x => x * x)
    .reduce((acc, cur) => acc   cur, 0);

console.log(sum);

看吧,for循环没了,代码意图也更明显了。

  1. filter(isPrimeNumber) 找出素数
  2. map(x => x * x) 变成平方
  3. reduce((acc, cur) => acc cur, 0) 求和

初入函数式编程。是不是比命令式看着更清晰了?

isPrimeNumber的函数式写法也放出,去掉了循环,看看好懂不。

// 输入范围,获得一个数组,例如 输入 1和5,返回 [1, 2, 3, 4, 5]
const range = (start, end) => start <= end ? [start].concat(range(start   1, end)) : [];
const isPrimeNumber = x => 
    x >= 2 ? range(2, Math.floor(Math.sqrt(x))).every(cur => x % cur != 0) : false;

有人说函数式的效率不高,因为filter map reduce每次调用,内部都会遍历一遍集合,而命令式只遍历了一次。

函数式是更高级的抽象,主要声明解决问题的步骤,把性能优化交给框架或者runtime来解决。

  • 框架
    transducer 可以让集合只遍历一次【篇幅有限,这里不展开】
    memorize 记录已经算过的,提高效率【后面讲纯函数的时候,会给出实现】
  • runtime
    有的语言map是多线程运行的,函数式代码不变,runtime一优化,性能就大幅的提升了,而前面的命令式,就做不到这一点。

HOW? 如何做起?

方法不难,回学校念个博士,搞清楚范畴论,幺半群之类的就可以了。

图片 1

人生苦短,还是来点实际的吧。

  1. filter map reduce 三板斧用好,从循环中解放出来
  2. small pure function 多写小的纯函数,小指功能聚焦
  3. compose pipeline curry 三个工具利用好,把小函数像搭积木一样拼成大函数

总结

函数式是一种编程思维,声明式、更抽象。

这种思维方式的利弊,大型项目里怎么用,我还没深刻的体会,练习还不足。

建议新手和我一样从下面三点开始多写多思考。

  1. filter map reduce 三板斧用好,从循环中解放出来
  2. small pure function 多写小的纯函数,小指功能聚焦
  3. compose pipeline curry 三个工具利用好,把小函数像搭积木一样拼成大函数

后面我会继续学习functor monad相关的知识,感兴趣可以关注。

本文由彩世界开奖app官网发布于前端技术,转载请注明出处:初入函数式编程

关键词: 函数式编程