关于函数式编程:Y-分钟速成-Lambda-Calculus

Lambda 演算Lambda 演算(lambda calculus, -calculus), 最后由阿隆佐·邱奇(Alonzo Church)提出, 是世界上最小的编程语言. 只管没有数字, 字符串, 布尔或者任何非函数的数据类型, lambda 演算仍能够示意任何图灵机. Lambda 演算由三种元素组成: 变量(variables)、函数(functions)和利用(applications)。 最根本的函数为恒等函数: x.x, 它等价于f(x) = x. 第一个"x"为函数的参数, 第二个为函数体. 自在变量和束缚变量:在函数x.x中, “x"被称作束缚变量因为它同时呈现在函数体和函数参数中在x.y中, "y"被称作自在变量因为它没有被事后申明.求值:求值操作是通过-归约(-Reduction)实现的, 它实质上是词法层面上的替换. 当对表达式(x.x)a求值时, 咱们将函数体中所有呈现的"x"替换为"a” (x.x)a计算结果为: a(x.y)a计算结果为: y你甚至能够创立高阶函数: (x.(y.x))a计算结果为: y.a只管 lambda 演算传统上仅反对单个参数的函数, 但咱们能够通过一种叫作柯里化(Currying)的技巧创立多个参数的函数 (x.y.z.xyz)等价于f(x, y, z) = ((x y) z)有时xy.<body>与x.y.<body>能够调换应用 意识到传统的 lambda 演算没有数字, 字符或者任何非函数的数据类型很重要. 布尔逻辑:在 lambda 演算中没有"真"或"假". 甚至没有 1 或 0. 作为替换: T示意为: x.y.x F示意为: x.y.y 首先, 咱们能够定义一个"if"函数btf, 它当b为真时返回t, b为假时返回f IF等价于: b.t.f.b t f 通过IF, 咱们能够定义根本的布尔逻辑运算符: a AND b等价于: ab.IF a b F a OR b等价于: ab.IF a T b ...

November 23, 2022 · 2 min · jiezi

关于函数式编程:Y-分钟速成-Lambda-Calculus

Lambda 演算Lambda 演算(lambda calculus, -calculus), 最后由阿隆佐·邱奇(Alonzo Church)提出, 是世界上最小的编程语言. 只管没有数字, 字符串, 布尔或者任何非函数的数据类型, lambda 演算仍能够示意任何图灵机. Lambda 演算由三种元素组成: 变量(variables)、函数(functions)和利用(applications)。 最根本的函数为恒等函数: x.x, 它等价于f(x) = x. 第一个"x"为函数的参数, 第二个为函数体. 自在变量和束缚变量:在函数x.x中, “x"被称作束缚变量因为它同时呈现在函数体和函数参数中在x.y中, "y"被称作自在变量因为它没有被事后申明.求值:求值操作是通过-归约(-Reduction)实现的, 它实质上是词法层面上的替换. 当对表达式(x.x)a求值时, 咱们将函数体中所有呈现的"x"替换为"a” (x.x)a计算结果为: a(x.y)a计算结果为: y你甚至能够创立高阶函数: (x.(y.x))a计算结果为: y.a只管 lambda 演算传统上仅反对单个参数的函数, 但咱们能够通过一种叫作柯里化(Currying)的技巧创立多个参数的函数 (x.y.z.xyz)等价于f(x, y, z) = ((x y) z)有时xy.<body>与x.y.<body>能够调换应用 意识到传统的 lambda 演算没有数字, 字符或者任何非函数的数据类型很重要. 布尔逻辑:在 lambda 演算中没有"真"或"假". 甚至没有 1 或 0. 作为替换: T示意为: x.y.x F示意为: x.y.y 首先, 咱们能够定义一个"if"函数btf, 它当b为真时返回t, b为假时返回f IF等价于: b.t.f.b t f 通过IF, 咱们能够定义根本的布尔逻辑运算符: a AND b等价于: ab.IF a b F a OR b等价于: ab.IF a T b ...

November 23, 2022 · 2 min · jiezi

关于函数式编程:理解函数组合compose及中间件实现

什么是函数组合?函数式组合能够了解为将一系列简略根底函数组合成能实现简单工作函数的过程;这些根底函数都须要承受一个参数并且返回数据,这数据应该是另一个尚未可知的程序的输出;利用compose 函数先来看一个只接管两个参数的compose 函数, 前面再欠缺compose函数 var compose = function(f,g) { return function(x) { return f(g(x)); };};这就是函数组合(compose),f 和 g 都是函数, x是在它们之间通过“管道”传输的值。 咱们能够通过组合函数使之产出一个簇新的函数: var toUpperCase = function(str) { return str.toUpperCase(); };var exclaim = function(str) { return str + '!'; };var shout = compose(exclaim, toUpperCase); shout('hello world');// HELLO WORLD!让代码从右向左运行,而不是由内而外运行 // 取列表中的第一个元素var head = function(arr) { return arr[0]; };// 反转列表var reverse = function (arr) { return arr.reduce(function(cur, next){ return [next].concat(cur); }, []);}var last = compose(head, reverse); last(['apple', 'banana', 'orange']); // orange能够看出compose的数据流是从右往左的,且从右向左执行更加可能反映数学上组合的含意; ...

June 18, 2022 · 4 min · jiezi

关于函数式编程:用JavaScript入门函数式编程刚入门趁热分享

函数式编程命令式和申明式咱们入门编程的时候,通常都是从命令式编程开始,即最简略的过程式代码。起初为了解决大型项目又接触到面向对象,也属于命令式。而函数式编程属于申明式,其实它的呈现早于面向对象。 MySQL就是一个很好的申明式语言的例子,它仅仅是申明了流程,却没有将过程的细节裸露进去。 SELECT * from data_base WHERE author='jack';再举一个例子来比拟命令式和申明式的代码: const arr = [1, 2, 3, 4, 5]// 要求将下面的数组中小于 3 的去掉,并且将剩下的数乘上 2// 命令式const result = []for (let i=0; i<arr.length; i++) { if (arr[i] >= 3){ result.push(arr[i] * 2) }}// 申明式const result = arr.filter(n >= 3).map(n => n*2)看了下面的例子你可能会想,申明式的代码就这?再看看下面的 MySQL 的例子,申明式确是如此,通过 filter、map 等办法,封装了细节,而后将逻辑申明进去,并不需要一步一个命令地阐明要怎么做,所以更简洁,而这就是最根本的申明式代码。 当咱们学面向对象的时候,都会先晓得它的三个个性:封装、继承、多态。基于这三个个性,人们在编写代码会延申出很多最佳实际,即设计模式,如工厂模式、依赖注入等。而函数式编程也是如此,有对应的个性和最佳实际。 函数式函数式编程是以函数为主的编程格调,其实程序就是由一段段逻辑组成,而将这些逻辑宰割成一个个函数,再将其组合施展到极致。 函数式解决了一个问题,当命令式的格调写代码时,一开始你能够很间接的实现工作代码,但当你开始思考边界解决和代码复用,慢慢的,你的代码会逐步背负它本不该有的复杂度,而函数式编程能解决这个问题。 程序的实质,除了是数据结构和算法,还能够是计算和副作用。 // 先看个例子,从 localStorage 中取出所有用户数据,并找出年龄最大的,显示在 DOM 上const users = localStorage.getItem('users')const sortedUsers = JSON.parse(users).sort((a, b) => b.age - a.age)const oldestUser = sortedUsers[0]document.querySelector('#user').innerText = oldestUser.name下面的代码很平时,然而了解起来却须要肯定工夫,先做个函数式的优化,将下面所有步骤封装一下。 ...

May 9, 2022 · 3 min · jiezi

关于函数式编程:前端函数式编程浅析

前言 在浅析函数式编程之前,咱们须要明确两个前导概念,即:编程范式(Programming Paradigm)与设计模式(Design Pattern): 对于编程范式(Programming Paradigm),维基百科给出的定义如下: Programming paradigms are a way to classify programming languages based on their features. Languages can be classified into multiple paradigms.能够看出,编程范式是一种组织代码的形式,它与各大语言的特点(特地是语言设计及编译器)非亲非故; 而设计模式(Design Pattern),维基百科给出的定义如下: In software engineering, a software design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design.从定义能够得出:设计模式是一种通用的解决方案,编程范式与语言的特点是强相干的,而设计模式则是任何语言都能够依据其特点进行实现的一种通用模板 从前导的概念,咱们能够理解,函数式编程是一种编程范式而不是一种设计模式,因此其与语言是强相干的,从上图能够看出,对于编程范式不能独自通过某一属性或某一边界将其辨别开,尤其是古代高级语言都已根本借鉴了其余语言的特色与办法,所以,目前大多数文章或教材中对编程形式的辨别办法都是做点状剖析,因为其从全局明确辨别的确比拟艰难,而常见的能够泛泛的将编程范式分为:命令式编程和申明式编程。其中命令式编程包含面向过程编程及面向对象编程(也有说面向对象编程属于元编程),而申明式编程包含函数式编程、逻辑式编程、响应式编程,咱们不谨严的能够简略的将常见编程范式进行简化为上图所示分类 概念前言中介绍了编程范式是与语言强相干的,因此函数式编程也是语言强相干的,最早的函数式编程语言是LISP,Schema语言是Lisp语言的一种方言,而古代语言比方Haskell、Clean、Erlang等也前仆后继的实现了函数式编程的特色。对于前端程序员而言,咱们应用的语言是JavaScript或TypeScript,而后者是前者的超集,因此能够算是类JavaScript的语言使用者,对于js而言,因为其设计者Brendan Eich自身是函数式编程的拥趸,因此其设计上借鉴了Schema的函数第一公民(First Class)的理念(ps:所谓函数第一公民,是指 they can be bound to names (including local identifiers), passed as arguments, and returned from other functions, just as any other data type can.,即函数具备能够通过名称绑定、传递参数,并且能够返回其余函数的特色),这就为js的函数式编程埋下了伏笔。既然js能够实现函数式编程的特点,那么函数式编程都有什么特点,或者说怎么样组织代码就是函数式编程了? ...

April 28, 2021 · 4 min · jiezi

关于函数式编程:什么是Sexpression

S-expressions,是 symbolic expressions的缩写, 也叫做 sexprs, 代表 nested list , 它是由编程语言Lisp创造并推广的,Lisp将它们用作源代码和数据。在Lisp罕用的圆括号语法中,s表达式通常被定义为 是 atom, 或者是是(x . y)这种模式,其中x和y也是一个exporession(递归定义)ps: atom的定义,即同时满足非pair数据, 非空数据 (define atom? (lambda (x)(and (not (pair? x)) (not (null? x)))))

April 14, 2021 · 1 min · jiezi

盘点Pandas-的100个常用函数

作者 | 刘顺祥 来源 | 数据分析1480这一期将分享我认为比较常规的100个实用函数,这些函数大致可以分为六类,分别是统计汇总函数、数据清洗函数、数据筛选、绘图与元素级运算函数、时间序列函数和其他函数。 统计汇总函数数据分析过程中,必然要做一些数据的统计汇总工作,那么对于这一块的数据运算有哪些可用的函数可以帮助到我们呢?具体看如下几张表。 import pandas as pdimport numpy as npx = pd.Series(np.random.normal(2,3,1000))y = 3*x + 10 + pd.Series(np.random.normal(1,2,1000))# 计算x与y的相关系数print(x.corr(y))# 计算y的偏度print(y.skew())# 计算y的统计描述值print(x.describe())z = pd.Series(['A','B','C']).sample(n = 1000, replace = True)# 重新修改z的行索引z.index = range(1000)# 按照z分组,统计y的组内平均值y.groupby(by = z).aggregate(np.mean) # 统计z中个元素的频次print(z.value_counts())a = pd.Series([1,5,10,15,25,30])# 计算a中各元素的累计百分比print(a.cumsum() / a.cumsum()[a.size - 1]) 数据清洗函数同样,数据清洗工作也是必不可少的工作,在如下表格中罗列了常有的数据清洗的函数。 x = pd.Series([10,13,np.nan,17,28,19,33,np.nan,27])#检验序列中是否存在缺失值print(x.hasnans)# 将缺失值填充为平均值print(x.fillna(value = x.mean()))# 前向填充缺失值print(x.ffill()) income = pd.Series(['12500元','8000元','8500元','15000元','9000元'])# 将收入转换为整型print(income.str[:-1].astype(int))gender = pd.Series(['男','女','女','女','男','女'])# 性别因子化处理print(gender.factorize())house = pd.Series(['大宁金茂府 | 3室2厅 | 158.32平米 | 南 | 精装', '昌里花园 | 2室2厅 | 104.73平米 | 南 | 精装', '纺大小区 | 3室1厅 | 68.38平米 | 南 | 简装'])# 取出二手房的面积,并转换为浮点型house.str.split('|').str[2].str.strip().str[:-2].astype(float) ...

October 16, 2019 · 1 min · jiezi

深入探寻JAVA8-part1函数式编程与Lambda表达式

开篇在很久之前粗略的看了一遍《Java8 实战》。客观的来,说这是一本写的非常好的书,它由浅入深的讲解了JAVA8的新特性以及这些新特性所解决的问题。最近重新拾起这本书并且对书中的内容进行深入的挖掘和沉淀。接下来的一段时间将会结合这本书,以及我自己阅读JDK8源码的心路历程,来深入的分析JAVA8是如何支持这么多新的特性的,以及这些特性是如何让Java8成为JAVA历史上一个具有里程碑性质的版本。 Java8的新特性概览在这个系列博客的开篇,结合Java8实战中的内容,先简单列举一下JAVA8中比较重要的几个新特性: 函数式编程与Lambda表达式Stram流处理Optional解决空指针噩梦异步问题解决方案CompletableFuture颠覆Date的时间解决方案后面将针对每个专题发博进行详细的说明。 简单说一说函数式编程函数式编程的概念并非这两年才涌现出来,这篇文章用一种通俗易懂的方式对函数式编程的理念进行讲解。顾名思义,函数式编程的核心是函数。函数在编程语言中的映射为方法,函数中的参数被映射为传入方法的参数,函数的返回结果被映射为方法的返回值。但是函数式编程的思想中,对函数的定义更加严苛,比如参数只能被赋值一次,即参数必须为final类型,在整个函数的声明周期中不能对参数进行修改。这个思想在如今看来是不可理喻的,因为这意味着任何参数的状态都不能发生变更。 那么函数式编程是如何解决状态变更的问题呢?它是通过函数来实现的。下面给了一个例子: String reverse(String arg) { if(arg.length == 0) { return arg; } else { return reverse(arg.substring(1, arg.length)) + arg.substring(0, 1); }}对字符串arg进行倒置并不会修改arg本身,而是会返回一个全新的值。它完全符合函数式编程的思想,因为在整个函数的生命周期中,函数中的每一个变量都没有发生修改。这种不变行在如今称为Immutable思想,它极大的减少了函数的副作用。这一特性使得它对单元测试,调试以及编发编程极度友好。因此在面向对象思想已经成为共识的时代,被重新提上历史的舞台。 但是,编程式思想并不只是局限于此,它强调的不是将所有的变量声明为final,而是将这种可重入的代码块在整个程序中自由的传递和复用。JAVA中是通过对象的传递来实现的。举个例子,假如现在有一个筛选订单的功能,需要对订单从不同的维度进行筛选,比如选出所有已经支付完成的订单,或是选出所有实付金额大于100的订单。 简化的订单模型如下所示: public class Order{ private String orderId; //实付金额 private long actualFee; //订单创建时间 private Date createTime; private boolean isPaid}接着写两段过滤逻辑分别实现选出已经支付完成的订单,和所有实付金额大于100的订单 //选出已经支付完成的订单public List<Order> filterPaidOrder(List<Order> orders) { List<Order> paidOrders = new ArrayList<>(); for(Order order : orders) { if(order.isPaid()) { paidOrders.add(order); } } return paidOrdres;}//选出实付金额大于100的订单public List<Order> filterByFee(List<Order> orders) { List<Order> resultOrders = new ArrayList<>(); for(Order order : orders) { if(order.getActualFee()>100) { resultOrders.add(order); } } return resultOrders;}可以看到,上面出现了大量的重复代码,明显的违背了DRY(Dont Repeat Yourself)原则,可以先通过模板模式将判断逻辑用抽象方法的形式抽取出来,交给具体的子类来实现。代码如下: ...

October 7, 2019 · 2 min · jiezi

如何编写高质量的-JS-函数3-函数式编程理论篇

本文首发于 vivo互联网技术 微信公众号  链接:https://mp.weixin.qq.com/s/EWSqZuujHIRyx8Eb2SSidQ 作者:杨昆 【编写高质量函数系列】中, 《如何编写高质量的 JS 函数(1) -- 敲山震虎篇》介绍了函数的执行机制,此篇将会从函数的命名、注释和鲁棒性方面,阐述如何通过 JavaScript 编写高质量的函数。  《如何编写高质量的 JS 函数(2)-- 命名/注释/鲁棒篇》从函数的命名、注释和鲁棒性方面,阐述如何通过 JavaScript编写高质量的函数。 【 前 言 】这是编写高质量函数系列文章的函数式编程篇。我们来说一说,如何运用函数式编程来提高你的函数质量。 函数式编程篇分为两篇,分别是理论篇和实战篇。此篇文章属于理论篇,在本文中,我将通过背景加提问的方式,对函数式编程的本质、目的、来龙去脉等方面进行一次清晰的阐述。 写作逻辑 通过对计算机和编程语言发展史的阐述,找到函数式编程的时代背景。通过对与函数式编程强相关的人物介绍,来探寻和感受函数式编程的那些不为人知的本质。 下面列一个简要目录: 一、背景介绍计算机和编程语言的发展史二、函数式编程的 10 问为什么会有函数式语言?函数式语言是如何产生的?它存在的意义是什么?lambda 演算系统是啥?lambda 具体说的是啥内容?lambda 和函数有啥联系?为啥会有 lambda 演算系统?函数式编程为什么要用函数去实现?函数式语言中,或者在函数式编程中,函数二字的含义是什么?它具备什么能力?函数式编程的特性关键词有哪些?命令式和函数式编程是对立的吗?按照 FP 思想,不能使用循环,那我们该如何去解决?抛出异常会产生副作用,但如果不抛出异常,又该用什么替代呢?函数式编程不允许使用可变状态的吗?如何没有副作用的表达我们的程序?为什么函数式编程建议消灭掉语句?三、JavaScript 函数式编程的 5 问为什么函数式编程要避免使用 thisJavaScript 中函数是一等公民, 就可以得出 JavaScript 是函数式语言吗?为什么说 JS 是多态语言?为什么 JS 函数内部可以使用 for 循环吗?JS 函数是一等公民是啥意识?这样做的目的是啥?用 JS 进行函数式编程的缺点是什么?四、总结函数式编程的未来。简要目录介绍完啦,大家请和我一起往下看。PS:我好像是一个在海边玩耍的孩子,不时为拾到比通常更光滑的石子,或更美丽的贝壳而欢欣鼓舞,而展现在我面前的是完全未探明的的真理之海。 【 正 文 】计算机和编程语言的发展史计算机和编程语言的发展史是由人类主导的,去了解在这个过程中起到关键作用的人物是非常重要的。 下面我们一起来认识几位起关键作用的超巨。一、戴维·希尔伯特 点击图片介绍: 戴维·希尔伯特 希尔伯特被称为数学界的无冕之王 ,他是天才中的天才。 在我看来,希尔伯特最厉害的一点就是:他鼓舞大家去将证明过程纯机械化,因为这样,机器就可以通过形式语言推理出大量定理。 也正是他的坚持推动,形式语言才逐渐走向历史的舞台中央。 二、艾伦·麦席森·图灵 点击图片介绍: 艾伦·麦席森·图灵 ...

October 7, 2019 · 3 min · jiezi

Haskell编程解决九连环3-详细的步骤

摘要在本系列的上一篇文章《Haskell编程解决九连环(2)— 多少步骤?》中,我们通过编写Python和Haskell的代码解决了关于拆解九连环最少需要多少步的问题。在本文中我们将更进一步,输出所有的详细步骤。每个步骤实际上是一个装上或者拆下一个圆环的动作。关于步骤动作的定义请参见本系列的第一篇文章《Haskell编程解决九连环(1)— 数学建模》。维基百科上关于九连环的条目中有拆解n连环所需的步数,在本文中我们将要通过编程计算来得出与下表中数字相对应的详细步骤动作,特别的,当连环的数目n=9时,结果应该是341条动作。 连环的数目123456789步数12510214285170341定理与推论定理与推论是我们编程实现的基础和指导,再次将它们罗列如下。 定理1:takeOff(1)的解法步骤序列为[OFF 1],putOn(1)的解法步骤序列为[ON 1]。定理2:takeOff(2)的解法步骤序列为[OFF 2, OFF 1],putOn(2)的解法步骤序列为[ON 1, ON 2]。定理3:当n>2时,takeOff(n)的解法由以下步骤组成:1) takeOff(n-2) 2) OFF n 3) putOn(n-2) 4) takeOff(n-1);而putOn(n) 由以下步骤组成 1) putOn(n-1) 2) takeOff(n-2) 3) ON n 4) putOn(n-2)。推论1:takeOff(n)的解法步骤序列和putOn(n)的解法步骤序列互为逆反序列。推论2:takeOff(n)的解法步骤序列和putOn(n)的解法步骤序列含有的步骤数目相等。推论3:对于任何整数m, n,如果m>n,那么第m环的状态(装上或是卸下)不影响takeOff(n)或者putOn(n)的解,同时解决takeOff(n)或者putOn(n)问题也不会改变第m环的状态。 照例,我们从一个命令式语言的实现开始。这有助于熟悉命令式编程语言的小伙伴们理解。也为随后的的Haskell实现设定结果规范。 Python 实现因为涉及到大量的输入输出,这次我们试着构造一个完整的程序。在该程序中提供了主函数入口,使得下面的代码不仅能在交互式环境中运行和测试,也可以在操作系统的命令行环境直接调用。 #!/usr/bin/pythonimport itertoolsdef printAction(stepNo, act, n): # (1) print('{:d}: {:s} {:d}'.format(stepNo, act, n))def takeOff(n, stepCount): # (2) if n == 1: printAction(next(stepCount), 'OFF', 1) elif n == 2: printAction(next(stepCount), 'OFF', 2) printAction(next(stepCount), 'OFF', 1) else: takeOff(n - 2, stepCount) printAction(next(stepCount), 'OFF', n) putOn(n - 2, stepCount) takeOff(n - 1, stepCount) def putOn(n, stepCount): if n == 1: printAction(next(stepCount), 'ON', 1) elif n == 2: printAction(next(stepCount), 'ON', 1) printAction(next(stepCount), 'ON', 2) else: putOn(n - 2, stepCount) printAction(next(stepCount), 'ON', n) takeOff(n - 2, stepCount) putOn(n - 1, stepCount)if __name__ == "__main__": # (3) n = int(input()) # (4) takeOff(n, itertools.count(start = 1)) # (5)这里我们不再赘述关于递归的基本条件或者递归的拆分算法部分。有兴趣的读者可以参阅本系列的前两篇文章。有一些新出现的东西值得关注。首先命令式语言并不禁止任何函数具有副作用,我们能很容易地在任何地方做输入输出。在代码中大家也可以看到函数的申明没有任何的区别,但是我们能够在迭代或是递归的过程中打印相应的结果。下面我们沿着在代码中标注的序号做一些解释。 ...

September 10, 2019 · 7 min · jiezi

函数式编程让你忘记设计模式

本文是一篇《Java 8实战》的阅读笔记,阅读大约需要5分钟。有点标题党,但是这确实是我最近使用Lambda表达式的感受。设计模式是过去的一些好的经验和套路的总结,但是好的语言特性可以让开发者不去考虑这些设计模式。面向对象常见的设计模式有策略模式、模板方法、观察者模式、责任链模式以及工厂模式,使用Lambda表达式(函数式编程思维)有助于避免面向对象开发中的那些固定代码。下面我们挑选了策略模式和职责链模式两个案例进行分析。 案例1:策略模式 当我们解决一个问题有不同的解法的时候,又不希望客户感知到这些解法的细节,这种情况下适合使用策略模式。策略模式包括三个部分: 解决问题的算法(上图中的Strategy);一个或多个该类算法的具体实现(上图中的ConcreteStrategyA、ConcreteStrategyB和ConcreteStrategyC)一个或多个客户使用场景(上图中的ClientContext)面向对象思路首先定义策略接口,表示排序策略: public interface ValidationStrategy { boolean execute(String s);}然后定义具体的实现类(即不同的排序算法): public class IsAllLowerCase implements ValidationStrategy { @Override public boolean execute(String s) { return s.matches("[a-z]+"); }}public class IsNumberic implements ValidationStrategy { @Override public boolean execute(String s) { return s.matches("\\d+"); }}最后定义客户使用场景,代码如下图所示。Validator是为客户提供服务时使用的上下文环境,每个Valiator对象中都封装了具体的Strategy对象,在实际工作中,我们可以通过更换具体的Strategy对象来进行客户服务的升级,而且不需要让客户进行升级。 public class Validator { private final ValidationStrategy strategy; public Validator(ValidationStrategy strategy) { this.strategy = strategy; } /** * 给客户的接口 */ public boolean validate(String s) { return strategy.execute(s); }}public class ClientTestDrive { public static void main(String[] args) { Validator numbericValidator = new Validator(new IsNumberic()); boolean res1 = numbericValidator.validate("7780"); System.out.println(res1); Validator lowerCaseValidator = new Validator(new IsAllLowerCase()); boolean res2 = lowerCaseValidator.validate("aaaddd"); System.out.println(res2); }}函数式编程思路如果使用Lambda表达式考虑,你会发现ValidationStrategy就是一个函数接口(还与Predicate<String>具有同样的函数描述),那么就不需要定义上面那些实现类了,可以直接用下面的代码替换,原因是Lambda表达式内部已经对这些类进行了一定的封装。 ...

July 7, 2019 · 2 min · jiezi

vavr让你像写Scala一样写Java

Hystrix是Netflix开源的限流、熔断降级组件,去年发现Hystrix已经不再更新了,而在github主页上将我引导到了另一个替代项目——resilience4j,这个项目是基于Java 8开发的,并且只使用了vavr库,也就是我们今天要介绍的主角。 Lambda表达式既然要谈vavr,那么先要谈为什么要使用vavr,vavr是为了增强Java的函数式编程体验的,那么这里先介绍下Java中的函数式编程。 Java 8引入了函数式编程范式,思路是:将函数作为其他函数的参数传递,其实在Java 8之前,Java也支持类似的功能,但是需要使用接口实现多态,或者使用匿名类实现。不管是接口还是匿名类,都有很多模板代码,因此Java 8引入了Lambda表达式,正式支持函数式编程。 比方说,我们要实现一个比较器来比较两个对象的大小,在Java 8之前,只能使用下面的代码: Compartor<Apple> byWeight = new Comparator<Apple>() { public int compare(Apple a1, Apple a2) { return a1.getWeight().compareTo(a2.getWeight()); }}上面的代码使用Lambda表达式可以写成下面这样(IDEA会提示你做代码的简化): Comparator<Apple> byWeight = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());关于Lambda表达式,你需要掌握的知识点有: Lambda表达式可以理解为是一种匿名函数:它没有名称,但是又参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表;函数式接口就是仅仅声明了一个抽象方法的接口;@FunctionalInterface注解对于函数式接口的作用,类似于@Override对于被重写的方法——不是必须的,但是用了有助于提升代码的可读性,因此如果你在开发中自己定义函数式接口,最好也使用这个注解修饰;Java 8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate<T>、Function<T,R>、Supplier<T>、Consumer<T>和BinaryOperator<T>等等;vavr受限于 Java 标准库的通用性要求和二进制文件大小,Java 标准库对函数式编程的 API 支持相对比较有限。函数的声明只提供了 Function 和 BiFunction 两种,流上所支持的操作的数量也较少。基于这些原因,你也许需要vavr来帮助你更好得使用Java 8进行函数式开发。 vavr是在尝试让Java拥有跟Scala类似的语法。vavr提供了不可变的集合框架;更好的函数式编程特性;元组。 集合Vavr实现了一套新的Java集合框架来匹配函数式编程范式,vavr提供的集合都是不可变的。在Java中使用Stream,需要显示得将集合转成steam的步骤,而在vavr中则免去了这样的步骤。 vavr的List是不可变的链表,在该链表对象上的操作都会生成一个新的链表对象。使用Java 8的代码: Arrays.asList(1, 2, 3).stream().reduce((i, j) -> i + j);IntStream.of(1, 2, 3).sum();使用vavr实现相同的功能,则更加直接: //io.vavr.collection.ListList.of(1, 2, 3).sum();vavr的Stream是惰性链表,元素只有在必要的时候才会参与计算,因此大部分操作都可以在常量时间内完成。函数(Functions)Java 8提供了接受一个参数的函数式接口Function和接受两个参数的函数式接口BiFunction,vavr则提供了最多可以接受8个参数的函数式接口:Function0、Function1、Function2、Function3、Function4……Function8。 vavr还提供了更多函数式编程的特性: 组合(Composition)在数学上,函数组合可以用两个函数形成第三个函数,例如函数f:X->Y和函数g:Y->Z可以组合成h:g(f(x)),表示X->Z。这里看个组合的例子: ...

June 29, 2019 · 2 min · jiezi

YRoute开发随笔

YRoute是一个新开发的Android路由库,使用了arrow函数式库作为核心库,是之前对于函数范式学习和思考的集大成者。但目前还在前期开发阶段,仅实现了一些简单的功能做架构验证用。 OOP中的23种设计模式相信大家已经烂熟于心了, 它们已经被广泛应用于软件工业的各个领域. 它们当初被创造是因为当时旧的编程思想在软件规模逐渐庞大的情况下已经难以驾驭了. 然而随着软件工业这么多年的持续发展, 同样的问题又来到了OOP的面前, 现在的代码抽象度越来越高, OOP的很多技法已经开始有点捉襟见肘, 这也是为什么这几年抽象度更高的函数范式的概念被越来越多的提起函数式编程中单子、高阶类型等概念被经常提起, 但面向组合子编程的概念却少有提及, 它是一种与以前构建程序完全的不同的思维模式: 由下至上构建程序. YRoute是使用这种方式进行构建的, 希望通过这个库和这篇对于开发过程描述的文章, 对大家会有所启发前因FragmentManager是几年前个人开发的一个Fragment管理库,相比其他库有Rx方式启动、多堆栈切换、Fragment与Activity一致的动画处理等等。此库在多个实际项目中被使用,功能被不断完善,稳定性、灵活性也得到了项目的验证,所以现在基本是项目开发的默认基础库了。 但对于个人而言其实一直不满于这个库本身的架构技术。最开始构建的时候以功能实现为主,也以以前习惯的OOP思想进行构建(因为那个时候还没有被函数范式“荼毒”),导致了架构上的各种问题: 状态和逻辑混杂在一起由于需求的功能基于Fragment和Activity的很多基础方法和生命周期,导致需要强继承BaseFragmentManagerActivity 和BaseManagerFragmentBaseFragmentManagerActivity被设计为了“超级类”:功能强大,但包含了大量可以被分离的逻辑,导致逻辑代码混杂由于启动、切换等功能逻辑很复杂,需要很多的判断和异常处理,导致有些方法虽然很相似却依然无法被重构合并,方法的粒度大,难以被组合虽然中期在添加新功能的时候尝试进行逻辑分离(比如侧滑返回返回功能SwipeBackUtil 、Rx启动功能、抖动抑制ThrottleUtil ),但基于OOP设计本身的缺点,它们的分离没有统一的模式,也无法真正清晰的分离,实际功能代码还是需要依赖混杂到BaseFragmentManagerActivity 后期虽然也希望借鉴Redux等架构设计进行了几次重构,但那时对函数范式架构的理解还不够深,导致架构本身难以承受库本身复杂的功能,而且也没有达到最初希望的灵活性,甚至相比原始的架构更加难用了得益于对函数范式在实践中的更多理解, 才有了YRoute这个库的出现 YRoute之前要理解YRoute库, 首先需要介绍一下相关的几个数据结构 Lens这是一个用于类型转换的数据类型, 从它的定义上就可以看出 data class Lens<S, T>( get: (S) -> T, set: (S, T) -> S)它包含两个函数, 一个是从数据类型S中提取T的get函数, 二一个是将旧的S和T数据组合成为新的S的函数set 用法可以参照: data class Player(val health: Int)val playerLens: Lens<Player, Int> = Lens( get = { player -> player.health }, set = { player, value -> player.copy(health = value) })val player = Player(70)Reader在函数范式中我们会提取很多的单子, 而其中函数本身其实也是一种单子, 而函数(D) -> A所抽取的单子就是Reader: ...

June 25, 2019 · 2 min · jiezi

JavaScript深入浅出第2课函数是一等公民是什么意思呢

摘要: 听起来很炫酷的一等公民是啥? 《JavaScript深入浅出》系列: JavaScript深入浅出第1课:箭头函数中的this究竟是什么鬼?JavaScript深入浅出第2课:函数是一等公民是什么意思呢?看到一篇讲JavaScript历史的文章里面提到:JavaScript借鉴Scheme语言,将函数提升到"一等公民"(first class citizen)的地位。 一等公民这个名字听起来很高大上,但是也相当晦涩,这个与翻译也没什么关系,因为first class citizen很多人包括我也不知所云。 JavaScript函数是一等公民,是什么意思呢?我来与大家探讨一下,抛砖引玉。 一等公民的定义根据维基百科,编程语言中一等公民的概念是由英国计算机学家Christopher Strachey提出来的,时间则早在上个世纪60年代,那个时候还没有个人电脑,没有互联网,没有浏览器,也没有JavaScript。 大概很多人和我一样,没听说过Christopher Strachey,并且他也只是提出了一等公民的概念,没有给出严格的定义。 关于一等公民,我找到一个权威的定义,来自于一本书《Programming Language Pragmatics》,这本书是很多大学的程序语言设计的教材。 In general, a value in a programming language is said to have first-class status if it can be passed as a parameter, returned from a subroutine, or assigned into a variable.也就是说,在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。 例如,字符串在几乎所有编程语言中都是一等公民,字符串可以做为函数参数,字符串可以作为函数返回值,字符串也可以赋值给变量。 对于各种编程语言来说,函数就不一定是一等公民了,比如Java 8之前的版本。 对于JavaScript来说,函数可以赋值给变量,也可以作为函数参数,还可以作为函数返回值,因此JavaScript中函数是一等公民。 函数作为函数参数回调函数(callback)是JavaScript异步编程的基础,其实就是把函数作为函数参数。例如,大家常用的setTimeout函数的第一个参数就是函数: setTimeout(function() { console.log("Hello, Fundebug!");}, 1000);JavaScript函数作为函数参数,或者说回调函数,作为实现异步的一种方式,大家都写得多了,其实它还有其他应用场景。 Array.prototype.sort()在对一些复杂数据结构进行排序时,可以使用自定义的比较函数作为参数: var employees = [ { name: "Liu", age: 21 }, { name: "Zhang", age: 37 }, { name: "Wang", age: 45 }, { name: "Li", age: 30 }, { name: "zan", age: 55 }, { name: "Xi", age: 37 }];// 员工按照年龄排序employees.sort(function(a, b) { return a.age - b.age;});// 员工按照名字排序employees.sort(function(a, b) { var nameA = a.name; var nameB = b.name; if (nameA < nameB) { return -1; } if (nameA > nameB) { return 1; } return 0;});这样写看起来没什么大不了的,但是对于JavaScript引擎来说就省事多了,因为它不需要为每一种数据类型去实现一个排序API,它只需要实现一个排序API就够了,至于数组元素大小怎么比较,交给用户去定义,用户如果非得说2大于1,那也不是不可以。 ...

June 25, 2019 · 2 min · jiezi

函数式编程二

高阶函数满足以下两点的函数: 函数可以作为参数被传递函数可以作为返回值输出叫高阶函数,很显然js中的函数满足高阶函数的条件。 函数作为参数: function pow(x) { return x * x;}const arr = [1, 2, 3];const res = arr.map(pow);函数作为返回值: function getPrintFn() { function print(msg) { console.log(msg); } return print;}高阶函数与函数式编程有什么关系?通过上一篇我们知道函数式编程采用纯函数,那怎么把不纯的函数转化为一个纯函数呢?通过把不纯的操作包装到一个函数中,再返回这个函数(即上面的例子),即可达到目的。 柯里化(curry)只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。特点:接收单一参数,将更多的参数通过回调函数来搞定; 返回一个新函数,用于处理所有的想要传入的参数;需要利用call/apply与arguments对象收集参数;返回的这个函数正是用来处理收集起来的参数; function add(x, y) { return x + y;}// 柯里化function add(x) { return function(y) { return x + y; }}const increment = add(1);increment(2); // 3当我们谈论纯函数的时候,我们说它们接受一个输入返回一个输出。curry 函数所做的正是这样:每传递一个参数调用函数,就返回一个新函数处理剩余的参数。这就是一个输入对应一个输出。curry函数适用于以下场景: 延迟执行:不断的柯里化,累积传入的参数,最后执行。固定易变因素:提前把易变因素,传参固定下来,生成一个更明确的应用函数。最典型的代表应用,是bind函数用以固定this这个易变对象。代码组合(compose)在函数式编程中,通过将一个个功能单一的纯函数组合起来实现一个复杂的功能,就像乐高拼积木一样,这种称为函数组合(代码组合)。下面看一个例子: 最佳实践是让组合可重用。 函子我们知道,函数式编程实质是通过管道把数据在一系列纯函数间传递,但是,控制流(control flow)、异常处理(error handling)、异步操作(asynchronous actions)和状态(state)呢?还有更棘手的副作用(effects)呢?这些问题的解决就要引入函子的概念了。 我们首先定义一个容器,用来封装数据 函子封装了数据和对数据的操作,functor 是实现了map函数并遵守一些特定规则的容器类型。 ...

June 9, 2019 · 1 min · jiezi

JS函数式编程小记

JS函数式编程思想维基百科定义:函数式编程(英语:functional programming),又称泛函编程,是一种编程范式,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。个人觉得这个定义肥肠生动形象了。函数式编程处处体现着数学的逻辑思想,通过减少外部依赖来避免外部变量的改变对于程式内部状态的改变,使得程序变得简易、声明式、易于维护。这里推荐一下F大的《JS函数式编程指南》,系统介绍了函数式编程的思想。中文版电子书下载地址走你:https://legacy.gitbook.com/bo...本文章是自己阅读后的一些理解和记录~ First Class Funtcion这个概念同样出于书中。规定了变量可以取的值得范围,以及该类型的值可以进行的操作。根据类型的值的可赋值状况,可以把类型分为三类。 一级的(first class)。该等级类型的值可以传给子程序作为参数,可以从子程序里返回,可以赋给变量。大多数程序设计语言里,整型、字符类型等简单类型都是一级的。二级的(second class)。该等级类型的值可以传给子程序作为参数,但是不能从子程序里返回,也不能赋给变量。三级的(third class)。该等级类型的值连作为参数传递也不行。在scala中,函数是可以作为参数来传递并且返回的,所以scala中的函数就是first class functionPS:scala 是一门多范式(multi-paradigm)的编程语言,设计初衷是要集成面向对象编程和函数式编程的各种特性。 因此,function作为变量的一种,可以被储存,当作参数传递,或者被复制给变量等等... Pure Function首先弄清纯函数的概念:Pure function 意指相同的輸入,永遠會得到相同的輸出,而且沒有任何顯著的副作用。它符合两个条件: 此函数在相同的输入值时,总是产生相同的输出。函数的输出和当前运行环境的上下文状态无关。此函数运行过程不影响运行环境,也就是无副作用(如触发事件、发起http请求、打印/log等)。简单来说,也就是当一个函数的输出不受外部环境影响,同时也不影响外部环境时,该函数就是纯函数,也就是它只关注逻辑运算和数学运算,同一个输入总得到同一个输出。比如slice和splice,但是slice是纯函数,而splice不是,因为后者会改变源数据。这就是强调使用纯函数的原因,因为纯函数相对于非纯函数来说,在可缓存性、可移植性、可测试性以及并行计算方面都有着巨大的优势。 看一下书中的例子: // impurevar minimum = 21;var checkAge = function(age) { return age >= minimum;};// purevar checkAge = function(age) { var minimum = 21; return age >= minimum;};在impure 的版本中,checkAge 的結果將取決於 minimum 這個变量,换句话说,除了输入以外,它将取决于系统状态,一旦引用了外部环境,它就不符合pure了。当然,也可以直接冻结变量,使得状态不再变化,那么它也是一个纯函数: var immutableState = Object.freeze({ minimum: 21,});Curry(柯里化)书中定义柯里化的概念是:你可以只透過部分的參數呼叫一個 function,它會回傳一個 function 去處理剩下的參數。函数柯里化是一种“预加载”函数的能力,通过传递一到两个参数调用函数,就能得到一个记住了这些参数的新函数。从某种意义上来讲,这是一种对参数的缓存,是一种非常高效的编写函数的方法,将一个低阶函数转换为高阶函数的过程就叫柯里化。 //举个栗子var checkage = min => (age => age > min);var checkage18 = checkage(18);checkage18(20);// =>truevar curry = require('lodash').curry;//柯里化两个纯函数var match = curry((what, str) => str.match(what));var filter = curry((f, ary) => ary.filter(f));//判断字符串里有没有空格var hasSpaces = match(/\s+/g);hasSpaces("hello world"); // [ ' ' ]hasSpaces("spaceless"); // nullvar findSpaces = filter(hasSpaces);findSpaces(["tori_spelling", "tori amos"]); // ["tori amos"]Compose—Functional饲养假设要对某个字符串做一系列操作,我们做的事情一多,嵌套的层数会非常深。类似于巢状堆积(比如c(b(a())))这种堆砌方式非常不直观,当我们希望代码以平行方式(书中成为左倾)组合执行时,就成为Composecompose 的概念直接來自於數學課本,所以compose都有的一个特性: ...

June 4, 2019 · 1 min · jiezi

乐字节Java8核心特性实战之函数式接口

大家好,上一篇小乐给大家讲述了《乐字节-Java8核心特性-Lambda表达式》,点击回顾。接下来继续:Java8核心特性之函数式接口。 什么时候可以使用Lambda?通常Lambda表达式是用在函数式接口上使用的。从Java8开始引入了函数式接口,其说明比较简单:函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。 java8引入@FunctionalInterface 注解声明该接口是一个函数式接口。 一、语法抽象方法有且仅有一个接口使用@FunctionalInterface 注解进行标注接口中可以存在默认方法和静态方法实现如下形式: /** * 定义函数式接口 * 接口上标注@FunctionalInterface 注解 */@FunctionalInterfacepublic interface ICollectionService { /** * 定义打印方法 */ void print();}在Java8 以前,已有大量函数式接口形式的接口(接口中只存在一个抽象方法),只是没有强制声明。例如java.lang.Runnable,java.util.concurrent.Callable,java.security.PrivilegedAction,java.io.FileFilter等,Java8 新增加的函数接口在java.util.function 包下,它包含了很多类,用来支持 Java的 函数式编程,该包中的函数式接口如下: 序号接口 & 描述1BiConsumer<T,U>代表了一个接受两个输入参数的操作,并且不返回任何结果2BiFunction<T,U,R>代表了一个接受两个输入参数的方法,并且返回一个结果3BinaryOperator<T>代表了一个作用于于两个同类型操作符的操作,并且返回了操作符同类型的结果4BiPredicate<T,U>代表了一个两个参数的boolean值方法5BooleanSupplier代表了boolean值结果的提供方6Consumer<T>代表了接受一个输入参数并且无返回的操作7DoubleBinaryOperator代表了作用于两个double值操作符的操作,并且返回了一个double值的结果。8DoubleConsumer代表一个接受double值参数的操作,并且不返回结果。9DoubleFunction<R>代表接受一个double值参数的方法,并且返回结果10DoublePredicate代表一个拥有double值参数的boolean值方法11DoubleSupplier代表一个double值结构的提供方12DoubleToIntFunction接受一个double类型输入,返回一个int类型结果。13DoubleToLongFunction接受一个double类型输入,返回一个long类型结果14DoubleUnaryOperator接受一个参数同为类型double,返回值类型也为double 。15Function<T,R>接受一个输入参数,返回一个结果。16IntBinaryOperator接受两个参数同为类型int,返回值类型也为int 。17IntConsumer接受一个int类型的输入参数,无返回值 。18IntFunction<R>接受一个int类型输入参数,返回一个结果 。19IntPredicate:接受一个int输入参数,返回一个布尔值的结果。20IntSupplier无参数,返回一个int类型结果。21IntToDoubleFunction接受一个int类型输入,返回一个double类型结果 。22IntToLongFunction接受一个int类型输入,返回一个long类型结果。23IntUnaryOperator接受一个参数同为类型int,返回值类型也为int 。24LongBinaryOperator接受两个参数同为类型long,返回值类型也为long。25LongConsumer接受一个long类型的输入参数,无返回值。26LongFunction<R>接受一个long类型输入参数,返回一个结果。27LongPredicateR接受一个long输入参数,返回一个布尔值类型结果。28LongSupplier无参数,返回一个结果long类型的值。29LongToDoubleFunction接受一个long类型输入,返回一个double类型结果。30LongToIntFunction接受一个long类型输入,返回一个int类型结果。31LongUnaryOperator接受一个参数同为类型long,返回值类型也为long。32ObjDoubleConsumer<T>接受一个object类型和一个double类型的输入参数,无返回值。33ObjIntConsumer<T>接受一个object类型和一个int类型的输入参数,无返回值。34ObjLongConsumer<T>接受一个object类型和一个long类型的输入参数,无返回值。35Predicate<T>接受一个输入参数,返回一个布尔值结果。36Supplier<T>无参数,返回一个结果。37ToDoubleBiFunction<T,U>接受两个输入参数,返回一个double类型结果38ToDoubleFunction<T>接受一个输入参数,返回一个double类型结果39ToIntBiFunction<T,U>接受两个输入参数,返回一个int类型结果。40ToIntFunction<T>接受一个输入参数,返回一个int类型结果。41ToLongBiFunction<T,U>接受两个输入参数,返回一个long类型结果。42ToLongFunction<T>接受一个输入参数,返回一个long类型结果。43UnaryOperator<T>接受一个参数为类型T,返回值类型也为T。对于Java8中提供的这么多函数式接口,开发中常用的函数式接口有以下几个 Predicate,Consumer,Function,Supplier。 二、函数式接口实例1、Predicatejava.util.function.Predicate<T> 接口定义了一个名叫 test 的抽象方法,它接受泛型 T 对象,并返回一个boolean值。在对类型 T进行断言判断时,可以使用这个接口。通常称为断言型接口 。 字符串判空 Predicate<String> p01=(str)->str.isEmpty()||str.trim().isEmpty(); /** * 测试传入的字符串是否为空 */ System.out.println(p01.test("")); System.out.println(p01.test(" ")); System.out.println(p01.test("admin"));用户合法性校验接口静态方法完成手机号合法校验功能,方法返回函数式接口Predicate public interface MyStringInter { public final String checkPhone= "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(16[0-9])" + "|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$"; /** * 用户手机格式合法性 * 返回L函数式接口Predicate 的实现 Lambda表达式 * @return */ static Predicate<String> checkPhone(){ return (e)-> { return Pattern.compile(checkPhone).matcher(e).matches(); }; }}2、Consumerjava.util.function.Consumer<T>接口定义了一个名叫 accept 的抽象方法,它接受泛型T,没有返回值(void)。如果需要访问类型 T 的对象,并对其执行某些操作,可以使用这个接口,通常称为消费型接口。 ...

June 3, 2019 · 2 min · jiezi

译函数式的React

原文:The functional side of React作者:Andrea Chiarelli译者:博轩React 是现在最流行的 JavaScript 库之一。使用 React 可以非常轻松地创建 Web 用户交互界面。 它的成功有很多因素,但也许其中一个因素是清晰有效的编程方法。 在 React 的世界中,UI 是由一个一个组件所组成的。组件可以组合在一起以创建其他组件, 应用本身就是一个包含了所有组件的一个大组件。开发者使用 React 会很容易联想到:面向对象编程 。因为定义组件的语法本身,就会给人这种感觉: class HelloReact extends Component { render() { return (<div>Hello React!</div>); }}然鹅,在面向对象的的表象之下,React 隐藏了一种函数式的特质。让我们看看这些特质都是什么? 使用 render() 渲染输出React 组件的一大特征是是包含了 render() 方法。没有包含 render() 方法的组件不是 React 组件。render() 方法总会返回一个 React 元素,这种行为就像是组件的一种特征一样。换句话说,React 会要求任何组件必须有输出。组件是根据输入来返回输出的,这样来考虑组件的话,就会让你感觉组件更像一个函数,而不是一个对象。 组件就是一个函数实际上,您不仅可以将 React 组件视为函数。 您还可以用函数来实现组件。 以下代码展示了如何使用函数实现上面定义的组件: const HelloReact = () => <div>Hello React!</div>;如您所见,它是一种实现组件的简单而紧凑的方法。 此外,您可以将参数传递给函数: const Hello = (props) => <div>Hello {props.name}!</div>;在上面的示例中,您传递了 props 参数,这里的 props 对象用于将数据从一个组件传递到另一个组件。 ...

May 7, 2019 · 2 min · jiezi

5分钟学会javascript多条件排序的函数式实现

一些筛选、排序的场景,会遇到多个条件组合对数据进行排序的需求 在javascript中,应如何实现?并且能够满足灵活配置呢?首先javascript中数组的sort函数可对数据进行排序处理 sort函数说明 sort所需要的参数为一个返回值为number类型的函数,通过调用此函数的结果与0进行比较,得到小于0、等于0、大于0的结果,进行排序假设需要进行多个条件的排序,那么可产生一个条件数组 [条件1, 条件2, 条件3] 通过对此数组中元素的调整,即可灵活配置多个条件,并控制其判断的先后顺序 sort的参数为一个函数,需要对数组进行转化单个条件判断函数,对一些特殊的判断逻辑,小于0、等于0、大于0的判断方式并不通用 如性别的判断,男or女 条件判断函数的参数实际为相邻的两个待排序的元素 通过对 (a, b) => boolean函数的包装,即可实现小于0、等于0、大于0的结果 只需要在调用时,调整a、b参数的顺序即可function getSort(fn) { return function(a, b) { var ret = 0; if (fn.call(this, a, b)) { ret = -1; } else if (fn.call(this, b, a)) { ret = 1; } return ret; }}function getMutipSort(arr) { return function(a, b) { var tmp, i = 0; do { tmp = arr[i++](a, b); } while (tmp == 0 && i < arr.length); return tmp; }}var ageSort = getSort(function(a, b) { return a.age < b.age;});var nameSort = getSort(function(a, b) { return a.name < b.name;});var sexSort = getSort(function(a, b) { return a.sex && !b.sex;});//判断条件先后顺序可调整var arr = [nameSort, ageSort, sexSort];var ret = data.sort(getMutipSort(arr));

May 7, 2019 · 1 min · jiezi

JavaScript函数式编程入门经典

一个持续更新的github笔记,链接地址:Front-End-Basics,可以watch,也可以star。 此篇文章的地址:JavaScript函数式编程入门经典 正文开始 什么是函数式编程?为何它重要?数学中的函数f(x) = y// 一个函数f,以x为参数,并返回输出y关键点: 函数必须总是接受一个参数函数必须总是返回一个值函数应该依据接收到的参数(例如x)而不是外部环境运行对于一个给定的x,只会输出唯一的一个y函数式编程技术主要基于数学函数和它的思想,所以要理解函数式编程,先了解数学函数是有必要的。 函数式编程的定义函数是一段可以通过其名称被调用的代码。它可以接受参数,并返回值。 与面向对象编程(Object-oriented programming)和过程式编程(Procedural programming)一样,函数式编程(Functional programming)也是一种编程范式。我们能够以此创建仅依赖输入就可以完成自身逻辑的函数。这保证了当函数被多次调用时仍然返回相同的结果(引用透明性)。函数不会改变任何外部环境的变量,这将产生可缓存的,可测试的代码库。 函数式编程具有以下特征1、引用透明性所有的函数对于相同的输入都将返回相同的值,函数的这一属性被称为引用透明性(Referential Transparency) // 引用透明的例子,函数identity无论输入什么,都会原封不动的返回var identity = (i) => {return i}替换模型把一个引用透明的函数用于其他函数调用之间。 sum(4,5) + identity(1) 根据引用透明的定义,我们可以把上面的语句换成: sum(4,5) + 1 该过程被称为替换模型(Substitution Model),因为函数的逻辑不依赖其他全局变量,你可以直接替换函数的结果,这与它的值是一样的。所以,这使得并发代码和缓存成为可能。 并发代码: 并发运行的时候,如果依赖了全局数据,要保证数据一致,必须同步,而且必要时需要锁机制。遵循引用透明的函数只依赖参数的输入,所以可以自由的运行。 缓存: 由于函数会为给定的输入返回相同的值,实际上我们就能缓存它了。比如实现一个计算给定数值的阶乘的函数,我们就可以把每次阶乘的结果缓存下来,下一次直接用,就不用计算了。比如第一次输入5,结果是120,第二次输入5,我们知道结果必然是120,所以就可以返回已缓存的值,而不必再计算一次。 2、声明式和抽象函数式编程主张声明式编程和编写抽象的代码。 比较命令式和声明式// 有一个数组,要遍历它并把它打印到控制台/*命令式*/var array = [1,2,3]for(var i = 0; i < array.length; i++)console(array[i]) // 打印 1,2,3// 命令式编程中,我们精确的告诉程序应该“如何”做:获取数组的长度,通过数组的长度循环数组,在每一次循环中用索引获取每一个数组元素,然后打印出来。// 但是我们的任务只是打印出数组的元素。并不是要告诉编译器要如何实现一个遍历。/*声明式*/var array = [1,2,3]array.forEach((element) => console.log(element)) // 打印 1,2,3// 我们使用了一个处理“如何”做的抽象函数,然后我们就能只关心做“什么”了函数式编程主张以抽象的方式创建函数,例如上文的forEach,这些函数能够在代码的其他部分被重用。3、纯函数大多数函数式编程的好处来自于编写纯函数,纯函数是对给定的输入返回相同的输出的函数,并且纯函数不应依赖任何外部变量,也不应改变任何外部变量。 纯函数的好处纯函数产生容易测试的代码纯函数容易写出合理的代码纯函数容易写出并发代码纯函数总是允许我们并发的执行代码。因为纯函数不会改变它的环境,这意味着我们根本不需要担心同步问题。 纯函数的输出结果可缓存既然纯函数总是为给定的输入返回相同的输出,那么我们就能够缓存函数的输出。 高阶函数数据和数据类型程序作用于数据,数据对于程序的执行很重要。每种编程语言都有数据类型。这些数据类型能够存储数据并允许程序作用其中。 ...

May 5, 2019 · 5 min · jiezi

译JavaScript-中的函数式编程原理

原文:Functional Programming Principles in Javascript作者:TK译者:博轩经过很长一段时间的学习和面向对象编程的工作,我退后一步,开始思考系统的复杂性。 “复杂性是任何使软件难以理解或修改的东西。” - John Outerhout做了一些研究,我发现了函数式编程概念,如不变性和纯函数。 这些概念使你能够构建无副作用的功能,而函数式编程的一些优点,也使得系统变得更加容易维护。 在这篇文章中,我将通过 JavaScript 中的大量代码示例向您详细介绍函数式编程和一些重要概念。 什么是函数式编程?维基百科:Functional programming 函数式编程是一种编程范式 - 一种构建计算机程序结构和元素的方式 - 将计算视为数学函数的评估并避免改变状态和可变数据 - Wikipedia纯函数当我们想要理解函数式编程时,我们学到的第一个基本概念是纯函数。 那么我们怎么知道函数是否纯粹呢? 这是一个非常严格的纯度定义: 如果给出相同的参数,它返回相同的结果(它也称为确定性)它不会引起任何可观察到的副作用如果给出相同的参数,它返回相同的结果我们想要实现一个计算圆的面积的函数。 不纯的函数将接收半径:radius 作为参数,然后计算 radius * radius * PI : const PI = 3.14;const calculateArea = (radius) => radius * radius * PI;calculateArea(10); // returns 314为什么这是一个不纯的功能? 仅仅因为它使用的是未作为参数传递给函数的全局对象。 想象一下,数学家认为 PI 值实际上是 42, 并且改变了全局对象的值。 不纯的函数现在将导致 10 * 10 * 42 = 4200 .对于相同的参数(radius= 10),我们得到不同的结果。 我们来解决它吧! const PI = 3.14;const calculateArea = (radius, pi) => radius * radius * pi;calculateArea(10, PI); // returns 314现在我们将 PI 的值作为参数传递给函数。 所以现在我们只是访问传递给函数的参数。 没有外部对象(参数)。 ...

May 1, 2019 · 7 min · jiezi

在函数范式中构建程序

什么是函数范式函数式编程(英语:functional programming) 以演算为理论基础的编程范型, 将电脑运算视为数学上的函数计算, 并且避免使用程序状态以及易变对象.比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。电脑运算视为数学上的函数计算数学上的函数只要遵守计算准则, 输入和输出是确定的, 是可以被组合、可以被严格推理的$$f(x)=ax^2+bx+c$$复杂的函数是由简单的函数组合而成的, 函数范式也是由简单运算一层层组合为复杂执行过程的但电脑运算最大的障碍是电脑程序是会有副作用的因此函数范式的核心问题即是如何处理这些副作用副作用变量IO操作(文件读写、网络操作等)线程操作与系统的交互(GUI、硬件交互等)等强类型数学中函数是有严格定义域的, 即值的定义范围, 映射到程序中就是类型换句话说我们定义一个类型实际就是在定义一个值的范围(“范围"这个词可能不太准确,可能更类似“枚举”,比如“True”和“False”两个枚举的集合就是Boolean类型)电脑运算视为数学上的函数计算另一个过程即是定义域的转换, 如果数学函数需要严格的定义域, 程序中需要严格的类型限定也是很自然的事情fun plusOne(a: Int): Int函数名定义域值域plusOnea: Int: Int高阶类型、逆变协变、类型别名从数据结构开始构建程序程序 = 数据结构 + 算法OOP实际很多时候将数据结构和算法混杂到了一起, 函数范式则回归本源将两者分离:函数式程序 = 数据结构 + 算法程序编写开始之前, 首先定义数据结构函数式数据结构OOP提倡针对不同问题定义不同的数据结构, 这导致定义的数据结构通常不通用, 比如不同XML解析库会定义不同的专用数据类型函数范式则不同, 它们用很少一组关键数据结构(如list、set、map)来搭配专为这些数据结构深度优化过的操作。在这些关键数据结构和操作构成的一套运转机构上,按需要插入另外的数据结构和高阶函数来调整以适应具体问题。Example创造值域:sealed class ItemDatadata class ItemListData(…) : ItemData()data class ItemOptionData(…) : ItemData()List<ItemData>typealias Selecable<T> = Pair<T, Boolean>List<Selecable<ItemData>>不可变数据类型定义代数数据类型的一个基本限制是其中不能有变元, 这就是代数的的含义所以我们在函数范式中只能定义不可变数据比如连接两个list会产生一个新的list,对输入的两个list不做改变。从另一方面来解释这种限制:变量的存在往往是程序Bug的最终来源, 它的值依赖于运行时(线程切换状态、用户操作顺序、系统状态等等), 无法在测试期完全限制掉, 墨菲定律函数式数据结构中的数据共享当我们对一个已存在的列表xs在前面添加一个元素1的时候,返回一个新的列表,即Cons(1, xs),既然列表是不可变的,我们不需要去复制一份xs,可以直接复用它,这称为数据共享。共享不可变数据可以让函数实现更高的效率。我们可以返回不可变数据结构而不用担心后续代码修改它,不需要悲观地复制一份以避免对其修改或污染。所有关于更高效支持不同操作方式的纯函数式数据结构,其实都是找到一种聪明的方式来利用数据共享。正因如此,在大型程序里,函数式编程往往能取得比依赖副作用更好的性能。用组合代替继承OOP中算法和数据结构是混合在一起放在叫做类的东西里, 因此对算法(通常算法需要操作数据结构)的扩展需要通过继承的方式来实现, 这种处理方式有两个问题:多继承问题、难以组合难以额外扩展方法可以被复写意味着方法是可变的eg: 两种实现了不同功能的Activity继承扩展类无法被组合回溯上文, 我们说函数式程序 = 数据结构 + 算法, 数据结构被单独定义后, 算法就被独立出来了, 因此函数范式采用了更加灵活的方式组合这些算法类型类这些被分离出来的算法不同于OOP中的类, 所以函数范式中将之称之为类型类(typeclass)由于数据本身不可变, 所以不用像OOP一样需要严格private内聚一些内部方法以防止内部变量被非法操作后数据变得不符合预期因此函数范式中方法默认是public的Exampleinterface Eq<in F> { fun F.eqv(b: F): Boolean}interface Show<in A> { fun A.show(): String}interface ShowEq<in A> : Eq<A>, Show<A> { override fun A.show(): String = this.toString()}interface CharEqInstance : Eq<Char> { override fun Char.eqv(b: Char): Boolean = this == b}fun Char.Companion.eq(): Eq<Char> = object : CharEqInstance {}QuickCheck我们定义一个方法(函数)实际上就是在定义一个数学上的函数, 因此它应该是可以被严格验证的, 即指定定义域上值应该都能被映射到指定的值域测试在数学意义上就是做这个验证的, 输入所有定义域上的可能值验证是否正确映射到了值域传统的测试实际并没有覆盖全定义域, 因此可能导致某些额外情况. 函数范式中常用的测试框架QuickCheck即是尝试做全覆盖测试这也是为什么提倡对函数参数进行强类型化, 没有足够的类型限制实际就是扩展了函数的定义域, 这一般有两种情况:假定了输入数据的子域(比如参数是String, 却假定格式为1.1.2)包含了实际自己处理不了的数据(比如不能处理空数据, 却标明可以为空)甚至函数对象也是可以被生成的Exampleobject EqLaws { inline fun <F> laws(EQ: Eq<F>, noinline cf: (Int) -> F): List<Law> = listOf( Law(“Eq Laws: identity”) { EQ.identityEquality(cf) }, Law(“Eq Laws: commutativity”) { EQ.commutativeEquality(cf) } ) fun <F> Eq<F>.identityEquality(cf: (Int) -> F): Unit = forAll(Gen.int()) { int: Int -> val a = cf(int) a.eqv(a) == a.eqv(a) } fun <F> Eq<F>.commutativeEquality(cf: (Int) -> F): Unit = forAll(Gen.int()) { int: Int -> val a = cf(int) val b = cf(int) a.eqv(b) == b.eqv(a) }}副作用的分离副作用在程序中是确实存在的, 问题是如何将之分离出去, 函数范式的处理方式可以看做建造一个管道, 管道本身是代数的、确定的, 而其中流淌的就是副作用这些管道常见的有:IO、Single、Maybe等对于必须存在的变量的处理方法就是, 将之放到安全的地方, 一般而言即线程安全IOIO不同于我们通常意义上的input/output操作的意思, 它是Haskell中用来抽象外部作用的特殊数据结构, 它隐含了一个叫RealWorld的上下文用于和外部环境进行交互(即副作用), 所有的副作用必须包含在其中ExampleHaskell:putStrLn :: String -> IO ()getLine :: IO Stringmain = do name <- getLine putStrLn (“Hey, " ++ name)Kotlin:fun putStrLn(s: String): IO<Unit>fun getLine(): IO<String>IO.monad().binding { val s = readString().bind() putStrLn(s).bind()}Rx:fun putStrLn(s: String): Completablefun getLine(): Single<String>fun main() = getLine() .flatMapCompletable { s -> putStrLn(s) }main().blockingAwait() ...

April 8, 2019 · 2 min · jiezi

JavaScript 之函数式编程

同步发布于 https://github.com/xianshanna…是个程序员都知道函数,但是有些人不一定清楚函数式编程的概念。应用的迭代使程序变得越来越复杂,那么程序员很有必要创造一个结构良好、可读性好、重用性高和可维护性高的代码。函数式编程就是一个良好的代码方式,但是这不代表函数式编程是必须的。你的项目没用到函数式编程,不代表项目不好。什么是函数式编程(FP)?函数式编程关心数据的映射,命令式编程关心解决问题的步骤。函数式编程的对立面就是命令式编程。函数式编程语言中的变量也不是命令式编程语言中的变量,即存储状态的单元,而是代数中的变量,即一个值的名称。 变量的值是不可变的(immutable),也就是说不允许像命令式编程语言中那样多次给一个变量赋值。函数式编程只是一个概念(一致编码方式),并没有严格的定义。本人根据网上的知识点,简单的总结一下函数式编程的定义(本人总结,或许有人会不同意这个观点)。函数式编程就是纯函数的应用,然后把不同的逻辑分离为许多独立功能的纯函数(模块化思想),然后再整合在一起,变成复杂的功能。什么是纯函数?一个函数如果输入确定,那么输出结果是唯一确定的,并且没有副作用,那么它就是纯函数。一般符合上面提到的两点就算纯函数:相同的输入必定产生相同的输出在计算的过程中,不会产生副作用那怎么理解副作用呢?简单的说就是变量的值不可变,包括函数外部变量和函数内部变量。所谓副作用,指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。这里说明一下不可变,不可变指的是我们不能改变原来的变量值。或者原来变量值的改变,不能影响到返回结果。不是变量值本来就是不可变。纯函数特性对比例子上面的理论描述对于刚接触这个概念的程序员,或许不好理解。下面会通过纯函数的特点一一举例说明。输入相同返回值相同纯函数function test(pi) { // 只要 pi 确定,返回结果就一定确定。 return pi + 2;}test(3);非纯函数function test(pi) { // 随机数返回值不确定 return pi + Math.random();}test(3);返回值不受外部变量的影响非纯函数,返回值会被其他变量影响(说明有副作用),返回值不确定。let a = 2;function test(pi) { // a 的值可能中途被修改 return pi + a;}a = 3;test(3);非纯函数,返回值受到对象 getter 的影响,返回结果不确定。const obj = Object.create( {}, { bar: { get: function() { return Math.random(); }, }, });function test(obj) { // obj.a 的值是随机数 return obj.a;}test(obj);纯函数,参数唯一,返回值确定。function test(pi) { // 只要 pi 确定,返回结果就一定确定。 return pi + 2;}test(3);输入值是不可以被改变的非纯函数,这个函数已经改变了外面 personInfo 的值了(产生了副作用)。const personInfo = { firstName: ‘shannan’, lastName: ‘xian’ };function revereName(p) { p.lastName = p.lastName .split(’’) .reverse() .join(’’); p.firstName = p.firstName .split(’’) .reverse() .join(’’); return ${p.firstName} ${p.lastName};}revereName(personInfo);console.log(personInfo);// 输出 { firstName: ’nannahs’,lastName: ’naix’ }// personInfo 被修改了纯函数,这个函数不影响外部任意的变量。const personInfo = { firstName: ‘shannan’, lastName: ‘xian’ };function reverseName(p) { const lastName = p.lastName .split(’’) .reverse() .join(’’); const firstName = p.firstName .split(’’) .reverse() .join(’’); return ${firstName} ${lastName};}revereName(personInfo);console.log(personInfo);// 输出 { firstName: ‘shannan’,lastName: ‘xian’ }// personInfo 还是原值那么你们是不是有疑问,personInfo 对象是引用类型,异步操作的时候,中途改变了 personInfo,那么输出结果那就可能不确定了。如果函数存在异步操作,的确有存在这个问题,的确应该确保 personInfo 不能被外部再次改变(可以通过深度拷贝)。但是,这个简单的函数里面并没有异步操作,reverseName 函数运行的那一刻 p 的值已经是确定的了,直到返回结果。下面的异步操作才需要确保 personInfo 中途不会被改变:async function reverseName(p) { await new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); const lastName = p.lastName .split(’’) .reverse() .join(’’); const firstName = p.firstName .split(’’) .reverse() .join(’’); return ${firstName} ${lastName};}const personInfo = { firstName: ‘shannan’, lastName: ‘xian’ };async function run() { const newName = await reverseName(personInfo); console.log(newName);}run();personInfo.firstName = ’test’;// 输出为 tset naix,因为异步操作的中途 firstName 被改变了修改成下面的方式就可以确保 personInfo 中途的修改不影响异步操作:// 这个才是纯函数async function reverseName(p) { // 浅层拷贝,这个对象并不复杂 const newP = { …p }; await new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); const lastName = newP.lastName .split(’’) .reverse() .join(’’); const firstName = newP.firstName .split(’’) .reverse() .join(’’); return ${firstName} ${lastName};}const personInfo = { firstName: ‘shannan’, lastName: ‘xian’ };// run 不是纯函数async function run() { const newName = await reverseName(personInfo); console.log(newName);}// 当然小先运行 run,然后再去改 personInfo 对象。run();personInfo.firstName = ’test’;// 输出为 nannahs naix这个还是有个缺点,就是外部 personInfo 对象还是会被改到,但不影响之前已经运行的 run 函数。如果再次运行 run 函数,输入都变了,输出当然也变了。参数和返回值可以是任意类型那么返回函数也是可以的。function addX(y) { return function(x) { return x + y; };}尽量只做一件事当然这个要看实际应用场景,这里举个简单例子。两件事一起做(不太好的做法):function getFilteredTasks(tasks) { let filteredTasks = []; for (let i = 0; i < tasks.length; i++) { let task = tasks[i]; if (task.type === ‘RE’ && !task.completed) { filteredTasks.push({ …task, userName: task.user.name }); } } return filteredTasks;}const filteredTasks = getFilteredTasks(tasks);getFilteredTasks 也是纯函数,但是下面的纯函数更好。两件事分开做(推荐的做法):function isPriorityTask(task) { return task.type === ‘RE’ && !task.completed;}function toTaskView(task) { return { …task, userName: task.user.name };}let filteredTasks = tasks.filter(isPriorityTask).map(toTaskView);isPriorityTask 和 toTaskView 就是纯函数,而且都只做了一件事,也可以单独反复使用。结果可缓存根据纯函数的定义,只要输入确定,那么输出结果就一定确定。我们就可以针对纯函数返回结果进行缓存(缓存代理设计模式)。const personInfo = { firstName: ‘shannan’, lastName: ‘xian’ };function reverseName(firstName, lastName) { const newLastName = lastName .split(’’) .reverse() .join(’’); const newFirstName = firstName .split(’’) .reverse() .join(’’); console.log(‘在 proxyReverseName 中,相同的输入,我只运行了一次’); return ${newFirstName} ${newLastName};}const proxyReverseName = (function() { const cache = {}; return (firstName, lastName) => { const name = firstName + lastName; if (!cache[name]) { cache[name] = reverseName(firstName, lastName); } return cache[name]; };})();函数式编程有什么优点?实施函数式编程的思想,我们应该尽量让我们的函数有以下的优点:更容易理解更容易重复使用更容易测试更容易维护更容易重构更容易优化更容易推理函数式编程有什么缺点?性能可能相对来说较差函数式编程可能会牺牲时间复杂度来换取了可读性和维护性。但是呢,这个对用户来说这个性能十分微小,有些场景甚至可忽略不计。前端一般场景不存在非常大的数据量计算,所以你尽可放心的使用函数式编程。看下上面提到个的例子(数据量要稍微大一点才好对比):首先我们先赋值 10 万条数据:const tasks = [];for (let i = 0; i < 100000; i++) { tasks.push({ user: { name: ‘one’, }, type: ‘RE’, }); tasks.push({ user: { name: ’two’, }, type: ‘’, });}两件事一起做,代码可读性不够好,理论上时间复杂度为 o(n),不考虑 push 的复杂度。(function() { function getFilteredTasks(tasks) { let filteredTasks = []; for (let i = 0; i < tasks.length; i++) { let task = tasks[i]; if (task.type === ‘RE’ && !task.completed) { filteredTasks.push({ …task, userName: task.user.name }); } } return filteredTasks; } const timeConsumings = []; for (let k = 0; k < 100; k++) { const beginTime = +new Date(); getFilteredTasks(tasks); const endTime = +new Date(); timeConsumings.push(endTime - beginTime); } const averageTimeConsuming = timeConsumings.reduce((all, current) => { return all + current; }) / timeConsumings.length; console.log(第一种风格平均耗时:${averageTimeConsuming} 毫秒);})();两件事分开做,代码可读性相对好,理论上时间复杂度接近 o(2n)(function() { function isPriorityTask(task) { return task.type === ‘RE’ && !task.completed; } function toTaskView(task) { return { …task, userName: task.user.name }; } const timeConsumings = []; for (let k = 0; k < 100; k++) { const beginTime = +new Date(); tasks.filter(isPriorityTask).map(toTaskView); const endTime = +new Date(); timeConsumings.push(endTime - beginTime); } const averageTimeConsuming = timeConsumings.reduce((all, current) => { return all + current; }) / timeConsumings.length; console.log(第二种风格平均耗时:${averageTimeConsuming} 毫秒);})();上面的例子多次运行得出耗时平均值,在数据较少和较多的情况下,发现两者平均值并没有多大差别。10 万条数据,运行 100 次取耗时平均值,第二种风格平均多耗时 15 毫秒左右,相当于 10 万条数据多耗时 1.5 秒,1 万条数多据耗时 150 毫秒(150 毫秒用户基本感知不到)。虽然理论上时间复杂度多了一倍,但是在数据不庞大的情况下(会有个临界线的),这个性能相差其实并不大,完全可以牺牲浏览器用户的这点性能换取可读和可维护性。很可能被过度使用过度使用反而是项目维护性变差。有些人可能写着写着,就变成别人看不懂的代码,自己觉得挺高大上的,但是你确定别人能快速的看懂不? 适当的使用才是合理的。应用场景概念是概念,实际应用却是五花八门,没有实际应用,记住了也是死记硬背。这里总结一些常用的函数式编程应用场景。简单使用有时候很多人都用到了函数式的编程思想(最简单的用法),但是没有意识到而已。下面的列子就是最简单的应用,这个不用怎么说明,根据上面的纯函数特点,都应该看的明白。function sum(a, b) { return a + b;}立即执行的匿名函数匿名函数经常用于隔离内外部变量(变量不可变)。const personInfo = { firstName: ‘shannan’, lastName: ‘xian’ };function reverseName(firstName, lastName) { const newLastName = lastName .split(’’) .reverse() .join(’’); const newFirstName = firstName .split(’’) .reverse() .join(’’); console.log(‘在 proxyReverseName 中,相同的输入,我只运行了一次’); return ${newFirstName} ${newLastName};}// 匿名函数const proxyReverseName = (function() { const cache = {}; return (firstName, lastName) => { const name = firstName + lastName; if (!cache[name]) { cache[name] = reverseName(firstName, lastName); } return cache[name]; };})();JavaScript 的一些 API如数组的 forEach、map、reduce、filter 等函数的思想就是函数式编程思想(返回新数组),我们并不需要使用 for 来处理。const arr = [1, 2, ‘’, false];const newArr = arr.filter(Boolean);// 相当于 const newArr = arr.filter(value => Boolean(value))递归递归也是一直常用的编程方式,可以代替 while 来处理一些逻辑,这样的可读性和上手度都比 while 简单。如下二叉树所有节点求和例子:const tree = { value: 0, left: { value: 1, left: { value: 3, }, }, right: { value: 2, right: { value: 4, }, },};while 的计算方式:function sum(tree) { let sumValue = 0; // 使用列队方式处理,使用栈也可以,处理顺序不一样 const stack = [tree]; while (stack.length !== 0) { const currentTree = stack.shift(); sumValue += currentTree.value; if (currentTree.left) { stack.push(currentTree.left); } if (currentTree.right) { stack.push(currentTree.right); } } return sumValue;}递归的计算方式:function sum(tree) { let sumValue = 0; if (tree && tree.value !== undefined) { sumValue += tree.value; if (tree.left) { sumValue += sum(tree.left); } if (tree.right) { sumValue += sum(tree.right); } } return sumValue;}递归会比 while 代码量少,而且可读性更好,更容易理解。链式编程如果接触过 jquery,我们最熟悉的莫过于 jq 的链式便利了。现在 ES6 的数组操作也支持链式操作:const arr = [1, 2, ‘’, false];const newArr = arr.filter(Boolean).map(String);// 输出 “1”, “2”]或者我们自定义链式,加减乘除的链式运算:function createOperation() { let theLastValue = 0; const plusTwoArguments = (a, b) => a + b; const multiplyTwoArguments = (a, b) => a * b; return { plus(…args) { theLastValue += args.reduce(plusTwoArguments); return this; }, subtract(…args) { theLastValue -= args.reduce(plusTwoArguments); return this; }, multiply(…args) { theLastValue *= args.reduce(multiplyTwoArguments); return this; }, divide(…args) { theLastValue /= args.reduce(multiplyTwoArguments); return this; }, valueOf() { const returnValue = theLastValue; // 获取值的时候需要重置 theLastValue = 0; return returnValue; }, };}const operaton = createOperation();const result = operation .plus(1, 2, 3) .subtract(1, 3) .multiply(1, 2, 10) .divide(10, 5) .valueOf();console.log(result);当然上面的例子不完全都是函数式编程,因为 valueOf 的返回值就不确定。高阶函数高阶函数(Higher Order Function),按照维基百科上面的定义,至少满足下列一个条件的函数函数作为参数传入返回值为一个函数简单的例子:function add(a, b, fn) { return fn(a) + fn(b);}function fn(a) { return a * a;}add(2, 3, fn); // 13还有一些我们平时常用高阶的方法,如 map、reduce、filter、sort,以及现在常用的 redux 中的 connect 等高阶组件也是高阶函数。柯里化(闭包)柯里化(Currying),又称部分求值(Partial Evaluation),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。柯里化的作用以下优点:参数复用提前返回延迟计算/运行缓存计算值柯里化实质就是闭包。其实上面的立即执行匿名函数的例子就用到了柯里化。// 柯里化之前function add(x, y) { return x + y;}add(1, 2); // 3// 柯里化之后function addX(y) { return function(x) { return x + y; };}addX(2)(1); // 3高阶组件这是组件化流行后的一个新概念,目前经常用到。ES6 语法中 class 只是个语法糖,实际上还是函数。一个简单例子:class ComponentOne extends React.Component { render() { return <h1>title</h1>; }}function HocComponent(Component) { Component.shouldComponentUpdate = function(nextProps, nextState) { if (this.props.id === nextProps.id) { return false; } return true; }; return Component;}export default HocComponent(ComponentOne);深入理解高阶组件请看这里。无参数风格(Point-free)其实上面的一些例子已经使用了无参数风格。无参数风格不是没参数,只是省略了多余参数的那一步。看下面的一些例子就很容易理解了。范例一:const arr = [1, 2, ‘’, false];const newArr = arr.filter(Boolean).map(String);// 有参数的用法如下:// arr.filter(value => Boolean(value)).map(value => String(value));范例二:const tasks = [];for (let i = 0; i < 1000; i++) { tasks.push({ user: { name: ‘one’, }, type: ‘RE’, }); tasks.push({ user: { name: ’two’, }, type: ‘’, });}function isPriorityTask(task) { return task.type === ‘RE’ && !task.completed;}function toTaskView(task) { return { …task, userName: task.user.name };}tasks.filter(isPriorityTask).map(toTaskView);范例三:// 比如,现成的函数如下:var toUpperCase = function(str) { return str.toUpperCase();};var split = function(str) { return str.split(’’);};var reverse = function(arr) { return arr.reverse();};var join = function(arr) { return arr.join(’’);};// 现要由现成的函数定义一个 point-free 函数toUpperCaseAndReversevar toUpperCaseAndReverse = _.flowRight( join, reverse, split, toUpperCase); // 自右向左流动执行// toUpperCaseAndReverse是一个point-free函数,它定义时并无可识别参数。只是在其子函数中操纵参数。flowRight 是引入了 lodash 库的组合函数,相当于 compose 组合函数console.log(toUpperCaseAndReverse(‘abcd’)); // => DCBA无参数风格优点?参风格的好处就是不需要费心思去给它的参数进行命名,把一些现成的函数按需组合起来使用。更容易理解、代码简小,同时分离的回调函数,是可以复用的。如果使用了原生 js 如数组,还可以利用 Boolean 等构造函数的便捷性进行一些过滤操作。无参数风格缺点?缺点就是需要熟悉无参数风格,刚接触不可能就可以用得得心应手的。对于一些新手,可能第一时间理解起来没那没快。参考文章Learn the fundamentals of functional programming — for free, in your inboxMake your code easier to read with Functional Programming从高阶函数—>高阶组件 ...

April 7, 2019 · 6 min · jiezi

乐字节-Java8新特性之函数式接口

上一篇小乐带大家学过 Java8新特性-Lambda表达式,什么时候可以使用Lambda?通常Lambda表达式是用在函数式接口上使用的。从Java8开始引入了函数式接口,其说明比较简单:函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。 java8引入@FunctionalInterface 注解声明该接口是一个函数式接口。1、语法定义/** * 定义函数式接口 * 接口上标注@FunctionalInterface 注解 /@FunctionalInterfacepublic interface ICollectionService { /* * 定义打印方法 / void print();}在Java8 以前,已有大量函数式接口形式的接口(接口中只存在一个抽象方法),只是没有强制声明。例如:java.lang.Runnablejava.util.concurrent.Callablejava.security.PrivilegedActionjava.io.FileFilterjava.nio.file.PathMatcherjava.lang.reflect.InvocationHandlerjava.beans.PropertyChangeListenerjava.awt.event.ActionListenerjavax.swing.event.ChangeListenerJava8 新增加的函数接口在java.util.function 包下,它包含了很多类,用来支持 Java的 函数式编程,该包中的函数式接口有: 序号 接口 & 描述 1 BiConsumer<T,U>代表了一个接受两个输入参数的操作,并且不返回任何结果 2 BiFunction<T,U,R>代表了一个接受两个输入参数的方法,并且返回一个结果 3 BinaryOperator<T>代表了一个作用于于两个同类型操作符的操作,并且返回了操作符同类型的结果 4 BiPredicate<T,U>代表了一个两个参数的boolean值方法 5 BooleanSupplier代表了boolean值结果的提供方 6 Consumer<T>代表了接受一个输入参数并且无返回的操作 7 DoubleBinaryOperator代表了作用于两个double值操作符的操作,并且返回了一个double值的结果。 8 DoubleConsumer代表一个接受double值参数的操作,并且不返回结果。 9 DoubleFunction<R>代表接受一个double值参数的方法,并且返回结果 10 DoublePredicate代表一个拥有double值参数的boolean值方法 11 DoubleSupplier代表一个double值结构的提供方 12 DoubleToIntFunction接受一个double类型输入,返回一个int类型结果。 13 DoubleToLongFunction接受一个double类型输入,返回一个long类型结果 14 DoubleUnaryOperator接受一个参数同为类型double,返回值类型也为double 。 15 Function<T,R>接受一个输入参数,返回一个结果。 16 IntBinaryOperator接受两个参数同为类型int,返回值类型也为int 。 17 IntConsumer接受一个int类型的输入参数,无返回值 。 18 IntFunction<R>接受一个int类型输入参数,返回一个结果 。 19 IntPredicate:接受一个int输入参数,返回一个布尔值的结果。 20 IntSupplier无参数,返回一个int类型结果。 21 IntToDoubleFunction接受一个int类型输入,返回一个double类型结果 。 22 IntToLongFunction接受一个int类型输入,返回一个long类型结果。 23 IntUnaryOperator接受一个参数同为类型int,返回值类型也为int 。 24 LongBinaryOperator接受两个参数同为类型long,返回值类型也为long。 25 LongConsumer接受一个long类型的输入参数,无返回值。 26 LongFunction<R>接受一个long类型输入参数,返回一个结果。 27 LongPredicateR接受一个long输入参数,返回一个布尔值类型结果。 28 LongSupplier无参数,返回一个结果long类型的值。 29 LongToDoubleFunction接受一个long类型输入,返回一个double类型结果。 30 LongToIntFunction接受一个long类型输入,返回一个int类型结果。 31 LongUnaryOperator接受一个参数同为类型long,返回值类型也为long。 32 ObjDoubleConsumer<T>接受一个object类型和一个double类型的输入参数,无返回值。 33 ObjIntConsumer<T>接受一个object类型和一个int类型的输入参数,无返回值。 34 ObjLongConsumer<T>接受一个object类型和一个long类型的输入参数,无返回值。 35 Predicate<T>接受一个输入参数,返回一个布尔值结果。 36 Supplier<T>无参数,返回一个结果。 37 ToDoubleBiFunction<T,U>接受两个输入参数,返回一个double类型结果 38 ToDoubleFunction<T>接受一个输入参数,返回一个double类型结果 39 ToIntBiFunction<T,U>接受两个输入参数,返回一个int类型结果。 40 ToIntFunction<T>接受一个输入参数,返回一个int类型结果。 41 ToLongBiFunction<T,U>接受两个输入参数,返回一个long类型结果。 42 ToLongFunction<T>接受一个输入参数,返回一个long类型结果。 43 UnaryOperator<T>接受一个参数为类型T,返回值类型也为T。 对于Java8中提供的这么多函数式接口,开发中常用的函数式接口有以下几个Predicate,Consumer,Function,Supplier。2、函数式接口实例2.1、Predicatejava.util.function.Predicate<T> 接口定义了一个名叫 test 的抽象方法,它接受泛型 T 对象,并返回一个boolean值。在对类型 T进行断言判断时,可以使用这个接口。通常称为断言性接口 。 使用Predicate接口实现字符串判空操作@FunctionalInterfacepublic interface Predicate<T> { /* * Evaluates this predicate on the given argument. * * @param t the input argument * @return {@code true} if the input argument matches the predicate, * otherwise {@code false} / boolean test(T t); …}public static void main(String[] args) { /* * 借助Lambda 表达式实现Predicate test方法 / Predicate<String> p01=(str)->str.isEmpty()||str.trim().isEmpty(); /* * 测试传入的字符串是否为空 / System.out.println(p01.test("")); System.out.println(p01.test(" “)); System.out.println(p01.test(“admin”));}测试代码public static void main(String[] args) { /* * 借助Lambda 表达式实现Predicate test方法 / Predicate<String> p01=(str)->str.isEmpty()||str.trim().isEmpty(); /* * 测试传入的字符串是否为空 / System.out.println(p01.test(”")); System.out.println(p01.test(" “)); System.out.println(p01.test(“admin”));}测试结果:2.2、Consumerjava.util.function.Consumer<T>接口定义了一个名叫 accept 的抽象方法,它接受泛型T,没有返回值(void)。如果需要访问类型 T 的对象,并对其执行某些操作,可以使用这个接口,通常称为消费性接口。 使用Consumer实现集合遍历操作@FunctionalInterfacepublic interface Consumer<T> { /* * Performs this operation on the given argument. * * @param t the input argument / void accept(T t); …}/** 借助Lambda表达式实现Consumer accept方法*/Consumer<Collection> c01 = (collection) -> {if (null != collection && collection.size() > 0) {for (Object c : collection) {System.out.println(c);}}};List<String> list = new ArrayList<String>();list.add(“诸葛亮”);list.add(“曹操”);list.add(“关羽”);// 遍历list 输出元素内容到控制台c01.accept(list);2.3、Functionjava.util.function.Function<T, R>接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。如果需要定义一个Lambda,将输入的信息映射到输出,可以使用这个接口(比如提取苹果的重量,或把字符串映射为它的长度),通常称为功能性接口。使用Function实现用户密码 Base64加密操作@FunctionalInterfacepublic interface Function<T, R> { /** * Applies this function to the given argument. * * @param t the function argument * @return the function result / R apply(T t);}// 实现用户密码 Base64加密操作Function<String,String> f01=(password)->Base64.getEncoder().encodeToString(password.getBytes());// 输出加密后的字符串System.out.println(f01.apply(“123456”));加密后结果如下:2.4、Supplierjava.util.function.Supplier<T>接口定义了一个get的抽象方法,它没有参数,返回一个泛型T的对象,这类似于一个工厂方法,通常称为功能性接口。使用Supplier实现SessionFactory创建@FunctionalInterfacepublic interface Supplier<T> { /* * Gets a result. * * @return a result / T get();}/* * 产生一个session工厂对象 */Supplier<SessionFactory> s = () -> { return new SessionFactory();};s.get().info();以上就是小乐带给大家的Java8新特性之函数式接口,下一篇将会为大家带来Java8新特性之方法引用,敬请关注。转载请注明文章出处和作者,谢谢合作! ...

April 4, 2019 · 2 min · jiezi

DslAdapter开发简介

DslAdapter开发简介DslAdapter是一个Android RecyclerView的Adapter构建器, DSL语法, 面向组合子设计. 专注类型安全, 所有代码采用Kotlin编写.为什么要开发DslAdapter实际上在DslAdapter开始开发的时点已经有很多RecyclerAdapter的扩展库存在了, 有的甚至从2015年开始已经持续开发到了现在. 从功能上来说, 这些库的各项功能都非常成熟了, 几乎所有的需求都有涉及; 而从思想上来说, 各种构建方式都有相应的库 以现在很常见的库举例:CymChad/BaseRecyclerViewAdapterHelper 1w6 star这是Github上检索出Star最多的RecyclerAdapter的库, 它支持添加Item事件、添加列表加载动画、添加头部、尾部、树形列表等等,甚至设置空布局FastAdapter 这个库是以前项目中也使用过的库, 功能也相当丰富, 相比上面的库更注重List的功能, 是一个从2015年开始开发一直持续维护的库KidAdapter kotlin编写,DSL语法的构建器,利用了kotlin的语法特性,构建上更简单,同时也实现了types功能。但功能上相比上面的库就简单很多了甚至我多年前也已经写过一个相关库AdapterRenderer,也实现了很多功能。可以说RecyclerAdapter领域是最难以有新突破的地方了,似乎能做的只是在现有基础上小修小改而已了。但现有的库真的已经完美到这种程度了吗?不,现有的库也有很多的缺点:非强类型,为了兼容多类型而直接忽略数据类型信息,失去了编译期类型检查,只能靠编写时自我约束繁琐的模板代码。有些库会需要继承基础Adapter或者基础Holder继承方法来实现功能,因此会写大量的XXAdapter、XXItem逻辑代码被迫分离。也是由于需要单独写XXAdapter、XXItem,而这些小类抽象度低,该界面的逻辑被迫分离到了这些小类中,即使是业务联系很大的逻辑功能过于繁杂,抽象度低。功能上虽然丰富,但实际上包括了很多并不是RecyclerView应该关注的功能,导致变成了一个全功能的Adapter,Adapter的逻辑结构极其复杂,难以维护也难以扩展类中变量和算法混杂,需要时常注意状态的同步问题正因如此,为了解决以上这些问题,让我们编写的Adapter更简单、更安全,从而有了开发一个新库的想法一个新的Adapter库应该是什么样的想要构建一个Adapter的库,首先我们要想想我们这个新库应该是干什么的,这就需要回到RecyclerView这个库中Adapter被定义为什么。RecyclerAdapter被定义为数据适配器,即将数据和视图进行绑定:数据 –映射–> 视图List而RecyclerView之所以很强大是因为它已经不仅仅是用于显示List,它会需要显示复杂的视图结构,比如树状图、可展开列表等等数据 –映射–> 复杂结构 –渲染–> 视图List我们的Adapter库需要完成的工作简单来说就是:将数据变换为复杂的抽象结构(比如树状),再将复杂的抽象结构渲染为视图List(因为RecyclerView最终只支持平整单列表)开始构建定义基本组合子实际我们需要实现的是一个变换问题,无论最终我们需要的抽象结构是简单的List还是复杂的树状图本质上都只是做这么一个数据的变换,因此这个变换函数就是我们的基础组合子,我们可以通过基础组合子的相互组合实现复杂功能这个基本组合子就是DslAdapter库中的BaseRenderer类:interface Renderer<Data, VD : ViewData<Data>> { fun getData(content: Data): VD fun getItemId(data: VD, index: Int): Long = RecyclerView.NO_ID fun getItemViewType(data: VD, position: Int): Int fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder fun bind(data: VD, index: Int, holder: RecyclerView.ViewHolder) fun recycle(holder: RecyclerView.ViewHolder)}它包含作为RecyclerAdapter基础组合子需要的几个基本方法。数据放在哪儿函数范式中副作用是要严格分离的,而变量就是一种副作用,如果允许变量在Renderer中不受管制的存在会使Renderer组合子本身失去可组合性,同时数据也变得很不可靠(线程不安全、Renderer并不保证只在一个地方使用一次)而可以看到Renderer的基础方法中定义的都是纯函数,并且不包含允许副作用存在的IO等类型(这里的IO不同于Java中的input/output,而是指Haskell中的IO类型类),因此数据被设计为与Renderer严格分离(数据与算法的严格分离),ViewData即是这个被分离的数据interface ViewData<out OriD> : ViewDataOf<OriD> { val count: Int val originData: OriD}Adapter设计根据前一章的描述,我们构建的Renderer就是一个映射函数,因此Adapter也只需要使用这个映射函数将数据进行渲染即可class RendererAdapter<T, VD : ViewData<T>>( val initData: T, val renderer: BaseRenderer<T, VD>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { private val dataLock = Any() private var adapterViewData: VD = renderer.getData(initData) … override fun getItemCount(): Int = adapterViewData.count override fun getItemViewType(position: Int): Int = renderer.getItemViewType(adapterViewData, position) override fun getItemId(position: Int): Long = renderer.getItemId(adapterViewData, position) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = renderer.onCreateViewHolder(parent, viewType) override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) = renderer.bind(adapterViewData, position, holder) override fun onFailedToRecycleView(holder: RecyclerView.ViewHolder): Boolean { renderer.recycle(holder) return super.onFailedToRecycleView(holder) } override fun onViewRecycled(holder: RecyclerView.ViewHolder) { renderer.recycle(holder) }}可以看到Adapter实际只是将Renderer中的各个函数实际应用于原生RecyclerView.Adapter的各个方法中即可,极其简单另外,ViewData只保存于Adapter中一份,它其实就包含整个Adapter的所有状态,换句话说只要保存它,可以完全恢复RecyclerView的数据状态;另外ViewData是被锁保护起来的,保证数据的线程安全性定义基础Renderer组合子Renderer被我们定义为基础组合子,那我们需要哪些Renderer呢:EmptyRenderer: 空Renderer, count为0LayoutRenderer: 与View绑定的末端Renderer, 可自定义数量ConstantItemRenderer: 将常量绑定到View的末端Renderer, 可适配任意数据源, 可自定义数量MapperRenderer: 转换目标Renderer的数据源类型, 一般通过mapT()来使用它ListRenderer: 将目标Renderer转换为适配列表数据源SealedItemRenderer: 根据数据源具体数据选择不同的Renderer渲染, 比如对于Int?类型,可以在为null的时候用EmptyRenderer渲染; 不为null的时候使用LayoutRenderer渲染ComposeRenderer: 组合多个不同RendererDataBindingRenderer : Android Databinding支持的Renderer复杂的结构基本都可以通过组合他们来实现:val adapter = RendererAdapter.multipleBuild() .add(layout<Unit>(R.layout.list_header)) .add(none<List<Option<ItemModel>>>(), optionRenderer( noneItemRenderer = LayoutRenderer.dataBindingItem<Unit, ItemLayoutBinding>( count = 5, layout = R.layout.item_layout, bindBinding = { ItemLayoutBinding.bind(it) }, binder = { bind, item, _ -> bind.content = “this is empty item” }, recycleFun = { it.model = null; it.content = null; it.click = null }), itemRenderer = LayoutRenderer.dataBindingItem<Option<ItemModel>, ItemLayoutBinding>( count = 5, layout = R.layout.item_layout, bindBinding = { ItemLayoutBinding.bind(it) }, binder = { bind, item, _ -> bind.content = “this is some item” }, recycleFun = { it.model = null; it.content = null; it.click = null }) .forList() )) .build()以上Adapter可图示为:|–LayoutRenderer header||–SealedItemRenderer| |–none -> LayoutRenderer placeholder count 5| || |–some -> ListRenderer| |–DataBindingRenderer 1| |–DataBindingRenderer 2| |–…技术细节特征类型上面说到Renderer被定义为:Renderer<T, VD : ViewData<T>>, 其中ViewData因为和Renderer是强绑定的,所以往往一种ViewData和一种Renderer是一一对应的,在类型上ViewData和Renderer是相等的用法上来说可以这样来看:类型Renderer<T, VD : LayoutViewData<T>>可以被等价看待为LayoutRenderer<T>, 相同的道理, 由于Updater和ViewData也是一一对应的关系, 因此类型Updater<T, VD : LayoutViewData<T>>可以被等价看待为LayoutUpdater<T>因此类型LayoutViewData<T>可以看做LayoutXX系列所有类的一个特征类型:通过这个类型可以唯一地识别出其对应的原始类型在高阶类型的Java类型系统实现中也使用了类似的手法:可以注意到VIewData的原始定义中继承了ViewDataOf类型,这个类型原始定义是这样的:class ForViewData private constructor() { companion object }typealias ViewDataOf<T> = Kind<ForViewData, T>@Suppress(“UNCHECKED_CAST”, “NOTHING_TO_INLINE”)inline fun <T> ViewDataOf<T>.fix(): ViewData<T> = this as ViewData<T>其中ForViewData就是我们定义的特征类型,在fix()函数中我们通过识别Kind<ForViewData, T>中的ForViewData部分即可识别为其唯一对应的ViewData<T>,从而安全地进行类型转换注意,特征类型只是用于类型系统识别用,代码中我们实际并不会使用它的实例,因此可以看到上面定义的ForViewData类型是私有构造函数,无法被实例化另外,实际只要是具有唯一性任意类型都可以作为特征类型,可以利用已有的类型(比如LayoutViewData)、也可以单独定义(比如ForViewData)利用特征类型我们可以更灵活使用类型(见Kotlin与高阶类型),也可以简化冗余的类型信息比如之前版本的DslAdapter中,Adapter的泛型信息为:<T, VD : ViewData<T>, BR : BaseRenderer<T, VD>>包含了T、VD和BR三个类型信息,但根据我们上面的分析,VD和BR两个类型实际是等价的,他们包含了相同的类型特征,因此我们可以将其简化为<T, VD : ViewData<T>,两个泛型即可保留所有我们需要的类型信息对于复杂的Adapter这种简化对于减少编译系统压力来说的是更加明显的:比如原来的类型有6000多字符:ComposeRenderer<HConsK<ForIdT, Pair<ED, SearchConditions>, HConsK<ForIdT, Pair<ED, SearchConditions>, HNilK<ForIdT>>>, HConsK<ForComposeItem, Pair<Pair<ED, SearchConditions>, SealedItemRenderer<Pair<ED, SearchConditions>, HConsK<Kind<ForSealedItem, Pair<ED, SearchConditions>>, Pair<List<Pair<ED, CategoryInfo>>, ListRenderer<List<Pair<ED, CategoryInfo>>, Pair<ED, CategoryInfo>, MapperViewData<Pair<ED, CategoryInfo>, HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, ComposeViewData<HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, HConsK<ForComposeItemData, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItemData, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItemData>>>>>, MapperRenderer<Pair<ED, CategoryInfo>, HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, ComposeViewData<HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, HConsK<ForComposeItemData, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItemData, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItemData>>>>, ComposeRenderer<HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, HConsK<ForComposeItem, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItem, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItem>>>, HConsK<ForComposeItemData, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItemData, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItemData>>>>>>>, HConsK<Kind<ForSealedItem, Pair<ED, SearchConditions>>, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HNilK<Kind<ForSealedItem, Pair<ED, SearchConditions>>>>>>>, HConsK<ForComposeItem, Pair<Pair<ED, SearchConditions>, MapperRenderer<Pair<ED, SearchConditions>, List<Pair<ED, NormalCategoryInfo>>, ListViewData<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>>, HNilK<ForComposeItem>>>, HConsK<ForComposeItemData, Pair<Pair<ED, SearchConditions>, SealedItemRenderer<Pair<ED, SearchConditions>, HConsK<Kind<ForSealedItem, Pair<ED, SearchConditions>>, Pair<List<Pair<ED, CategoryInfo>>, ListRenderer<List<Pair<ED, CategoryInfo>>, Pair<ED, CategoryInfo>, MapperViewData<Pair<ED, CategoryInfo>, HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, ComposeViewData<HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, HConsK<ForComposeItemData, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItemData, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItemData>>>>>, MapperRenderer<Pair<ED, CategoryInfo>, HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, ComposeViewData<HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, HConsK<ForComposeItemData, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItemData, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItemData>>>>, ComposeRenderer<HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, HConsK<ForComposeItem, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItem, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItem>>>, HConsK<ForComposeItemData, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItemData, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItemData>>>>>>>, HConsK<Kind<ForSealedItem, Pair<ED, SearchConditions>>, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HNilK<Kind<ForSealedItem, Pair<ED, SearchConditions>>>>>>>, HConsK<ForComposeItemData, Pair<Pair<ED, SearchConditions>, MapperRenderer<Pair<ED, SearchConditions>, List<Pair<ED, NormalCategoryInfo>>, ListViewData<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>>, HNilK<ForComposeItemData>>>>简化后只有1900多个字符:ComposeRenderer<HConsK<ForIdT, Pair<ED, SearchConditions>, HConsK<ForIdT, Pair<ED, SearchConditions>, HNilK<ForIdT>>>, HConsK<ForComposeItemData, Pair<Pair<ED, SearchConditions>, SealedViewData<Pair<ED, SearchConditions>, HConsK<Kind<ForSealedItem, Pair<ED, SearchConditions>>, Pair<List<Pair<ED, CategoryInfo>>, ListViewData<List<Pair<ED, CategoryInfo>>, Pair<ED, CategoryInfo>, MapperViewData<Pair<ED, CategoryInfo>, HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, ComposeViewData<HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, HConsK<ForComposeItemData, Pair<List<Pair<ED, NormalCategoryInfo>>, ListViewData<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD>>, HConsK<ForComposeItemData, Pair<CategoryInfo, SealedViewData<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyViewData<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingViewData<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItemData>>>>>>>, HConsK<Kind<ForSealedItem, Pair<ED, SearchConditions>>, Pair<List<Pair<ED, NormalCategoryInfo>>, ListViewData<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD>>, HNilK<Kind<ForSealedItem, Pair<ED, SearchConditions>>>>>>>, HConsK<ForComposeItemData, Pair<Pair<ED, SearchConditions>, MapperViewData<Pair<ED, SearchConditions>, List<Pair<ED, NormalCategoryInfo>>, ListViewData<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD>>>, HNilK<ForComposeItemData>>>>递归类型函数式列表函数范式中有List类型,但与OOP中的列表的结构是完全不同的:List a = Nil | Cons a (List a)或者用kotlin来描述为:sealed class List<A>object Nil : List<Nothing>data class Cons<T>(val head: T, val tail: List<T>) : List<T>它可以看做是一个自包含的数据结构:如果没有数据就是Nil;如果有数据则包含一个数据以及下一个List<T>,直到取到Nil结束即递归数据结构异构列表强类型化最困难的在于ComposeRenderer的强类型化,ComposeRenderer可以实现的是组合不同的Renderer:|– item1 (eg: itemRenderer)|– item2 (eg: stringRenderer)val composeRenderer = ComposeRenderer.startBuild .add(itemRenderer) .add(stringRenderer) .build()原始的实现方法可以通过一个List来保存:List<BaseRenderer>但这样就丢失了每个元素的类型特征信息,比如我们无法知道第二个元素是stringRenderer还是itemRenderer,这也是现有所有库都存在的问题,常常我们只能使用强制类型转换的方式来得到我们希望的类型,但没有类型系统的检查这种转换是不安全的,只能在运行期被检查出来无论是OOP的列表还是上面介绍的函数式列表都无法满足这个需求,他们在add()的时候都把类型特征丢弃了而异构列表可以将这些保留下来:sealed class HListobject HNil : HList()data class HCons<out H, out T : HList>(val head: H, val tail: T) : HList()可以看到它的数据结构和原始的函数式列表的结构很相似,都是递归数据结构但它在泛型中增加了out T : HList,这是一个引用了自己类型的泛型声明,即:HList = HCons<T1, HList>HList = HCons<T1, HCons<T2, HList>>HList = HCons<T1, HCons<T2, HCons<T3, HList>>>HList = HCons<T1, HCons<T2, HCons<T3, HCons<T4, HList>>>>…它的类型可以在不断的代换中形成类型列表,这种即是递归类型使用上:// 原函数fun test2(s: String, i: Int): List<Any?> = listOf(s, i)// 异构列表fun test2(s: String, i: Int): HCons<Int, HCons<String, HNil>> = HCons(i, HCons(s, HNil))同样是构建列表, 异构列表包含了更丰富的类型信息:容器的size为2容器中第一个元素为String, 第二个为Int相比传统列表,异构列表的优势:完整保存所有元素的类型信息自带容器的size信息完整保存每个元素的位置信息这是基本的异构列表,DslAdapter为了做类型限定而自定义了高阶异构列表,可以参考源码HListK.kt递归类型递归类型是指的包含有自己的类型声明:fun <DL : HListK<ForIdT, DL>> test() = …这种泛型可以在不断代换中形成上面描述的类型列表:HList = HCons<T1, HList>HList = HCons<T1, HCons<T2, HList>>HList = HCons<T1, HCons<T2, HCons<T3, HList>>>HList = HCons<T1, HCons<T2, HCons<T3, HCons<T4, HList>>>>…在描述数量不确定的类型时很有用辅助类型在使用DslAdapter中可能会注意到有时会有一个奇特的参数type:fun <T, D, VD : ViewData<D>> BaseRenderer<D, VD>.mapT(type: TypeCheck<T>, mapper: (T) -> D, demapper: (oldData: T, newMapData: D) -> T) : MapperRenderer<T, D, VD> = …这个参数的实际值并不会在函数中被使用到,而跳转到TypeCheck的定义中:class TypeCheck<T>private val typeFake = TypeCheck<Nothing>()@Suppress(“UNCHECKED_CAST”)fun <T> type(): TypeCheck<T> = typeFake as TypeCheck<T>TypeCheck只是一个没有任何功能的空类型,返回的值也是固定的同一个值,那这个参数究竟是用来干什么的?要解释这个问题我们需要看一下mapT这个函数的调用:LayoutRenderer<String>(MOCK_LAYOUT_RES, 3) .mapT(type = type<TestModel>(), mapper = { it.msg }, demapper = { oldData, newMapData -> oldData.copy(msg = newMapData) })上面这段代码的作用是将接收String数据类型的Renderer转换为接受TestModel数据类型如果我们不使用type这个参数的话这段代码会变成什么样呢:LayoutRenderer<String>(MOCK_LAYOUT_RES, 3) .map<TestModel, String, LayoutViewData<String>>( mapper = { it.msg }, demapper = { oldData, newMapData -> oldData.copy(msg = newMapData) })可以看到由于函数的第一个泛型T类型系统也无法推测出来为TestModel,因此需要我们显式地把T的类型给写出来,但kotlin和Java的语法中无法只写泛型声明中的一项,所以我们不得不把其他可以被推测出来的泛型也显式的声明出来,不仅代码繁杂而且在类型复杂的时候几乎是不可完成的工作参数type中的泛型<T>就可以用于辅助编译器进行类型推测,我们只需要手动声明编译器难以自动推测的部分泛型,而其他可以被推测的泛型还是交由编译器自动完成,即可简化代码而这里的type = type<TestModel>()即是辅助类型,它的实例本身没有任何作用,它只是为了辅助编译器进行类型检查最后DslAdapter致力于完整的静态类型,使用了各种类型编程的技法,这是因为足够的类型信息不仅能帮编译器进行类型检查、早期排除问题,而且能够帮助我们在编码的时候编写足够准确的代码准确的代码意味着我们即不需要处理不会发生的额外情况、也不会少处理了可能的情况。(早期版本的DslAdapter的更新模块就是被设计为自动检查更新,导致需要处理大量额外情况,极其不安全)同时DslAdapter本身不是一个希望做到全能的库,它的目标是将Adapter的工作足够简化,并只专注于Adapter工作,其他功能就交给专注其他功能的库Do One Thing and Do It Well.但这并不意味着DslAdapter是一个抛弃了功能性的库,相反,它是一个极其灵活的库。它的核心被设计得非常简单,只有BaseRenderer和RendererAdapter, 这两个类也相当简单,并且由于全局只有一个变量被保存在RendererAdapter中,保证了数据的线程安全性。而数据中都是不可变属性,使内部数据也可以被完全暴露出来,方便了功能的扩展实际上DSL更新器 dsladapter-updater模块以及DSL position获取器 dsladapter-position模块都是以扩展方式存在的,后续还会根据需求继续扩展其他模块本文只是浅尝则止地介绍了一点DslAdapter的开发技巧,欢迎Star和提出issue ...

April 3, 2019 · 5 min · jiezi

Js-函数式编程

前言JavaScript是一门多范式语言,即可使用OOP(面向对象),也可以使用FP(函数式),由于笔者最近在学习React相关的技术栈,想进一步深入了解其思想,所以学习了一些FP相关的知识点,本文纯属个人的读书笔记,如果有错误,望轻喷且提点。什么是函数式编程函数式编程(英语:functional programming)或称函数程序设计、泛函编程,是一种编程范式,它将计算机运算视为函数运算,并且避免使用程序状态以及易变对象。即对过程进行抽象,将数据以输入输出流的方式封装进过程内部,从而也降低系统的耦合度。为什么Js支持FPJs支持FP的一个重要原因在于,在JS中,函数是一等公民。即你可以像对其他数据类型一样对其进行操作,把他们存在数组里,当作参数传递,赋值给变量…等等。如下:const func = () => {}// 存储const a = [func]// 参数 返回值const x = (func) => { …… …… return func}x(func)这个特性在编写语言程序时带来了极大的便利,下面的知识及例子都建立在此基础上。纯函数概念纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。 副作用包括但不限于:打印/log发送一个http请求可变数据DOM查询简单一句话, 即只要是与函数外部环境发生交互的都是副作用。 像Js中, slice就是纯函数, 而splice则不是var xs = [1,2,3,4,5];// 纯的xs.slice(0,3);//=> [1,2,3]xs.slice(0,3);//=> [1,2,3]xs.slice(0,3);//=> [1,2,3]// 不纯的xs.splice(0,3);//=> [1,2,3]xs.splice(0,3);//=> [4,5]xs.splice(0,3);//=> []例子在React生态中,使用纯函数的例子很常见,如React Redner函数,Redux的reducer,Redux-saga的声明式effects等等。 React Render 在React中,Render返回了一个JSX表达式,只要输入相同,即可以保证我们拿到同样的输出(最终结果渲染到DOM上),而内部的封装细节我们不需要关心,只要知道它是没有副作用的,这在我们开发过程中带来了极大的便利。当我们的程序出问题时(渲染出来与预期不符合),我们只要关心我们的入参是否有问题即可。class Component extends React.Component { render() { return ( <div /> ) }}Redux的reducer Redux的reducer函数要求我们每一次都要返回一个新的state, 并且在其中不能有任何副作用,只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。这样做可以使得我们很容易的保存了每一次state改变的情况,对于时间旅行这种需求更是天然的亲近。特别是在调试的过程中,我们可以借助插件,任意达到每一个state状态,能够轻松的捕捉到错误是在哪一个节点出现。function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) case ADD_TODO: return Object.assign({}, state, { todos: [ …state.todos, { text: action.text, completed: false } ] }) default: return state }}Redux-sage的声明式effects 许多时候, 我们会写这样的函数const sendRequest = () => { return axions.post(…)}这是一个不纯的函数,因为它包含了副作用,发起了http请求,我们可以这样封装一下:const sendRequestReducer = () => { return () => { return axios.post(…) }}ok, 现在是一个纯函数了,正如Redux-saga中的effects一样:import { call } from ‘redux-saga/effects’function* fetchProducts() { const products = yield call(Api.fetch, ‘/products’) // …}实际上call不立即执行异步调用,相反,call 创建了一条描述结果的信息。那么这样做除了增加代码的复杂度,还可以给我们带来什么?参考saga的官方文档就知道了, 答案是测试:这些 声明式调用(declarative calls) 的优势是,我们可以通过简单地遍历 Generator 并在 yield 后的成功的值上面做一个 deepEqual 测试, 就能测试 Saga 中所有的逻辑。这是一个真正的好处,因为复杂的异步操作都不再是黑盒,你可以详细地测试操作逻辑,不管它有多么复杂。import { call } from ‘redux-saga/effects’import Api from ‘…‘const iterator = fetchProducts()// expects a call instructionassert.deepEqual( iterator.next().value, call(Api.fetch, ‘/products’), “fetchProducts should yield an Effect call(Api.fetch, ‘./products’)")总结纯函数有着以下的优点 可缓存性 首先,纯函数总能够根据输入来做缓存。实现缓存的一种典型方式是 memoize 技术:var memoize = function(f) { var cache = {}; return function() { var arg_str = JSON.stringify(arguments); cache[arg_str] = cache[arg_str] || f.apply(f, arguments); return cache[arg_str]; };};var squareNumber = memoize(function(x){ return xx; });squareNumber(4);//=> 16squareNumber(4); // 从缓存中读取输入值为 4 的结果//=> 16squareNumber(5);//=> 25squareNumber(5); // 从缓存中读取输入值为 5 的结果//=> 25可移植性 纯函数因为不依赖外部环境,所以非常便于移植,你可以在任何地方使用它而不需要附带着引入其他不需要的属性。 可测试性 如上面提到的Redux reducer和Redux-saga一样, 它对于测试天然亲近。 并行代码 我们可以并行运行任意纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态(race condition)。柯里化概念在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。var add = function(x) { return function(y) { return x + y; };};var increment = add(1);var addTen = add(10);increment(2);// 3addTen(2);// 12例子在Lodash类库中,就有这么一个curry函数来帮助我们处理科里化,关于如何实现一个curry函数,推荐大家参考这篇文章var abc = function(a, b, c) { return [a, b, c];}; var curried = .curry(abc); curried(1)(2)(3);// => [1, 2, 3] curried(1, 2)(3);// => [1, 2, 3] curried(1, 2, 3);// => [1, 2, 3] // Curried with placeholders.curried(1)(, 3)(2);// => [1, 2, 3]偏函数应用偏函数本身与科里化并不相关, 但在日常的编写程序中,或许我们使用更多的是偏函数,所以在这里简单的介绍一下偏函数偏函数应用是找一个函数,固定其中的几个参数值,从而得到一个新的函数。有时候,我们会写一个专门发送http请求的函数const sendRequest = (host, fixPath, path) => { axios.post(${host}\${fixPath}\{path})}但是大多数时候, host和fixPath是固定的, 我们不想每次都写一次host和fixPath,但我们又不能写死,因为我们需要sendRequest这个函数是可以移植的,不受环境的约束,那么我们可以这样const sendRequestPart = (path) => { const host = ‘…’ const fixPath = ‘…’ return sendRequest(host, fixPath, path)}总结科里化和偏函数的主要用途是在组合中,这一小节主要介绍了他们的使用方法和行为。组合 compose组合的功能非常强大, 也是函数式编程的一个核心概念, 所谓的对过程进行封装很大程度上就是依赖于组合。那么什么是组合?var compose = function(f,g) { return function(x) { return f(g(x)); };};var toUpperCase = function(x) { return x.toUpperCase(); };var exclaim = function(x) { return x + ‘!’; };var shout = compose(exclaim, toUpperCase);shout(“send in the clowns”);//=> “SEND IN THE CLOWNS!“上面的compose就是一个最简单的组合函数, 当然组合函数并不限制于传入多少个函数参数,它最后只返回一个函数,我个人更喜欢将它认为像管道一样,将数据经过不同函数的逐渐加工,最后得到我们想要的结果const testFunc = compose(func1, func2, func3, func4) testFunc(…args) 在js中, 实现compose函数比较容易const compose = (…fns) => { return (…args) => { let res = args for (let i = fns.length - 1; i > -1; i–) { res = fnsi } return res }}例子React官方推崇组合优于继承这个概念,这里选择两个比较典型的例子来看 React中的高阶组件 在React中,有许多使用高阶组件的地方,如React-router的withRouter函数,React-redux的connect函数返回的函数,// Navbar 和 Comment都是组件const NavbarWithRouter = withRouter(Navbar);const ConnectedComment = connect(commentSelector, commentActions)(Comment);而由于高阶函数的签名是Component => Component。所以我们可以很容易的将他们组合到一起,这也是官方推荐的做法// 不要这样做……const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))// ……你可以使用一个函数组合工具// compose(f, g, h) 和 (…args) => f(g(h(…args)))是一样的const enhance = compose( // 这些都是单独一个参数的高阶组件 withRouter, connect(commentSelector))const EnhancedComponent = enhance(WrappedComponent)Redux的compose函数 Redux的compose函数实现要比上面提到的简洁的多export default function compose(…funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (…args) => a(b(…args)))}这个实现咋看之下有点懵逼, 所以可以拆开来看一下composeFn = compose(fn1, fn2, fn3, fn4)那么reduce循环运行时, 第一次a就是fn1, b是fn2, 第二次a是(…args) => fn1(fn2(…args)), b是fn3, 第三次运行的时候则是a是(…args) => fn1(fn2(fn3(…args))), b是fn4, 最后返回了fn1(fn2(fn3(fn4(…args))))pointfree它的意思是说,函数无须提及将要操作的数据是什么样的。// 非 pointfree,因为提到了数据:wordvar snakeCase = function (word) { return word.toLowerCase().replace(/\s+/ig, ‘’);};// pointfreevar snakeCase = compose(replace(/\s+/ig, ‘’), toLowerCase);pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用。对函数式代码来说,pointfree 是非常好的石蕊试验,因为它能告诉我们一个函数是否是接受输入返回输出的小函数。比如,while 循环是不能组合的。不过你也要警惕,pointfree 就像是一把双刃剑,有时候也能混淆视听。并非所有的函数式代码都是 pointfree 的,不过这没关系。可以使用它的时候就使用,不能使用的时候就用普通函数。总结有了组合, 配合上面提到的科里化和偏函数应用, 可以将程序拆成一个个小函数然后组合起来, 优点已经很明显的呈现出来,也很直观的表达出了函数式编程的封装过程的核心概念。范畴学函数式编程建立在范畴学上,很多时候讨论起来难免有点理论化,所以这里简单的介绍一下范畴。 有着以下这些组件(component)的搜集(collection)就构成了一个范畴:对象的搜集态射的搜集态射的组合identity 这个独特的态射对象的搜集 对象就是数据类型,例如 String、Boolean、Number 和 Object 等等。通常我们把数据类型视作所有可能的值的一个集合(set)。像 Boolean 就可以看作是 [true, false] 的集合,Number 可以是所有实数的一个集合。把类型当作集合对待是有好处的,因为我们可以利用集合论(set theory)处理类型。 态射的搜集 态射是标准的、普通的纯函数。 态射的组合 即上面提到的compose identity 这个独特的态射 让我们介绍一个名为 id 的实用函数。这个函数接受随便什么输入然后原封不动地返回它:var id = function(x){ return x; };functor在学习函数式编程的时候,第一次看到functor的时候一脸懵逼, 确实不理解这个东西是什么, 可以做什么,加上一堆术语,头都大了。在理解functor之前,先认识一个东西概念容器容器为函数式编程里普通的变量、对象、函数提供了一层极其强大的外衣,赋予了它们一些很惊艳的特性。var Container = function(x) { this.__value = x;}Container.of = x => new Container(x);//试试看Container.of(1);//=> Container(1)Container.of(‘abcd’);//=> Container(‘abcd’)Container.of 把东西装进容器里之后,由于这一层外壳的阻挡,普通的函数就对他们不再起作用了,所以我们需要加一个接口来让外部的函数也能作用到容器里面的值(像Array也是一个容器):Container.prototype.fmap = function(f){ return Container.of(f(this.__value))}我们可以这样使用它:Container.of(3) .fmap(x => x + 1) //=> Container(4) .fmap(x => ‘Result is ’ + x); //=> Container(‘Result is 4’)我们通过简单的代码就实现了一个链式调用,并且这也是一个functor。Functor(函子)是实现了 fmap 并遵守一些特定规则的容器类型。这样子看还是有点不好理解, 那么参考下面这句话可能会好一点:a functor is nothing more than a data structure you can map functions over with the purpose of lifting values from a container, modifying them, and then putting them back into a container. 都是些简单的单词,意会比起本人翻译会更容易理解。加上一张图: ok, 现在大概知道functor是一个什么样的东西了。作用那么functor有什么作用呢? 链式调用 首先它可以链式调用,正如上面提到的一样。 Immutable 可以看到, 我们每次都是返回了一个新的Container.of, 所以数据是Immutable的, 而Immutable的作用就不在这里赘述了。 将控制权交给Container 将控制权交给Container, 这样他就可以决定何时何地怎么去调用我们传给fmap的function,这个作用非常强大,可以为我们做空值判断、异步处理、惰性求值等一系列麻烦的事。例子上面作用的第三点可能直观上有点难以理解, 下面举三个简单的例子 Maybe Container 定义一个Maybe Container来帮我们处理空值的判断var Maybe = function(x) { this.__value = x;}Maybe.of = function(x) { return new Maybe(x);}Maybe.prototype.fmap = function(f) { return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));}Maybe.prototype.isNothing = function() { return (this.__value === null || this.__value === undefined);}//试试看import _ from ’lodash’;var add = .curry(.add);Maybe.of({name: “Stark”}) .fmap(.prop(“age”)) .fmap(add(10));//=> Maybe(null)Maybe.of({name: “Stark”, age: 21}) .fmap(.prop(“age”)) .fmap(add(10));//=> Maybe(31)当然, 这里可以利用上面提到的科里化函数来简化掉一堆fmap的情况import _ from ’lodash’;var compose = _.flowRight;var add = .curry(.add);// 创造一个柯里化的 mapvar map = _.curry((f, functor) => functor.fmap(f));var doEverything = map(compose(add(10), _.property(“age”)));var functor = Maybe.of({name: “Stark”, age: 21});doEverything(functor);//=> Maybe(31)Task Container 我们可以编写一个Task Container来帮我们处理异步的情况var fs = require(‘fs’);// readFile :: String -> Task(Error, JSON)var readFile = function(filename) { return new Task(function(reject, result) { fs.readFile(filename, ‘utf-8’, function(err, data) { err ? reject(err) : result(data); }); });};readFile(“metamorphosis”).fmap(split(’\n’)).fmap(head);例子中的 reject 和 result 函数分别是失败和成功的回调。正如你看到的,我们只是简单地调用 Task 的 map 函数,就能操作将来的值,好像这个值就在那儿似的。(这看起来有点像Promise) Io Container 我们可以利用Io Container来做惰性求值import _ from ’lodash’;var compose = _.flowRight;var IO = function(f) { this._value = f;}IO.of = x => new IO( => x);IO.prototype.map = function(f) { return new IO(compose(f, this._value))};var io_document = new IO( => window.document);io_document.map(function(doc){ return doc.title });//=> IO(document.title)注意我们这里虽然感觉上返回了一个实际的值 IO(document.title),但事实上只是一个对象:{ __value: [Function] },它并没有执行,而是简单地把我们想要的操作存了起来,只有当我们在真的需要这个值得时候,IO 才会真的开始求值,functor 范畴functor 的概念来自于范畴学,并满足一些定律。 即functor 接受一个范畴的对象和态射(morphism),然后把它们映射(map)到另一个范畴里去。Js中的functorJs中也有一些实现了functor, 如map、filtermap :: (A -> B) -> Array(A) -> Array(B)filter :: (A -> Boolean) -> Array(A) -> Array(A)Monad普通functor的问题我们来写一个函数 cat,这个函数的作用和 Linux 命令行下的 cat 一样,读取一个文件,然后打出这个文件的内容import fs from ‘fs’;import _ from ’lodash’;var map = .curry((f, x) => x.map(f));var compose = .flowRight;var readFile = function(filename) { return new IO( => fs.readFileSync(filename, ‘utf-8’));};var print = function(x) { return new IO( => { console.log(x); return x; });}var cat = compose(map(print), readFile);cat(“file”)//=> IO(IO(“file的内容”))ok, 我们最后得到的是两层嵌套的IO, 要获取其中的值cat(“file”).__value().__value()问题很明显的出来了, 我们需要连续调用两次_value才能获取, 那么假如我们嵌套了更多呢, 难道每次都要调用一大堆__value吗, 那当然是不可能的。概念我们可以使用一个join函数, 来将Container里面的东西拿出来, 像这样var join = x => x.join();IO.prototype.join = function() { return this.__value ? IO.of(null) : this._value();}// 试试看var foo = IO.of(IO.of(‘123’));foo.join();似乎这样也有点麻烦, 每次都要使用一个join来剖析var doSomething = compose(join, map(f), join, map(g), join, map(h));我们可以使用一个chain函数, 来帮助我们做这些事var chain = .curry((f, functor) => functor.chain(f));IO.prototype.chain = function(f) { return this.map(f).join();}// 现在可以这样调用了var doSomething = compose(chain(f), chain(g), chain(h));// 当然,也可以这样someMonad.chain(f).chain(g).chain(h)// 写成这样是不是很熟悉呢?readFile(‘file’) .chain(x => new IO( => { console.log(x); return x; })) .chain(x => new IO( => { // 对x做一些事情,然后返回 }))ok, 事实上这就是一个Monad, 而且你也会很熟悉, 这就像一个Promise的then, 那么什么是Monad呢? Monad有一个bind方法, 就是上面讲到的chain(同一个东西不同叫法),function bind<T, U>(instance: M<T>, transform: (value: T) => M<U>): M<U> { // …}其实,Monad 的作用跟 Functor 类似,也是应用一个函数到一个上下文中的值。不同之处在于,Functor 应用的是一个接收一个普通值并且返回一个普通值的函数,而 Monad 应用的是一个接收一个普通值但是返回一个在上下文中的值的函数。上下文即一个Container。Promise是Monad需要被认为是Monad需要具备以下三个条件拥有容器, 即Maybe、IO之类。一个可以将普通类型转换为具有上下文的值的函数, 即Contanier.of拥有bind函数(即上面提到的bind, 而不是ES5的bind)那么Promise具备了什么条件?拥有容器 Promise, 即上面第一点Promise.resolve(value)将值转换为一个具有上下文的值, 即上面第二点。Promise.prototype.then(onFullfill: value => Promise) 拥有一个bind(then)函数, 接受一个函数作为参数, 该函数接受一个普通值并返回一个含有上下文的值。 即上面第三点不过Promise比Monad拥有更多的功能。如果then返回了一个正常的value, Promise会调用Promise.resolve将其转换为Promise普通的Monad只能提供在计算的时候传递一个值, 而Promise有两个不同的值 - 一个用于成功值,一个用于错误(类似于Either monad)。可以使用then方法的第二个回调或使用特殊的.catch方法捕获错误Applicative Functor提到了Functor和Monad而不提Applicative Functor就不完整了。概念Applicative Functor就是让不同 functor 可以相互应用(apply)的能力。 举一个简单的例子, 假设有两个同类型的 functor,我们想把这两者作为一个函数的两个参数传递过去来调用这个函数。// 这样是行不通的,因为 2 和 3 都藏在瓶子里。add(Container.of(2), Container.of(3));//NaN// 使用可靠的 map 函数试试var container_of_add_2 = map(add, Container.of(2));// Container(add(2))这时候我们创建了一个 Container,它内部的值是一个局部调用的(partially applied)的函数。确切点讲就是,我们想让 Container(add(2)) 中的 add(2) 应用到 Container(3) 中的 3 上来完成调用。也就是说,我们想把一个 functor 应用到另一个上。巧的是,完成这种任务的工具已经存在了,即 chain 函数。我们可以先 chain 然后再 map 那个局部调用的 add(2),就像这样:Container.of(2).chain(function(two) { return Container.of(3).map(add(two));});然而这样我们需要延迟Container.of(3)的建立, 这对我们来说是很不方便的也是没有必要的, 我们可以通过建立一个ap函数来达成我们想要的效果Container.prototype.ap = function(other_container) { return other_container.map(this.__value);}Container.of(2).map(add).ap(Container.of(3));// Container(5)注意上面的add是科里化函数, this.__value是一个纯函数。 由于这种先 map 再 ap 的操作很普遍,我们可以抽象出一个工具函数 liftA2:const liftA2 = (f, m1, m2) => m1.map(f).ap(m2)liftA2(add, Container.of(2), Container.of(3))应用正如我们上面所说, 我们可以独立创建两个Container, 那么在Task中也可以同时发起两个http请求,而不必等到第一个返回再执行第二个// Http.get :: String -> Task Error HTMLvar renderPage = curry(function(destinations, events) { / render page */ });Task.of(renderPage).ap(Http.get(’/destinations’)).ap(Http.get(’/events’))// Task("<div>some page with dest and events</div>")FunctorMonadApplicative Functor的数学规律Functor// identitymap(id) === id;// compositioncompose(map(f), map(g)) === map(compose(f, g));Monadbind(unit(x), f) ≡ f(x)bind(m, unit) ≡ mbind(bind(m, f), g) ≡ bind(m, x ⇒ bind(f(x), g))Applicative FunctorIdentity: A.of(x => x).ap(v) === vHomomorphism: A.of(f).ap(A.of(x)) === A.of(f(x))Interchange: u.ap(A.of(y)) === A.of(f => f(y)).ap(u)js 与 函数式和面向对象以下引用自文章漫谈 JS 函数式编程(一)面向对象对数据进行抽象,将行为以对象方法的方式封装到数据实体内部,从而降低系统的耦合度。而函数式编程,选择对过程进行抽象,将数据以输入输出流的方式封装进过程内部,从而也降低系统的耦合度。两者虽是截然不同,然而在系统设计的目标上可以说是殊途同归的。面向对象思想和函数式编程思想也是不矛盾的,因为一个庞大的系统,可能既要对数据进行抽象,又要对过程进行抽象,或者一个局部适合进行数据抽象,另一个局部适合进行过程抽象,这都是可能的。数据抽象不一定以对象实体为形式,同样过程抽象也不是说形式上必然是 functional 的,比如流式对象(InputStream、OutputStream)、Express 的 middleware,就带有明显的过程抽象的特征。但是在通常情况下,OOP更适合用来做数据抽象,FP更适合用来做过程抽象。当然由于Javascript本身是多范式语言, 所以可以在合适的地方使用合适的编程方式。总而言之, 两者互不排斥,是可共存的。尾递归优化由于函数式编程,如果尾递归不做优化,很容易爆栈, 这个知识点有很多文章提出来了, 这里推荐一篇文章声明式编程声明式主要表现在于只关心结果而不关心过程, 这里推荐一篇轻松易懂的文章 或者举个例子: 在JQ时代的时候, 假如我们需要渲染一个DOM, 并改变其文字颜色, 我们需要这样的步骤:找到DOM的class或者id根据class或者id找到DOM重新赋值DOM的style属性的color属性而在React中, 我们可以直接告诉JSX我们想要DOM的颜色变成红色即可。const textColor = ‘red’const comp = () => { return ( <div style={{ color: textColor }} /> )}而关于声明式和函数式, 我个人认为函数式和声明式一样, 也是属于关心结果, 但是函数式最重要的特点是“函数第一位”,即函数可以出现在任何地方。 两者其实不应该做比较。函数式编程在JS中的实践Undescore/Lodash/Ramda库 特别是Lodash, 打开node_modules基本都能看到Immutable-js 数据不可变ReactReduxES6 尾递归优化函数式编程在前端开发中的优势以下引用自知乎答案 优化绑定说白了前端和后端不一样的关键点是后端HTTP较多,前端渲染多,前端真正的刚需是数据绑定机制。后端一次对话,计算好Response发回就完成任务了,所以后端吃了二十年年MVC老本还是挺好用的。前端处理的是连续的时间轴,并非一次对话,像后端那样赋值简单传递就容易断档,导致状态不一致,带来大量额外复杂度和Bug。不管是标准FRP还是Mobx这种命令式API的TFRP,内部都是基于函数式设计的。函数式重新发明的Return和分号是要比裸命令式好得多的(前端状态可以同步,后端线程安全等等,想怎么封装就怎么封装)。封装作用接上条,大幅简化异步,IO,渲染等作用/副作用相关代码。和很多人想象的不一样,函数式很擅长处理作用,只是多一层抽象,如果应用稍微复杂一点,这点成本很快就能找回来(Redux Saga是个例子,特别是你写测试的情况下)。渲染现在大家都可以理解幂等渲染地好处了,其实函数式编程各种作用和状态也是幂等的,对于复杂应用非常有帮助。复用引用透明,无副作用,代数设计让函数式代码可以正确优雅地复用。前端不像后端业务固定,做好业务分析和DDD就可以搭个静态结构,高枕无忧了。前端的好代码一定是活的,每处都可能乱改。可组合性其实很重要。通过高阶函数来组合效果和效率都要高于继承,试着多用ramda,你就可以发现绝大部分东西都能一行写完,最后给个实参就变成一个UI,来需求改两笔就变成另外一个。总结函数式编程在JS的未来是大放异彩还是泯然众人,都不影响我们学习它的思想。本文里面有许多引用没有特别指出,但都会在底部放上链接(如介意请留言), 望见谅。参考&引用声明式编程和命令式编程有什么区别? 用 JS 代码完整解释 Monad 怎么理解“声明式渲染”? JavaScript函数式编程(二) JavaScript Functors Explained 前端开发js函数式编程真实用途体现在哪里? js 是更倾向于函数式编程了还是更倾向于面向对象?或者没有倾向?只是简单的提供了更多的语法糖? 漫谈 JS 函数式编程(一) 有哪些函数式编程在前端的实践经验? 前端使用面向对象式编程 还是 函数式编程 针对什么问题用什么方式 分别有什么具体案例? 什么是 Monad (Functional Programming)? Monads In Javascript Functor、Applicative 和 Monad JavaScript 让 Monad 更简单 函数式编程 ...

March 31, 2019 · 6 min · jiezi

《前端面试手记》之JavaScript基础知识梳理(上)

???? 内容速览 ????普通函数和箭头函数的this原始数据类型及其判断和转化方法深浅拷贝及实现JS事件模型常见的高阶函数????查看全部教程 / 阅读原文????普通函数和箭头函数的this还是一道经典题目,下面的这段代码的输出是什么?(为了方便解释,输出放在了注释中)function fn() { console.log(this); // 1. {a: 100} var arr = [1, 2, 3]; (function() { console.log(this); // 2. Window })(); // 普通 JS arr.map(function(item) { console.log(this); // 3. Window return item + 1; }); // 箭头函数 let brr = arr.map(item => { console.log(“es6”, this); // 4. {a: 100} return item + 1; });}fn.call({ a: 100 });其实诀窍很简单,常见的基本是3种情况:es5普通函数、es6的箭头函数以及通过bind改变过上下文返回的新函数。① es5普通函数:函数被直接调用,上下文一定是window函数作为对象属性被调用,例如:obj.foo(),上下文就是对象本身obj通过new调用,this绑定在返回的实例上② es6箭头函数: 它本身没有this,会沿着作用域向上寻找,直到global / window。请看下面的这段代码:function run() { const inner = () => { return () => { console.log(this.a) } } inner()()}run.bind({a: 1})() // Output: 1③ bind绑定上下文返回的新函数:就是被第一个bind绑定的上下文,而且bind对“箭头函数”无效。请看下面的这段代码:function run() { console.log(this.a)}run.bind({a: 1})() // output: 1// 多次bind,上下文由第一个bind的上下文决定run .bind({a: 2}) .bind({a: 1}) () // output: 2最后,再说说这几种方法的优先级:new > bind > 对象调用 > 直接调用至此,这道题目的输出就说可以解释明白了。原始数据类型和判断方法题目:JS中的原始数据类型?ECMAScript 中定义了 7 种原始类型:BooleanStringNumberNullUndefinedSymbol(新定义)BigInt(新定义)注意:原始类型不包含Object和Function题目:常用的判断方法?在进行判断的时候有typeof、instanceof。对于数组的判断,使用Array.isArray():typeof:typeof基本都可以正确判断数据类型typeof null和typeof [1, 2, 3]均返回"object"ES6新增:typeof Symbol()返回"symbol"instanceof:专门用于实例和构造函数对应function Obj(value){ this.value = value; }let obj = new Obj(“test”);console.log(obj instanceof Obj); // output: true判断是否是数组:[1, 2, 3] instanceof Array Array.isArray():ES6新增,用来判断是否是’Array’。Array.isArray({})返回false。原始类型转化当我们对一个“对象”进行数学运算操作时候,会涉及到对象 => 基础数据类型的转化问题。事实上,当一个对象执行例如加法操作的时候,如果它是原始类型,那么就不需要转换。否则,将遵循以下规则:调用实例的valueOf()方法,如果有返回的是基础类型,停止下面的过程;否则继续调用实例的toString()方法,如果有返回的是基础类型,停止下面的过程;否则继续都没返回原始类型,就会报错请看下面的测试代码:let a = { toString: function() { return ‘a’ }}let b = { valueOf: function() { return 100 }, toString: function() { return ‘b’ }}let c = Object.create(null) // 创建一个空对象console.log(a + ‘123’) // output: a123console.log(b + 1) // output: 101console.log(c + ‘123’) // 报错除了valueOf和toString,es6还提供了Symbol.toPrimitive供对象向原始类型转化,并且它的优先级最高!!稍微改造下上面的代码:let b = { valueOf: function() { return 100 }, toString: function() { return ‘b’ }, [Symbol.toPrimitive]: function() { return 10000 }}console.log(b + 1) // output: 10001最后,其实关于instanceof判断是否是某个对象的实例,es6也提供了Symbol.hasInstance接口,代码如下:class Even { static Symbol.hasInstance { return Number(num) % 2 === 0; }}const Odd = { Symbol.hasInstance { return Number(num) % 2 !== 0; }};console.log(1 instanceof Even); // output: falseconsole.log(1 instanceof Odd); // output: true深拷贝和浅拷贝题目:实现对象的深拷贝。在JS中,函数和对象都是浅拷贝(地址引用);其他的,例如布尔值、数字等基础数据类型都是深拷贝(值引用)。值得提醒的是,ES6的Object.assign()和ES7的…解构运算符都是“浅拷贝”。实现深拷贝还是需要自己手动撸“轮子”或者借助第三方库(例如lodash):手动做一个“完美”的深拷贝函数:https://godbmw.com/passages/2019-03-18-interview-js-code/借助第三方库:jq的extend(true, result, src1, src2[ ,src3])、lodash的cloneDeep(src)JSON.parse(JSON.stringify(src)):这种方法有局限性,如果属性值是函数或者一个类的实例的时候,无法正确拷贝借助HTML5的MessageChannel:这种方法有局限性,当属性值是函数的时候,会报错<script> function deepClone(obj) { return new Promise(resolve => { const {port1, port2} = new MessageChannel(); port2.onmessage = ev => resolve(ev.data); port1.postMessage(obj); }); } const obj = { a: 1, b: { c: [1, 2], d: ‘() => {}’ } }; deepClone(obj) .then(obj2 => { obj2.b.c[0] = 100; console.log(obj.b.c); // output: [1, 2] console.log(obj2.b.c); // output: [100, 2] })</script>JS事件流事件冒泡和事件捕获事件流分为:冒泡和捕获,顺序是先捕获再冒泡。事件冒泡:子元素的触发事件会一直向父节点传递,一直到根结点停止。此过程中,可以在每个节点捕捉到相关事件。可以通过stopPropagation方法终止冒泡。事件捕获:和“事件冒泡”相反,从根节点开始执行,一直向子节点传递,直到目标节点。addEventListener给出了第三个参数同时支持冒泡与捕获:默认是false,事件冒泡;设置为true时,是事件捕获。<div id=“app” style=“width: 100vw; background: red;"> <span id=“btn”>点我</span></div><script> // 事件捕获:先输出 “外层click事件触发”; 再输出 “内层click事件触发” var useCapture = true; var btn = document.getElementById(“btn”); btn.addEventListener( “click”, function() { console.log(“内层click事件触发”); }, useCapture ); var app = document.getElementById(“app”); app.onclick = function() { console.log(“外层click事件触发”); };</script>DOM0级 和 DOM2级DOM2级:前面说的addEventListener,它定义了DOM事件流,捕获 + 冒泡。DOM0级:直接在html标签内绑定on事件在JS中绑定on系列事件注意:现在通用DOM2级事件,优点如下:可以绑定 / 卸载事件支持事件流冒泡 + 捕获:相当于每个节点同一个事件,至少2次处理机会同一类事件,可以绑定多个函数常见的高阶函数没什么好说的,跑一下下面的代码就可以理解了:// map: 生成一个新数组,遍历原数组,// 将每个元素拿出来做一些变换然后放入到新的数组中let newArr = [1, 2, 3].map(item => item * 2);console.log(New array is ${newArr});// filter: 数组过滤, 根据返回的boolean// 决定是否添加到数组中let newArr2 = [1, 2, 4, 6].filter(item => item !== 6);console.log(New array2 is ${newArr2});// reduce: 结果汇总为单个返回值// acc: 累计值; current: 当前itemlet arr = [1, 2, 3];const sum = arr.reduce((acc, current) => acc + current);const sum2 = arr.reduce((acc, current) => acc + current, 100);console.log(sum); // 6console.log(sum2); // 106更多系列文章⭐在GitHub上收藏/订阅⭐《前端知识体系》JavaScript基础知识梳理(上)JavaScript基础知识梳理(下)谈谈promise/async/await的执行顺序与V8引擎的BUG前端面试中常考的源码实现Flex上手与实战……《设计模式手册》单例模式策略模式代理模式迭代器模式订阅-发布模式桥接模式备忘录模式模板模式……《Webpack4渐进式教程》webpack4 系列教程(二): 编译 ES6webpack4 系列教程(三): 多页面解决方案–提取公共代码webpack4 系列教程(四): 单页面解决方案–代码分割和懒加载webpack4 系列教程(五): 处理 CSSwebpack4 系列教程(八): JS Tree Shakingwebpack4 系列教程(十二):处理第三方 JavaScript 库webpack4 系列教程(十五):开发模式与 webpack-dev-server……⭐在GitHub上收藏/订阅⭐ ...

March 31, 2019 · 3 min · jiezi

JS基础——高阶函数

定义高阶函数是至少满足下面一个条件的函数:1、接收一个或多个函数作为参数。比如filter函数2、返回一个函数。 比如bind函数举个例子:比如我们要筛数组[1,2,3,4,5]中大于3的所有元素,我们通常的实现方法为:let newArr = [];for(let i = 0,len = arr.length; i < len; i++){ arr[i] > 3 && newArr.push(arr[i])}而使用数组filter方法的话,只需要 let newArr = arr.filter((item) => {return item > 3})。当然我们也可以通过高阶函数来自己实现:Array.prototype.myFilter = function (fn){ let newArr = []; for(let i = 0,len = this.length; i < len; i++){ fn(this[i]) && newArr.push(this[i]) } return newArr;}[1,2,3,4,5].myFilter((item) => { return item > 3})我们可以通过封装高阶函数来复用和简化我们的代码。柯里化柯里化是将一个多参数的函数转换成多个单参数的函数,这个函数会返回一个函数去处理下一个参数。也就是把fn(a,b,c)转换为newFn(a)(b)(c)这种形象。柯里化常见的应用有:参数复用、延迟计算。比如我们有个拼接接口地址的函数:function getUrl(service,context,api){ return service + context + api;}let loginUrl = getUrl(‘http://localhost:8080/’,‘auth’,’/login’) let logoutUrl = getUrl(‘http://localhost:8080/’,‘auth’,’/logout’)每次前两个参数的值都是一样,我们可以柯里化来封装下来达到参数复用:function curry(fn){ let args = Array.prototype.slice.call(arguments,1); return function(){ let innerArgs = Array.prototype.slice.call(arguments); let finalArgs = args.concat(innerArgs); if(finalArgs.length < fn.length){ //fn.length为函数的参数个数 return curry.call(this,fn,…finalArgs) }else{ return fn.apply(null,finalArgs) } }}var getAuthUrl = curry(getUrl,‘http://localhost:8080/’,‘auth’);let loginUrl = getAuthUrl(’/login’)let logoutUrl = getAuthUrl(’/logout’)组合函数组合函数类似于管道,多个函数的执行时,上一个函数的返回值会自动传入到第二个参数继续执行。比如我们替换一个url中的参数:function replaceToken(str){ return str.replace(/{token}/g,‘123455’)}function replaceAccount(str){ return str.replace(/{account}/g,‘xuriliang’)}replaceAccount(replaceToken(‘http://localhost/api/login?token={token}&account={account}’))我们可以利用这种嵌套的写法来实现,但如果嵌套过多,代码可读性就不是很好了。当然我们也可以在一个函数里分过程实现,不过这样函数就不符合单一原则了。利用函数组合我们可以这样写:function compose() { var args = arguments; var start = args.length - 1; return function() { var i = start; var result = args[start].apply(this, arguments); while (i–) result = args[i].call(this, result); return result; }}compose(replaceToken,replaceAccount)(‘http://localhost/api/login?token={token}&account={account}’)组合函数使得我们可以使用一些通用的函数,组合出各种复杂运算。这也是函数编程中pointfree的概念。 ...

March 16, 2019 · 1 min · jiezi

js无侵入埋点方案

今天给大家介绍一个js无侵入埋点方案:min版: https://github.com/aoping/tra…原版:https://github.com/Qquanwei/t…min版是我在原版的基础上进行优化和精简开发的,打包后大小只有8k(原版190k)min版提供两个api: before after这里只介绍min版使用:安装npm i trackpoint-tools -S 或npm i trackpoint-tools-min@latest -S使用代码: https://codesandbox.io/s/oxxw…喜欢的可以star

March 15, 2019 · 1 min · jiezi

函数式编程(一)

什么是函数式编程函数式编程是一种编程范式,常见的编程范式有以下三种:命令式编程声明式编程函数式编程函数式编程的本质是将计算描述为一种表达式求值。在函数式编程中,函数作为一等公民,可以在任何地方定义(在函数内或函数外),可以作为函数的参数和返回值,可以对函数进行组合。函数式编程的准则:不依赖于外部的数据,而且也不改变外部数据的值,而是返回一个新的值给你。看个简单的例子: // 非函数式的例子 let count = 0; function increment() { count++; // 依赖于函数外部的值,并改变了它的值 } // 函数式的例子 function increment(count) { return count++; }为什么采用函数式编程函数式编程不依赖外部的状态也不修改外部的状态,函数调用的结果不依赖调用的时间和位置,这些写代码容易进行推理,不易犯错,而且单测和调试都更简单。即函数编程采用纯函数。纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。副作用可能包含,但不限于:更改文件系统往数据库插入记录发送一个 http 请求可变数据打印/log获取用户输入DOM 查询访问系统状态副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。纯函数的好处:纯函数能根据输入来做缓存(memoize技术) const memoize = function(f) { const cache = {}; return function() { const argStr = JSON.stringify(arguments); if (!cache[argStr]) { cache[argStr] = f.apply(f, arguments); } return cache[argStr]; } }可移植性/自文档化纯函数的输出只依赖与它的输入,依赖很明确,易于理解。由于纯函数不依赖它的上下文环境,因此我们可以轻易的把它移植到任何地方运行它。可测试性我们不必在每次测试前都去配置和构造初始环境,只需简单给函数一个输入,然后断言它的输出就好了。合理性由于纯函数总是能够根据相同的输入返回相同的输出,所以它们就能够保证总是返回同一个结果,这也就保证了引用透明性。并行执行我们可以并行运行任意纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态。并行代码在服务端 js 环境以及使用了 web worker 的浏览器那里是非常容易实现的,因为它们使用了线程(thread)。不过出于对非纯函数复杂度的考虑,当前主流观点还是避免使用这种并行。实现函数式编程的技术这里我们先不展开这些技术的细节内容,本文我们先侧重于对函数式编程有一个整体上的认识,具体的技术细节我们将在下一章展开。curry(柯里化)compose(代码组合)Monad(Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算所需的函数,整个运算就会自动进行下去。)如何正确看待函数式编程我们先来看以下几种观点:你这段代码用了 for 循环,这是过程式的。为了优雅,你应该写成函数式的。你这段代码有副作用,这是肮脏的。为了纯净性,你应该把 IO 包在 Monad 里。你这段代码用了 class,这是面向对象的。为了无状态,你应该写成高阶函数。我想说的是这种偏激的观点是不正确的,我们不应该把函数式编程和命令式编程对立起来,我们更多的时候需要考虑的是技术的适用场景。函数式编程写起代码来,有一定的难度,如果一个团队的整体水平达不到,那么写代码的质量和效率还不如采用命令式编程好。函数式编程利用纯函数的无状态性,它的好处非常多(结果可预期、利于测试、利于复用、利于并发),但一个系统工程的代码,是不可能全部采用纯函数来写的。当我们越贴近业务,我们就离纯函数与无状态越远。函数式编程非常重要,学习它我们能打开我们的思维方式,适用它也有很多好处,但它也有一些局限,我们应该客观看待。保持开放的心态,根据实际场景选择合适的技术,是一个工程师基本的素养。

March 15, 2019 · 1 min · jiezi

函数式编程 - 容器(container)

最近一直在学习函数式编程,前面介绍了函数式编程中非常重要的两个运算函数柯里化 和 函数组合,下文出现的curry 和 compose函数可以从前两篇文章中找到。它们都可以直接在实际开发中用到,写出函数式的程序。本文主要初探容器的相关概念,以及如何处理编程中异步操作,错误处理等依赖外部环境状态变化的情况,,容器(container)容器可以想象成一个瓶子,也就是一个对象,里面可以放各种不同类型的值。想想,瓶子还有一个特点,跟外界隔开,只有从瓶口才能拿到里面的东西;类比看看, container 回暴露出接口供外界操作内部的值。一个典型的容器示例: var Container = function(x) { this.__value = x; } Container.of = function(x) { return new Container(x); } Container.of(“test”) // 在chrome下会打印出 // Container {__value: “test”} 我们已经实现了一个容器,并且实现了一个把值放到容器里面的 Container.of方法,简单看,它像是一个利用工厂模式创建特定对象的方法。of方法正是返回一个container。函子(functor)上面容器上定义了of方法,functor的定义也类似Functor 是实现了map函数并遵守一些特定规则的容器类型。把值留在容器中,只能暴露出map接口处理它。函子是非常重要的数据类型,后面会讲到各种不同功能的函子,对应处理各种依赖外部变量状态的问题。Container.prototype.map = function(f) { return Container.of(f(this.__value))}把即将处理容器内变量的函数,包裹在map方法里面,返回的执行结果也会是一个Container。这样有几点好处:保证容器内的value一直不会暴露出去,对value的操作方法最终会交给容器执行,可以决定何时执行。方便链式调用 // 利用上一篇中讲到的柯里化,就可以看出其特性。 var add2 = function(x, y) { return x + y; }; curriedAdd = curry(add2); Container.of(2).map(curriedAdd(3)); // Container {__value: 5}不同类型的函子maybe容器在处理内部值时,经常遇到传入参数异常的情况的情况,检查value 值的合理性就非常重要。Maybe 函子保证在调用传入的函数之前,检查值是否为空。var Maybe = function(x) { this.__value = x;}Maybe.of = function(x) { return new Maybe(x);}Maybe.prototype.isNothing = function() { return (this.__value === null || this.__value === undefined);}Maybe.prototype.map = function(f) { return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));} 这个通用的例子,体现了输出结果的不确定性,也可以看出,在容器内部所有对value值的操作,都会交给容器来执行。在value 为空的情况下,也会返回包裹着null的容器,避免后续容器链式调用报错。异常捕获函子通常 使用throw/catch就可以捕获异常,抛出错误,但它并不是一种纯函数方法,最好的方法是在出现异常时,可以正常返回信息。Either函子,内部两个子类Left 和 right; 可以看成右值是正常情况下使用并返回的值,左值是操作简单的默认值。var Left = function(x) { this.__value = x;}Left.of = function(x) { return new Left(x);}Left.prototype.map = function(f) { return this;}var Right = function(x) { this.__value = x;}Right.of = function(x) { return new Right(x);}Right.prototype.map = function(f) { return Right.of(f(this.__value));}// 输入数据进行校验var setage = function(age) { return typeof age === ’number’? Right.of(age): Left.of(’error age’)}setage(12).map(function(age){return ‘my age is’ + age})// Right {__value: “my age is12”}setage(“age”).map(function(age){return ‘my age is’ + age})// Left {__value: “error age”}left 和 right 唯一的区别在于map 方法的实现,当然,一个函子最大的特点也体现在map方法上,Left.map 不管传入的是什么函数,直接返回当前容器;Right.map则是示例里面的方法一样。IO 操作IO 操作本身就是不纯的操作,生来就得跟外界环境变量打交道,不过可以掩盖他的不确定性。跟下面localStorage包裹函数类似,延迟执行IO 操作。 var getStorage = function(key) { return function() { return localStorage[key]; }}再看看,封装了高级一点的IO 函子: var IO = function(f) { this.__value = f; } IO.of = function(x) { return new IO(function(){ return x; }) } IO.prototype.map = function(f) { // 使用上一句定义的compose函数 return new IO(compose(f, this.__value)) }compose函数组合,里面存放的都是函数,this.__value跟其他函子内部值不同,它是函数。IO.of方法在new对象之前,把值包裹在函数里面,试图延迟执行。 // 测试一下var io__dom= new IO(function() {return window.document})io__dom.map(function(doc) { return doc.title})// IO {__value: ƒ}返回一个没有执行的函数对象,里面的__value值对应的函数,在上面函数调用后并没有执行,只有在调用了this.__value值后,才执行。最后一步不纯的操作,交给了函数调用者去做。Monad一个functor, 只要他定义了一个join 方法和一个of 方法,那么它就是一个monad。 它可以将多层相同类型的嵌套扁平化,像剥洋葱一样。关键在于它比一般functor 多了一个join 方法。 我们先看看剥开一层的join方法。 var IO = function(f) { this.__value = f } IO.of = function(x) { return new IO(function(){ return x; }) } IO.prototype.join = function() { return this.__value ? this.__value(): IO.of(null); } // 包裹上两层 var foo = IO.of(IO.of(’test bar’)); foo.join().__value(); // 返回里面嵌套着的IO类。 IO {__value: ƒ},接着只需调用这里的__value(),就可以返回字符串test bar;回头看看前面map方法,return new IO(),生成新的容器,方便链式调用,跟 join方法结合一起使用,生成容器后,再扁平化。形成 chain 函数, var chain = curry(function(f, m) { return m.map(f).join(); })看一个完整示例,其中curry 和compose,分别用到了链接里面的实现,:var IO = function(f) { this.__value = f;}IO.of = function(x) { return new IO(function() { return x; })}IO.prototype.map = function(f) { // 使用上一句定义的compose函数 return new IO(compose(f, this.__value))}IO.prototype.join = function() { return this.__value ? this.__value() : IO.of(null);}var chain = curry(function(f, m) { return m.map(f).join();})var log = function(x) { return new IO(function() { console.log(x); return x; })}var setStyle = curry(function(sel, props) { return new IO(function() { return document.querySelector(sel).style.background = props })})var getItem = function(key) { return new IO(function() { return localStorage.getItem(key); })};var map = curry(function(f, functor) { return functor.map(f);});// 简单实现joinvar join = function(functor) { return functor.join();}localStorage.background = ‘#000’;var setItemStyle = compose(join, map(setStyle(‘body’)), join, map(log), getItem);// 换成 链式调用。setItemStyle = compose(chain(setStyle(‘body’)), chain(log), getItem);setItemStyle(‘background’).__value(); // 操作dom 改变背景颜色总结本文主要利用简单代码举例,介绍了容器,函子等相关概念,初步认识了各种不同的函子。深入实践示例,可以参考阅读下面链接:函数式编程风格 js函数式编程指南https://llh911001.gitbooks.io…JavaScript函数式编程(二)JavaScript:函数式编程基本概念学习JS函数式编程 - 函子和范畴论javascript函数式编程之 函子(functor)函数式编程入门教程 ...

March 7, 2019 · 2 min · jiezi

JavaScript函数式编程,真香之组合(一)

JavaScript函数式编程,真香之认识函数式编程(一)该系列文章不是针对前端新手,需要有一定的编程经验,而且了解 JavaScript 里面作用域,闭包等概念组合函数组合是一种为软件的行为,进行清晰建模的一种简单、优雅而富于表现力的方式。通过组合小的、确定性的函数,来创建更大的软件组件和功能的过程,会生成更容易组织、理解、调试、扩展、测试和维护的软件。对于组合,我觉得是函数式编程里面最精髓的地方之一,所以我迫不及待的把这个概念拿出来先介绍,因为在整个学习函数式编程里,所遇到的基本上都是以组合的方式来编写代码,这也是改变你从一个面向对象,或者结构化编程思想的一个关键点。我这里也不去证明组合比继承好,也不说组合的方式写代码有多好,我希望你看了这篇文章能知道以组合的方式去抽象代码,这会扩展你的视野,在你想重构你的代码,或者想写出更易于维护的代码的时候,提供一种思路。组合的概念是非常直观的,并不是函数式编程独有的,在我们生活中或者前端开发中处处可见。比如我们现在流行的 SPA (单页面应用),都会有组件的概念,为什么要有组件的概念呢,因为它的目的就是想让你把一些通用的功能或者元素组合抽象成可重用的组件,就算不通用,你在构建一个复杂页面的时候也可以拆分成一个个具有简单功能的组件,然后再组合成你满足各种需求的页面。其实我们函数式编程里面的组合也是类似,函数组合就是一种将已被分解的简单任务组织成复杂的整体过程。现在我们有这样一个需求:给你一个字符串,将这个字符串转化成大写,然后逆序。你可能会这么写。// 例 1.1var str = ‘function program’// 一行代码搞定function oneLine(str) { var res = str.toUpperCase().split(’’).reverse().join(’’) return res;}// 或者 按要求一步一步来,先转成大写,然后逆序function multiLine(str) { var upperStr = str.toUpperCase() var res = upperStr.split(’’).reverse().join(’’) return res;}console.log(oneLine(str)) // MARGORP NOITCNUFconsole.log(multiLine(str)) // MARGORP NOITCNUF可能看到这里你并没有觉得有什么不对的,但是现在产品又突发奇想,改了下需求,把字符串大写之后,把每个字符拆开之后组装成一个数组,比如 ’aaa‘ 最终会变成 [A, A, A]。那么这个时候我们就需要更改我们之前我们封装的函数。这就修改了以前封装的代码,其实在设计模式里面就是破坏了开闭原则。那么我们如果把最开始的需求代码写成这个样子,以函数式编程的方式来写。// 例 1.2var str = ‘function program’function stringToUpper(str) { return str.toUpperCase()}function stringReverse(str) { return str.split(’’).reverse().join(’’)}var toUpperAndReverse = 组合(stringReverse, stringToUpper)var res = toUpperAndReverse(str)那么当我们需求变化的时候,我们根本不需要修改之前封装过的东西。// 例 2var str = ‘function program’function stringToUpper(str) { return str.toUpperCase()}function stringReverse(str) { return str.split(’’).reverse().join(’’)}// var toUpperAndReverse = 组合(stringReverse, stringToUpper)// var res = toUpperAndReverse(str)function stringToArray(str) { return str.split(’’)}var toUpperAndArray = 组合(stringReverse, stringToUpper)toUpperAndArray(str)可以看到当变更需求的时候,我们没有打破以前封装的代码,只是新增了函数功能,然后把函数进行重新组合。这里可能会有人说,需求修改,肯定要更改代码呀,你这不是也删除了以前的代码么,也不是算破坏了开闭原则么。我这里声明一下,开闭原则是指一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。是针对我们封装,抽象出来的代码,而是调用逻辑。所以这样写并不算破坏开闭原则。突然产品又灵光一闪,又想改一下需求,把字符串大写之后,再翻转,再转成数组。要是你按照以前的思考,没有进行抽象,你肯定心理一万只草泥马在奔腾,但是如果你抽象了,你完全可以不慌。// 例 3var str = ‘function program’function stringToUpper(str) { return str.toUpperCase()}function stringReverse(str) { return str.split(’’).reverse().join(’’)}function stringToArray(str) { return str.split(’’)}var strUpperAndReverseAndArray = 组合(stringToArray, stringReverse, stringToUpper)strUpperAndReverseAndArray(str)发现并没有更换你之前封装的代码,只是更换了函数的组合方式。可以看到,组合的方式是真的就是抽象单一功能的函数,然后再组成复杂功能。这种方式既锻炼了你的抽象能力,也给维护带来巨大的方便。但是上面的组合我只是用汉字来代替的,我们应该如何去实现这个组合呢。首先我们可以知道,这是一个函数,同时参数也是函数,返回值也是函数。我们看到例 2, 怎么将两个函数进行组合呢,根据上面说的,参数和返回值都是函数,那么我们可以确定函数的基本结构如下(顺便把组合换成英文的 compose)。function twoFuntionCompose(fn1, fn2) { return function() { // code }}我们再思考一下,如果我们不用 compose 这个函数,在例 2 中怎么将两个函数合成呢,我们是不是也可以这么做来达到组合的目的。var res = stringReverse(stringToUpper(str))那么按照这个逻辑是不是我们就可以写出 twoFuntonCompose 的实现了,就是function twoFuntonCompose(fn1, fn2) { return function(arg) { return fn1(fn2(arg)) }}同理我们也可以写出三个函数的组合函数,四个函数的组合函数,无非就是一直嵌套多层嘛,变成:function multiFuntionCompose(fn1, fn2, .., fnn) { return function(arg) { return fnn(…(fn1(fn2(arg)))) }}这种恶心的方式很显然不是我们程序员应该做的,然后我们也可以看到一些规律,无非就是把前一个函数的返回值作为后一个返回值的参数,当直接到最后一个函数的时候,就返回。所以按照正常的思维就会这么写。function aCompose(…args) { let length = args.length let count = length - 1 let result return function f1 (…arg1) { result = args[count].apply(this, arg1) if (count <= 0) { count = length - 1 return result } count– return f1.call(null, result) }}这样写没问题,underscore 也是这么写的,不过里面还有很多健壮性的处理,核心大概就是这样。但是作为一个函数式爱好者,尽量还是以函数式的方式去思考,所以就用 reduceRight 写出如下代码。function compose(…args) { return (result) => { return args.reduceRight((result, fn) => { return fn(result) }, result) }}当然对于 compose 的实现还有很多种方式,在这篇实现 compose 的五种思路中还给出了另外脑洞大开的实现方式,在我看这篇文章之前,另外三种我是没想到的,不过感觉也不是太有用,但是可以扩展我们的思路,有兴趣的同学可以看一看。注意:要传给 compose 函数是有规范的,首先函数的执行是从最后一个参数开始执行,一直执行到第一个,而且对于传给 compose 作为参数的函数也是有要求的,必须只有一个形参,而且函数的返回值是下一个函数的实参。对于 compose 从最后一个函数开始求值的方式如果你不是很适应的话,你可以通过 pipe 函数来从左到右的方式。function pipe(…args) { return (result) => { return args.reduce((result, fn) => { return fn(result) }, result) }}实现跟 compose 差不多,只是把参数的遍历方式从右到左(reduceRight)改为从左到右(reduce)。之前是不是看过很多文章写过如何实现 compose,或者柯里化,部分应用等函数,但是你可能不知道是用来干啥的,也没用过,所以记了又忘,忘了又记,看了这篇文章之后我希望这些你都可以轻松实现。后面会继续讲到柯里化和部分应用的实现。point-free在函数式编程的世界中,有这样一种很流行的编程风格。这种风格被称为 tacit programming,也被称作为 point-free,point 表示的就是形参,意思大概就是没有形参的编程风格。// 这就是有参的,因为 word 这个形参var snakeCase = word => word.toLowerCase().replace(/\s+/ig, ‘’);// 这是 pointfree,没有任何形参var snakeCase = compose(replace(/\s+/ig, ‘’), toLowerCase);有参的函数的目的是得到一个数据,而 pointfree 的函数的目的是得到另一个函数。那这 pointfree 有什么用? 它可以让我们把注意力集中在函数上,参数命名的麻烦肯定是省了,代码也更简洁优雅。 需要注意的是,一个 pointfree 的函数可能是由众多非 pointfree 的函数组成的,也就是说底层的基础函数大都是有参的,pointfree 体现在用基础函数组合而成的高级函数上,这些高级函数往往可以作为我们的业务函数,通过组合不同的基础函数构成我们的复制的业务逻辑。可以说 pointfree 使我们的编程看起来更美,更具有声明式,这种风格算是函数式编程里面的一种追求,一种标准,我们可以尽量的写成 pointfree,但是不要过度的使用,任何模式的过度使用都是不对的。另外可以看到通过 compose 组合而成的基础函数都是只有一个参数的,但是往往我们的基础函数参数很可能不止一个,这个时候就会用到一个神奇的函数(柯里化函数)。柯里化在维基百科里面是这么定义柯里化的:在计算机科学,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。在定义中获取两个比较重要的信息:接受一个单一参数返回结果是函数这两个要点不是 compose 函数参数的要求么,而且可以将多个参数的函数转换成接受单一参数的函数,岂不是可以解决我们再上面提到的基础函数如果是多个参数不能用的问题,所以这就很清楚了柯里化函数的作用了。柯里化函数可以使我们更好的去追求 pointfree,让我们代码写得更优美!接下来我们具体看一个例子来理解柯里化吧:比如你有一间士多店并且你想给你优惠的顾客给个 10% 的折扣(即打九折):function discount(price, discount) { return price * discount}当一位优惠的顾客买了一间价值$500的物品,你给他打折:const price = discount(500, 0.10); // $50 你可以预见,从长远来看,我们会发现自己每天都在计算 10% 的折扣:const price = discount(1500,0.10); // $150const price = discount(2000,0.10); // $200// … 等等很多我们可以将 discount 函数柯里化,这样我们就不用总是每次增加这 0.01 的折扣。// 这个就是一个柯里化函数,将本来两个参数的 discount ,转化为每次接收单个参数完成求职function discountCurry(discount) { return (price) => { return price * discount; }}const tenPercentDiscount = discountCurry(0.1);现在,我们可以只计算你的顾客买的物品都价格了:tenPercentDiscount(500); // $50同样地,有些优惠顾客比一些优惠顾客更重要-让我们称之为超级客户。并且我们想给这些超级客户提供20%的折扣。可以使用我们的柯里化的discount函数:const twentyPercentDiscount = discountCurry(0.2);我们通过这个柯里化的 discount 函数折扣调为 0.2(即20%),给我们的超级客户配置了一个新的函数。返回的函数 twentyPercentDiscount 将用于计算我们的超级客户的折扣:twentyPercentDiscount(500); // 100我相信通过上面的 discountCurry 你已经对柯里化有点感觉了,这篇文章是谈的柯里化在函数式编程里面的应用,所以我们再来看看在函数式里面怎么应用。现在我们有这么一个需求:给定的一个字符串,先翻转,然后转大写,找是否有TAOWENG,如果有那么就输出 yes,否则就输出 no。function stringToUpper(str) { return str.toUpperCase()}function stringReverse(str) { return str.split(’’).reverse().join(’’)}function find(str, targetStr) { return str.includes(targetStr)}function judge(is) { console.log(is ? ‘yes’ : ’no’)}我们很容易就写出了这四个函数,前面两个是上面就已经写过的,然后 find 函数也很简单,现在我们想通过 compose 的方式来实现 pointfree,但是我们的 find 函数要接受两个参数,不符合 compose 参数的规定,这个时候我们像前面一个例子一样,把 find 函数柯里化一下,然后再进行组合:// 柯里化 find 函数function findCurry(targetStr) { return str => str.includes(targetStr)}const findTaoweng = findCurry(‘TAOWENG’)const result = compose(judge, findTaoweng, stringReverse, stringToUpper)看到这里是不是可以看到柯里化在达到 pointfree 是非常的有用,较少参数,一步一步的实现我们的组合。但是通过上面那种方式柯里化需要去修改以前封装好的函数,这也是破坏了开闭原则,而且对于一些基础函数去把源码修改了,其他地方用了可能就会有问题,所以我们应该写一个函数来手动柯里化。根据定义之前对柯里化的定义,以及前面两个柯里化函数,我们可以写一个二元(参数个数为 2)的通用柯里化函数:function twoCurry(fn) { return function(firstArg) { // 第一次调用获得第一个参数 return function(secondArg) { // 第二次调用获得第二个参数 return fn(firstArg, secondArg) // 将两个参数应用到函数 fn 上 } }}所以上面的 findCurry 就可以通过 twoCurry 来得到:const findCurry = twoCurry(find)这样我们就可以不更改封装好的函数,也可以使用柯里化,然后进行函数组合。不过我们这里只实现了二元函数的柯里化,要是三元,四元是不是我们又要要写三元柯里化函数,四元柯里化函数呢,其实我们可以写一个通用的 n 元柯里化。function currying(fn, …args) { if (args.length >= fn.length) { return fn(…args) } return function (…args2) { return currying(fn, …args, …args2) }}我这里采用的是递归的思路,当获取的参数个数大于或者等于 fn 的参数个数的时候,就证明参数已经获取完毕,所以直接执行 fn 了,如果没有获取完,就继续递归获取参数。可以看到其实一个通用的柯里化函数核心思想是非常的简单,代码也非常简洁,而且还支持在一次调用的时候可以传多个参数(但是这种传递多个参数跟柯里化的定义不是很合,所以可以作为一种柯里化的变种)。我这里重点不是讲柯里化的实现,所以没有写得很健壮,更强大的柯里化函数可见羽讶的:JavaScript专题之函数柯里化。部分应用部分应用是一种通过将函数的不可变参数子集,初始化为固定值来创建更小元数函数的操作。简单来说,如果存在一个具有五个参数的函数,给出三个参数后,就会得到一个、两个参数的函数。看到上面的定义可能你会觉得这跟柯里化很相似,都是用来缩短函数参数的长度,所以如果理解了柯里化,理解部分应用是非常的简单:function debug(type, firstArg, secondArg) { if(type === ’log’) { console.log(firstArg, secondArg) } else if(type === ‘info’) { console.info(firstArg, secondArg) } else if(type === ‘warn’) { console.warn(firstArg, secondArg) } else { console.error(firstArg, secondArg) }}const logDebug = 部分应用(debug, ’log’)const infoDebug = 部分应用(debug, ‘info’)const warnDebug = 部分应用(debug, ‘warn’)const errDebug = 部分应用(debug, ’error’)logDebug(’log:’, ‘测试部分应用’)infoDebug(‘info:’, ‘测试部分应用’)warnDebug(‘warn:’, ‘测试部分应用’)errDebug(’error:’, ‘测试部分应用’)debug方法封装了我们平时用 console 对象调试的时候各种方法,本来是要传三个参数,我们通过部分应用的封装之后,我们只需要根据需要调用不同的方法,传必须的参数就可以了。我这个例子可能你会觉得没必要这么封装,根本没有减少什么工作量,但是如果我们在 debug 的时候不仅是要打印到控制台,还要把调试信息保存到数据库,或者做点其他的,那是不是这个封装就有用了。因为部分应用也可以减少参数,所以他在我们进行编写组合函数的时候也占有一席之地,而且可以更快传递需要的参数,留下为了 compose 传递的参数,这里是跟柯里化比较,因为柯里化按照定义的话,一次函数调用只能传一个参数,如果有四五个参数就需要:function add(a, b, c, d) { return a + b + c +d}// 使用柯里化方式来使 add 转化为一个一元函数let addPreThreeCurry = currying(add)(1)(2)(3)addPreThree(4) // 10这种连续调用(这里所说的柯里化是按照定义的柯里化,而不是我们写的柯里化变种),但是用部分应用就可以:// 使用部分应用的方式使 add 转化为一个一元函数const addPreThreePartial = 部分应用(add, 1, 2, 3)addPreThree(4) // 10既然我们现在已经明白了部分应用这个函数的作用了,那么还是来实现一个吧,真的是非常的简单:// 通用的部分应用函数的核心实现function partial(fn, …args) { return (…_arg) => { return fn(…args, …_arg); }}另外不知道你有没有发现,这个部分应用跟 JavaScript 里面的 bind 函数很相似,都是把第一次穿进去的参数通过闭包存在函数里,等到再次调用的时候再把另外的参数传给函数,只是部分应用不用指定 this,所以也可以用 bind 来实现一个部分应用函数。// 通用的部分应用函数的核心实现function partial(fn, …args) { return fn.bind(null, …args)}另外可以看到实际上柯里化和部分应用确实很相似,所以这两种技术很容易被混淆。它们主要的区别在于参数传递的内部机制与控制:柯里化在每次分布调用时都会生成嵌套的一元函数。在底层 ,函数的最终结果是由这些一元函数逐步组合产生的。同时,curry 的变体允许同时传递一部分参数。因此,可以完全控制函数求值的时间与方式。部分应用将函数的参数与一些预设值绑定(赋值),从而产生一个拥有更少参数的新函数。改函数的闭包中包含了这些已赋值的参数,在之后的调用中被完全求值。总结在这篇文章里我重点想介绍的是函数以组合的方式来完成我们的需求,另外介绍了一种函数式编程风格:pointfree,让我们在函数式编程里面有了一个最佳实践,尽量写成 pointfree 形式(尽量,不是都要),然后介绍了通过柯里化或者部分应用来减少函数参数,符合 compose 或者 pipe 的参数要求。所以这种文章的重点是理解我们如何去组合函数,如何去抽象复杂的函数为颗粒度更小,功能单一的函数。这将使我们的代码更容易维护,更具声明式的特点。对于这篇文章里面提到的其他概念:闭包、作用域,然后柯里化的其他用途我希望是在番外篇里面更深入的去理解,而这篇文章主要掌握函数组合就行了。参考文章JavaScript函数式编程之pointfree与声明式编程Understanding Currying in JavaScript《JavaScript 函数式编程指南》文章首发于自己的个人网站桃园,另外也可以在 github blog 上找到。如果有兴趣,也可以关注我的个人公众号:「前端桃园」 ...

February 21, 2019 · 3 min · jiezi

函数柯里化

什么是“函数柯里化”curry 的概念:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数先看一个简单例子,add函数接受 2 个参数(或者多个),addX函数接受 1 个参数。换而言之,所谓"柯里化",就是把一个多参数的函数,转化为单参数函数。将一个函数转换为一个新的函数// 非柯里化function add(x, y) { return x + y;}add(1, 2) === 3; // true// 柯里化function addX(y) { return function(x) { return x + y; };}addX(2)(1) == 3; // true柯里化的好处我能想到的是:代码复用,减少维护成本尽可能的函数化,便于阅读"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。所以,不用想太多,就是一种规范一样的东西~ ~如何实现柯里化函数 curry先来看一个栗子(这里借助了ramda,请自行安装),在下面的栗子中我们对 add 进行了柯里化,从结果上可以看到当参数为 1 个时返回的是个函数,当参数为 2 个的时候返回函数,当参数为 3 个的时候返回函数执行结果。var _ = require(“ramda”);var add = function(a, b, c) { return a, b, c;};var curry_add = _.curry(add);console.log(curry_add(1)); // 输出函数console.log(curry_add(1)(2)); // 输出函数console.log(curry_add(1)(2)(3)); // 输出结果根据上述的小栗子,可以得到,柯里化后的函数如果接受到全部参数则返回函数执行结果,否则返回一个柯里化函数。很容易想到以下伪代码var curry = function(fn) { return function() { // 假设柯里化的函数叫 curry_fn // if “curry_fn接受到的参数数量等于fn接受参数的数量” // return “fn的执行结果” // else return “一个柯里化函数” };};上述伪代码是不是很像递归?递归出口:curry_fn接受到的参数数量等于fn接受参数的数量重复逻辑:return “一个柯里化函数"于是有了以下简单实现柯里化的代码var curry = function(fn) { var limit = fn.length; // fn接受的参数个数 var params = []; // 存储递归过程的所有参数,用于递归出口计算值 return function _curry(…args) { params = params.concat(args); // 收集递归参数 if (limit <= params.length) { // 返回函数执行结果 return fn.apply(null, params); } else { // 返回一个柯里化函数 return _curry; } };};参考资料JS函数式编程指南函数式编程入门教程一行写出javascript函数式编程中的curry ...

February 19, 2019 · 1 min · jiezi

编程范式 —— 函数式编程入门

该系列会有 3 篇文章,分别介绍什么是函数式编程、剖析函数式编程库、以及函数式编程在 React 中的应用,欢迎关注我的 blog命令式编程和声明式编程拿泡茶这个事例进行区分命令式编程和声明式编程命令式编程1.烧开水(为第一人称)2.拿个茶杯3.放茶叶4.冲水声明式编程1.给我泡杯茶(为第二人称)举个 demo// 命令式编程const convert = function(arr) { const result = [] for (let i = 0; i < arr.length; i++) { result[i] = arr[i].toLowerCase() } return result}// 声明式编程const convert = function(arr) { return arr.map(r => r.toLowerCase())}什么是函数式编程函数式编程是声明式编程的范式。在函数式编程中数据在由纯函数组成的管道中传递。函数式编程可以用简单如交换律、结合律、分配律的数学之法来帮我们简化代码的实现。它具有如下一些特性:纯粹性: 纯函数不改变除当前作用域以外的值;// 反面示例let a = 0const add = (b) => a = a + b // 两次 add(1) 结果不一致// 正确示例const add = (a, b) => a + b数据不可变性: Immutable// 反面示例const arr = [1, 2]const arrAdd = (value) => { arr.push(value) return arr}arrAdd(3) // [1, 2, 3]arrAdd(3) // [1, 2, 3, 3]// 正面示例const arr = [1, 2]const arrAdd = (value) => { return arr.concat(value)}arrAdd(3) // [1, 2, 3]arrAdd(3) // [1, 2, 3]在后记 1 中对数组字符串方法是否对原值有影响作了整理函数柯里化: 将多个入参的函数转化为一个入参的函数;const add = a => b => c => a + b + cadd(1)(2)(3)偏函数: 将多个入参的函数转化成两部分;const add = a => (b, c) => a + b + cadd(1)(2, 3)可组合: 函数之间能组合使用const add = (x) => x + xconst mult = (x) => x * xconst addAndMult = (x) => add(mult(x))柯里化(curry)如下是一个加法函数:var add = (a, b, c) => a + b + cadd(1, 2, 3) // 6假如有这样一个 curry 函数, 用其包装 add 函数后返回一个新的函数 curryAdd, 我们可以将参数 a、b 进行分开传递进行调用。var curryAdd = curry(add)// 以下输出结果都相同curryAdd(1, 2, 3) // 6curryAdd(1, 2)(3) // 6curryAdd(1)(2)(3) // 6curryAdd(1)(2, 3) // 6动手实现一个 curry 函数核心思路: 若传进去的参数个数未达到 curryAdd 的个数,则将参数缓存在闭包变量 lists 中:function curry(fn, …args) { const length = fn.length let lists = args || [] let listLen return function (…_args) { lists = […lists, …_args] listLen = lists.length if (listLen < length) { const that = lists lists = [] return curry(fn, …that) } else if (listLen === length) { const that = lists lists = [] return fn.apply(this, that) } }}代码组合(compose)现在有 toUpperCase、reverse、head 三个函数, 分别如下:var toUpperCase = (str) => str.toUpperCase()var reverse = (arr) => arr.reverse()var head = (arr) => arr[0]接着使用它们实现将数组末位元素大写化输出, 可以这样做:var reverseHeadUpperCase = (arr) => toUpperCase(head(reverse(arr)))reverseHeadUpperCase([‘apple’, ‘banana’, ‘peach’]) // “PEACH"此时在构建 reverseHeadUpperCase 函数的时候, 必须手动声明传入参数 arr, 是否能提供一个 compose 函数让使用者更加友好的使用呢? 类似如下形式:var reverseHeadUpperCase = compose(toUpperCase, head, reverse)reverseHeadUpperCase([‘apple’, ‘banana’, ‘peach’]) // “PEACH"此外 compose 函数符合结合律, 我们可以这样子使用:compose(compose(toUpperCase, head), reverse)compose(toUpperCase, compose(head, reverse))以上两种写法与 compose(toUpperCase, head, reverse) 的效果完全相同, 都是依次从右到左执行传参中的函数。此外 compose 和 map 一起使用时也有相关的结合律, 以下两种写法效果相等compose(map(f), map(g))map(compose(f, g))动手实现一个 compose 函数代码精华集中在一行之内, 其为众多开源库(比如 Redux) 所采用。var compose = (…args) => (initValue) => args.reduceRight((a, c) => c(a), initValue)范畴论范畴论是数学中的一个分支。可以将范畴理解为一个容器, 把原来对值的操作,现转为对容器的操作。如下图:学习函数式编程就是学习各种函子的过程。函数式编程中, 函子(Functor) 是实现了 map 函数的容器, 下文中将函子视为范畴,模型可表示如下:class Functor { constructor(value) { this.value = value } map(fn) { return new Functor(fn(this.value)) }}但是在函数式编程中, 要避免使用 new 这种面向对象的编程方式, 取而代之对外暴露了一个 of 的接口, 也称为 pointed functor。Functor.of = value => new Functor(value)Maybe 函子Maybe 函子是为了解决 this.value 为 null 的情形, 用法如下:Maybe.of(null).map(r => r.toUpperCase()) // nullMaybe.of(’m’).map(r => r.toUpperCase()) // Maybe {value: “M”}实现代码如下:class Maybe { constructor(value) { this.value = value } map(fn) { return this.value ? new Maybe(fn(this.value)) : null }}Maybe.of = value => new Maybe(value)Either 函子Either 函子 是为了对应 if…else… 的语法, 即非左即右。因此可以将之拆分为 Left 和 Right 两个函子, 它们的用法如下:Left.of(1).map(r => r + 1) // Left {value: 1}Right.of(1).map(r => r + 1) // Right {value: 2}Left 函子实现代码如下:class Left { constructor(value) { this.value = value } map(fn) { return this }}Left.of = value => new Left(value)Right 函子实现代码如下(其实就是上面的 Functor):class Right { constructor(value) { this.value = value } map(fn) { return new Right(fn(this.value)) }}Right.of = value => new Right(value)具体 Either 函数只是对调用 Left 函子 或 Right 函子 作一层筛选, 其接收 f、g 两个函数以及一个函子(Left or Right)var Either = function(f, g, functor) { switch(functor.constructor) { case ‘Left’: return f(functor.value) case ‘Right’: return g(functor.value) default: return f(functor.value) }}使用 demo:Either((v) => console.log(’left’, v), (v) => console.log(‘def’, v), left) // left 1Either((v) => console.log(‘rigth’, v), (v) => console.log(‘def’, v), rigth) // rigth 2Monad 函子函子会发生嵌套, 比如下面这样:Functor.of(Functor.of(1)) // Functor { value: Functor { value: 1 } }Monad 函子 对外暴露了 join 和 flatmap 接口, 调用者从而可以扁平化嵌套的函子。class Monad { constructor(value) { this.value = value } map(fn) { return new Monad(fn(this.value)) } join() { return this.value } flatmap(fn) { return this.map(fn).join() }}Monad.of = value => new Monad(value)使用方法:// joinMonad.of(Monad.of(1).join()) // Monad { value: 1 }Monad.of(Monad.of(1)).join() // Monad { value: 1 }// flatmapMonad.of(1).flatmap(r => r + 1) // 2Monad 函子可以运用在 I/O 这种不纯的操作上将之变为纯函数的操作,目前比较懵懂,日后补充。后记 1: 数组字符串方法小结(是否对原值有影响)不会对原数组有影响的方法slicevar test = [1, 2, 3]var result = test.slice(0, 1)console.log(test) // [1, 2, 3]console.log(result) // [1]concatvar test = [1, 2, 3]var result = test.concat(4)console.log(test) // [1, 2, 3]console.log(result) // [1, 2, 3, 4]对原数组有影响的方法splice(这个需要特别记一下)var test = [1, 2, 3]var result = test.splice(0, 1)console.log(test) // [2, 3]console.log(result) // [1]sortvar arr = [2, 1, 3, 4]arr.sort((r1, r2) => (r1 - r2))console.log(arr) // [1, 2, 3, 4]reversevar test = [1, 2, 3]var result = test.reverse()console.log(test) // [3, 2, 1]console.log(result) // [3, 2, 1]push/pop/unshift/shiftvar test = [1, 2, 3]var result = test.push(4)console.log(test) // [1, 2, 3, 4]console.log(result) // 4不会对原字符串造成影响的方法substr/substring/slice// substrvar test = ‘abc’var result = test.substr(0, 1)console.log(test) // ‘abc’console.log(result) // a// substringvar test = ‘abc’var result = test.substring(0, 1)console.log(test) // ‘abc’console.log(result) // a// slicevar test = ‘abc’var result = test.slice(0, 1)console.log(test) // ‘abc’console.log(result) // a参考mostly-adequate-guideJavaScript 专题之函数柯里化函数式编程入门教程 ...

February 5, 2019 · 4 min · jiezi

深入理解 lambda表达式 与 函数式编程 函数式接口源码解析(二)

package com.java.design.java8;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import java.util.Arrays;import java.util.Comparator;import java.util.List;import java.util.function.*;import java.util.stream.Collectors;/** * @author 陈杨 /@RunWith(SpringRunner.class)@SpringBootTestpublic class FuncInterface {一、函数式编程的理解// 函数式编程的理解//// 函数接口式编程 是 对 业务应用的进一步抽象// 在类方法定义中 只需实现FunctionalInterface 而不管业务实现的逻辑// 在外部应用 调用该业务时 使用Lambda表达式 灵活实现其业务逻辑二、 函数式接口的测试方法1、Function接口// FunctionFunction<Integer, Integer> sum = integer -> integer + 1;Function<Integer, Integer> multiply = integer -> integer * integer;List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 0);public int testFunctionCompose(Integer integer) { return sum.compose(multiply).apply(integer);}public int testFunctionAndThen(Integer integer) { return sum.andThen(multiply).apply(integer);}2、BiFunction接口// BiFunctionBiFunction<Integer, Integer, Integer> subtract = (first, last) -> first - last;public int testBiFunctionAndThen(Integer first, Integer last) { return subtract.andThen(multiply).apply(first, last);}3、BinaryOperator接口// BinaryOperatorBinaryOperator<Integer> binaryOperator = (first, last) -> first - last;public int testBinaryOperator(Integer first, Integer last) { return binaryOperator.apply(first, last);}public String testMinBy(String first, String last, Comparator<String> comparator) { return BinaryOperator.minBy(comparator).apply(first, last);}public String testMaxBy(String first, String last, Comparator<String> comparator) { return BinaryOperator.maxBy(comparator).apply(first, last);}//比较器//比较字符串的长度Comparator<String> length = (first, last) -> first.length() - last.length();//比较字符串首字母ASCII码大小Comparator<String> asc = (first, last) -> first.charAt(0) - last.charAt(0);4、Predicate接口// Predicatepublic List<Integer> testPredicate(Predicate<Integer> predicate) { return list.stream().filter(predicate).collect(Collectors.toList());}public Predicate<String> isEqual(Object object) { return Predicate.isEqual(object);}public Predicate<Integer> notPredicate(Predicate<Integer> predicate) { return Predicate.not(predicate);}public List<Integer> testPredicateNegate(Predicate<Integer> predicate) { return list.stream().filter(predicate.negate()).collect(Collectors.toList());}public List<Integer> testPredicateAnd(Predicate<Integer> first, Predicate<Integer> last) { return list.stream().filter(first.and(last)).collect(Collectors.toList());}public List<Integer> testPredicateOr(Predicate<Integer> first, Predicate<Integer> last) { return list.stream().filter(first.or(last)).collect(Collectors.toList());}5、Supplier接口// Supplier@Data@AllArgsConstructor@NoArgsConstructorprivate class Student { private Integer id; private String name; private String sex; private Integer age; private String addr; private Double salary;}三、测试结果 . ____ _ __ _ _ /\ / ’ __ _ () __ __ _ \ \ \ ( ( )__ | ‘_ | ‘| | ‘ / ` | \ \ \ \ \/ )| |)| | | | | || (| | ) ) ) ) ’ || .__|| ||| |_, | / / / / =========||==============|/=//// :: Spring Boot :: (v2.1.2.RELEASE)2019-01-31 11:51:58.460 INFO 12080 — [ main] com.java.design.java8.FuncInterface : Starting FuncInterface on DESKTOP-87RMBG4 with PID 12080 (started by 46250 in E:\IdeaProjects\design)2019-01-31 11:51:58.461 INFO 12080 — [ main] com.java.design.java8.FuncInterface : No active profile set, falling back to default profiles: default2019-01-31 11:51:58.988 INFO 12080 — [ main] com.java.design.java8.FuncInterface : Started FuncInterface in 0.729 seconds (JVM running for 1.556)——————–Function接口的理解———————6581——————BiFunction接口的理解———————64——————-Predicate接口的理解———————获取满足条件的集合:大于4[5, 6, 7, 8, 9]——————————获取满足条件的集合:大于4且是偶数[6, 8]——————————获取满足条件的集合:大于4 取反[1, 2, 3, 4, 0]——————————获取满足条件的集合:大于4或是偶数[2, 4, 5, 6, 7, 8, 9, 0]——————————使用Objects的Equals方法判断对象是否相同true——————————Predicate.not()返回(Predicate<T>)target.negate(); [1, 2, 3, 4, 0]——————————双重否定表肯定[5, 6, 7, 8, 9]————————————————-Supplier接口的理解———————FuncInterface.Student(id=1, name=Kirito, sex=Male, age=18, addr=ShenZhen, salary=9.99999999E8)———————————————BinaryOperator接口的理解——————-继承BiFunction的Apply方法 实现减法10 - 2 = 8——————————字符串较短的是:Asuna字符串较长的是:Kirito——————————字符串首字母ASCII码较小的是:Asuna字符串首字母ASCII码较大的是:KiritoProcess finished with exit code 0四、透过现象看本质 函数式接口的源码实现1、Function接口@Testpublic void testFuncInterface() { System.out.println("——————–Function接口的理解———————\n"); // Function接口的理解 // public interface Function<T, R> // R apply(T t); // Represents a function that accepts one argument and produces a result. // 一个函数:接收一个参数 返回一个结果 // T 输入类型 R 输出类型 /default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } * Returns a composed function that first applies the {@code before} * function to its input, and then applies this function to the result. * If evaluation of either function throws an exception, it is relayed to * the caller of the composed function. / // 输入–>beforeFunction()处理–>得到结果作为下一个函数apply()的输入参数 形成函数式接口的串联调用 // beforeFunction 在当前函数apply前 进行调用 /default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } * Returns a composed function that first applies this function to * its input, and then applies the {@code after} function to the result. * If evaluation of either function throws an exception, it is relayed to * the caller of the composed function. / // 输入–>apply()处理–>得到结果作为下一个函数after.apply()的输入参数 形成函数式接口的串联调用 // afterFunction 在当前函数apply后 进行调用 /static <T> Function<T, T> identity() { return t -> t; * Returns a function that always returns its input argument. }/ // 总是返回输入参数 // 总结: // function1.compose(function2) 执行顺序 –>BeforeFunction()–>thisFunction()–> function2 –> function1 // function1.andThen(function2) 执行顺序 –>thisFunction()–>AfterFunction()–> function1 –> function2 // 前一个函数的运算结果 作为下一个函数的输入参数 // 理解运行时机 可以类比 Junit中 @Before 与 @After System.out.println(this.testFunctionCompose(8)); System.out.println(this.testFunctionAndThen(8));2、BiFunction接口System.out.println("——————BiFunction接口的理解———————\n");// BiFunction 接口 的 理解// @FunctionalInterface// public interface BiFunction<T, U, R> {// R apply(T t, U u);// 一个函数:接收二个参数 返回一个结果/ default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t, U u) -> after.apply(apply(t, u)); } }/// 利用反证法 可以证明 BiFunction 没有 compose方法 (提示: 参数 与 返回值)// 将2个参数应用于BiFunction 只会得到一个返回值 这个返回值会作为Function传入的参数// biFunction.andthen(function)System.out.println(this.testBiFunctionAndThen(10, 2));3、Predicate接口System.out.println("——————-Predicate接口的理解———————\n");// public interface Predicate<T> {// Represents a predicate (boolean-valued function) of one argument./ * Evaluates this predicate on the given argument. * * 接收一个判断 判断是否满足预期条件 返回true false * boolean test(T t); /System.out.println(“获取满足条件的集合:大于4”);System.out.println(this.testPredicate(in -> in > 4));System.out.println("——————————\n"); / * Returns a composed predicate that represents a short-circuiting logical * AND of this predicate and another. When evaluating the composed * predicate, if this predicate is {@code false}, then the {@code other} * predicate is not evaluated. * * 短路逻辑与计算 * default Predicate<T> and(Predicate<? super T> other) { * Objects.requireNonNull(other); * return (t) -> test(t) && other.test(t); }/System.out.println(“获取满足条件的集合:大于4且是偶数”);System.out.println(this.testPredicateAnd(in -> in > 4, in -> in % 2 == 0));System.out.println("——————————\n");/ * Returns a predicate that represents the logical negation of this * predicate. * * 取反 * default Predicate<T> negate() { * return (t) -> !test(t); * } /System.out.println(“获取满足条件的集合:大于4 取反”);System.out.println(this.testPredicateNegate(in -> in > 4));System.out.println("——————————\n");/ * Returns a composed predicate that represents a short-circuiting logical * OR of this predicate and another. When evaluating the composed * predicate, if this predicate is {@code true}, then the {@code other} * predicate is not evaluated. * * 短路逻辑或计算 * default Predicate<T> or(Predicate<? super T> other) { * Objects.requireNonNull(other); * return (t) -> test(t) || other.test(t); * } /System.out.println(“获取满足条件的集合:大于4或是偶数”);System.out.println(this.testPredicateOr(in -> in > 4, in -> in % 2 == 0));System.out.println("——————————\n");/ * Returns a predicate that tests if two arguments are equal according * to {@link Objects#equals(Object, Object)}. * * 根据Objects的equals方法 来判断两个对象 是否相同 * static <T> Predicate<T> isEqual(Object targetRef) { * return (null == targetRef) * ? Objects::isNull * : object -> targetRef.equals(object); * } /System.out.println(“使用Objects的Equals方法判断对象是否相同”);System.out.println(this.isEqual(“Kirito”).test(“Kirito”));System.out.println("——————————\n");/ * Returns a predicate that is the negation of the supplied predicate. * This is accomplished by returning result of the calling * {@code target.negate()}. * * 返回提供的predicate的否定 * @SuppressWarnings(“unchecked”) * static <T> Predicate<T> not(Predicate<? super T> target) { * Objects.requireNonNull(target); * return (Predicate<T>)target.negate(); * } * } /System.out.println(“Predicate.not()返回(Predicate<T>)target.negate(); “);System.out.println(testPredicate(this.notPredicate(integer -> integer > 4)));System.out.println(”——————————\n”);System.out.println(“双重否定表肯定”);System.out.println(testPredicateNegate(this.notPredicate(integer -> integer > 4)));System.out.println("——————————\n");4、Supplier接口System.out.println("——————-Supplier接口的理解———————\n");/ * Represents a supplier of results. * * public interface Supplier<T> { * T get(); * } /// 构造方法引用 构造函数接口实例// 利用Supplier接口 Student类必须要有无参的构造方法// Supplier<Student> studentSupplier = () -> new Student();Supplier<Student> studentSupplier = Student::new;Student student = studentSupplier.get();student.setId(1);student.setName(“Kirito”);student.setSex(“Male”);student.setAge(18);student.setSalary(999999999.0);student.setAddr(“ShenZhen”);System.out.println(student);System.out.println("——————————\n");5、BinaryOperator接口 System.out.println("—————BinaryOperator接口的理解——————-\n"); / * * public interface BinaryOperator<T> extends BiFunction<T,T,T> { * * 返回2个比较参数之间的较小值 * public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) { * Objects.requireNonNull(comparator); * return (a, b) -> comparator.compare(a, b) <= 0 ? a : b; * } * * 返回2个比较参数之间的较大值 * public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) { * Objects.requireNonNull(comparator); * return (a, b) -> comparator.compare(a, b) >= 0 ? a : b; * } * } */ System.out.println(“继承BiFunction的Apply方法 实现减法”); System.out.println(“10 - 2 = “+this.testBinaryOperator(10, 2)); System.out.println(”——————————\n”); System.out.println(“字符串较短的是:"+this.testMinBy(“Kirito”,“Asuna”,length)); System.out.println(“字符串较长的是:"+this.testMaxBy(“Kirito”,“Asuna”,length)); System.out.println(”——————————\n”); System.out.println(“字符串首字母ASCII码较小的是:"+this.testMinBy(“Kirito”,“Asuna”,asc)); System.out.println(“字符串首字母ASCII码较大的是:"+this.testMaxBy(“Kirito”,“Asuna”,asc)); }} ...

January 31, 2019 · 6 min · jiezi

深入理解lambda表达式与@FunctionalInterface函数式接口(一)

一、集合遍历与Lambda表达式 引入package com.java.design.java8;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner;import java.util.*;import java.util.function.Consumer;/** * @author 陈杨 */@RunWith(SpringRunner.class)@SpringBootTestpublic class ErgodicList { @Test public void testErgodicList() { // 直接构造集合对象 保证了集合size>0 List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 0); System.out.println("—————————传统for循环——————–\n"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } System.out.println("—————————增强for循环——————–\n"); for (Integer i : list) { System.out.println(i); } System.out.println("—————————迭代器————————-\n"); Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { Integer integer = iterator.next(); System.out.println(integer); } System.out.println("—————————forEach————————\n"); list.forEach(new Consumer<Integer>() { @Override public void accept(Integer integer) { System.out.println(integer); } });二、 @FunctionalInterface函数式接口与Lambda表达式1、概念// Consumer @FunctionalInterface函数式接口// Conceptually, a functional interface has exactly one abstract method.// 从概念上看,一个函数式接口有且只有一个精确的抽象方法// 从java8开始 接口中不仅仅存在抽象方法 还能存在有具体实现的方法(默认方法)2、 函数式接口的区分// Since {@linkplain java.lang.reflect.Method#isDefault()// default methods} have an implementation, they are not abstract. If// an interface declares an abstract method overriding one of the// public methods of {@code java.lang.Object}, that also does// <em>not</em> count toward the interface’s abstract method count// since any implementation of the interface will have an// implementation from {@code java.lang.Object} or elsewhere.// 因为java.lang.reflect.Method#isDefault() default methods 有一个实现 所以不是抽象的// 如果一个接口声明一个抽象方法,其实现了java.lang.Object类中public方法:不计入抽象方法的个数3、函数式接口的实例化方式// Note that instances of functional interfaces can be created with// lambda expressions, method references, or constructor references.// 函数式接口的实例化: lambda表达式 方法引用 构造方法引用4、函数式接口中的默认方法// default void forEach(Consumer<? super T> action) {// Objects.requireNonNull(action);// for (T t : this) {// action.accept(t);// }// }// action 针对每个元素 执行的动作行为// default 修饰接口 已实现的默认方法5、总结与思考// 1、如果一个接口中有且只有一个抽象方法 则其为一个函数式接口// 2、如果一个接口上声明了@FunctionalInterface注解 则编译器会按照函数式接口的定义来要求该接口// If a type is annotated with this annotation type, compilers are// required to generate an error message unless:// (1)The type is an interface type and not an annotation type, enum, or class.// (2)The annotated type satisfies the requirements of a functional interface.// However, the compiler will treat any interface meeting the// definition of a functional interface as a functional interface// regardless of whether or not a {@code FunctionalInterface}// annotation is present on the interface declaration.// 3、如果接口上只有一个抽象方法,但我们没有对其加上@FunctionalInterface 编译器仍然将其看作函数式接口// 加上注解后 一目了然 如果没有满足强制性要求 则会抛出错误信息// 4、只有一个抽象方法的接口 有必要加上 @FunctionalInterface 如 Runnable接口// 5、所有的函数式接口 都可以使用lambda表达式 实现(表达易懂 简单)三、函数式接口实例化 之 Lambda表达式System.out.println("——————–lambda创建函数式接口实例—————\n");list.forEach(i -> { // 若能推断出i的类型 不需要声明 // 如不能推断出(Integer i)->{} System.out.println(i);});四、在排序过程中 Lambda表达式的 演变System.out.println("————————–lambda排序———————–\n");// Collections.sort(list, new Comparator<Integer>() {// @Override// public int compare(Integer o1, Integer o2) {// return o2.compareTo(o1);// }// });// Collections.sort(list,(String o1, String o2)->{return o2.compareTo(o1);});// Collections.sort(list, (o1, o2) -> { return o2.compareTo(o1); });// statement { return o2.compareTo(o1); }// expression o2.compareTo(o1)// Collections.sort(list,(String::compareTo));// Collections.sort(list,Collections.reverseOrder());Collections.sort(list, (o1, o2) -> o2.compareTo(o1));System.out.println(list);五、函数式接口实例化 之 方法引用System.out.println("——————–方法引用创建函数式接口实例————–\n");list.forEach(System.out::println); }}六、深入理解Lambda表达式// lambda表达式:// 1、从函数式编程角度来看:// lambda表达式为Java添加了函数式编程的新特性 函数升格成为一等公民// 在函数作为一等公民的语言 如Python中 lambda表达式为函数类型// 但在java中 lambda表达式是对象类型 依赖于函数式接口Functional Interface// 2、lambda表达式书写// lambda表达式形式 () -> {} 必需根据上下文确定其匿名函数类型 函数方法 -> 函数实现// () 省略 参数只有一个且类型可根据上下文推导// {} 省略 方法体主体只有一条语句,返回值类型与主体表达式(匿名函数)一致// 3、进一步理解lambda表达式// lambda表达式传递行为action 不仅仅是值的传递 (类比Node.js的事件驱动 与 回调函数callback)// lambda表达式替换前: 事先定义对象及所持有的方法 根据 “对象.方法” 进行方法的调用 预先定义好的action// lambda表达式替换后: {} 方法调用 R apply(T t); 事先不知道action 仅在调用时才知道 action// 提升抽象层次 API重用性 使用灵活 ...

January 30, 2019 · 2 min · jiezi

【译】再见,面向对象编程(二)

Charles Scalfani原文:https://medium.com/@cscalfani…元旦过了,以下是第二部分的翻译。Museum by Mark Chang https://www.artstation.com/ar…封装,第二个坍塌的支柱乍一看,封装应该是OO编程中的第二大收益。对象的变量状态不能从外部访问,他们被封装在对象中。封装对你的变量来说很安全。封装太厉害了!直到。。。引用的问题基于效率上的考量,对象不是基于他们的值而是基于他们的引用传递给方法。这个意思是对象没有传给方法,实际传的是引用或对象的指针。如果一个对象是按照引用传给对象的构造函数,构造函数可以把对象引用放在被封装所保护的私有变量中。但是传递的对象并不安全!为什么?因为代码的其他地方有指向对象的指针,那段代码可以调用构造函数。他必须要有对对象的引用不然他不可以将其传给构造函数。引用的解法构造函数需要将传递过来的对象克隆。而且不是浅克隆而是一个深克隆,每一个传递给对象的参数里包含的对象与每一个在这个对象中包含的对象,一个接一个。都是为了效率。不是所有的对象都可以被克隆。一些是与操作系统资源所关联的对象,对其进行克隆最好的结局是没什么用,而最坏的结局是做不了。而每一个主流OO语言都有这个问题。再见,封装。多态,第三个坍塌的支柱Polymorphism was the redheaded stepchild of the Object Oriented Trinity.It’s sort of the Larry Fine of the group.Everywhere they went he was there, but he was just a supporting character.(翻译注,以上三句水平有限,应该有美语语境的典故,不好翻译,故放原文)并不是说多态不好,只是你不需要在面向对象语言里做这个。接口可以给你这个能力。而不需要OO的特性。而使用接口,不管你加入多少行为都没有任何限制。所以不用太麻烦,我们对基于OO的多态说再见,拥抱基于接口的多态。被打破的承诺当然,OO在早期承诺了很多。而这些承诺对于在教室中的,读博客和学习线上课程的天真程序员有效。我花了许多年才认识到OO骗了我。我被卖了。再见,面向对象编程。怎么办?你好,函数式编程。能在这几年能遇到你真不错。只有再一,没有再二。如果你想加入一个web开发者互相学习和帮助的使用Elm函数式编程的社区,你可以访问我的Facebook组, Learn Elm Programming。https://www.facebook.com/grou…—微信公众号「麦芽面包」,id「darkjune_think」开发者/科幻爱好者/硬核主机玩家/业余翻译家/书虫交流Email: zhukunrong@yeah.net

January 2, 2019 · 1 min · jiezi

学会使用函数式编程的程序员(第3部分)

本系列的其它篇:学会使用函数式编程的程序员(第1部分)学会使用函数式编程的程序员(第2部分)引用透明 (Referential Transparency)引用透明是一个富有想象力的优秀术语,它是用来描述纯函数可以被它的表达式安全的替换,通过下例来帮助我们理解。在代数中,有一个如下的公式:y = x + 10接着: x = 3然后带入表达式:y = 3 + 10注意这个方程仍然是有效的,我们可以利用纯函数做一些相同类型的替换。下面是一个 JavaScript 的方法,在传入的字符串两边加上单引号:function quote (str) { retrun “’” + str + “’"}下面是调用它: function findError (key) { return “不能找到 " + quote(key) }当查询 key 值失败时,findError 返回一个报错信息。因为 quote 是纯函数,我们可以简单地将 quote 函数体(这里仅仅只是个表达式)替换掉在findError中的方法调用: function findError (key) { return “不能找到 " + “’” + str + “’” }这个就是通常所说的“反向重构”(它对我而言有更多的意义),可以用来帮程序员或者程序(例如编译器和测试程序)推理代码的过程一个很好的方法。如,这在推导递归函数时尤其有用的。执行顺序 (Execution Order)大多数程序都是单线程的,即一次只执行一段代码。即使你有一个多线程程序,大多数线程都被阻塞等待I/O完成,例如文件,网络等等。这也是当我们编写代码的时候,我们很自然考虑按次序来编写代码:1. 拿到面包 2. 把2片面包放入烤面包机 3. 选择加热时间 4. 按下开始按钮 5. 等待面包片弹出 6. 取出烤面包 7. 拿黄油 8. 拿黄油刀 9. 制作黄油面包 在这个例子中,有两个独立的操作:拿黄油以及 加热面包。它们在 步骤9 时开始变得相互依赖。我们可以将 步骤7 和 步骤8 与 步骤1 到 步骤6 同时执行,因为它们彼此独立。当我们开始做的时候,事情开始复杂了:线程一————————–1. 拿到面包 2. 把2片面包放入烤面包机 3. 选择加热时间 4. 按下开始按钮 5. 等待面包片弹出 6. 取出烤面包 线程二————————-1. 拿黄油 2. 拿黄油刀 3. 等待线程1完成 4. 取出烤面包 果线程1失败,线程2怎么办? 怎么协调这两个线程? 烤面包这一步骤在哪个线程运行:线程1,线程2或者两者?不考虑这些复杂性,让我们的程序保持单线程会更容易。但是,只要能够提升我们程序的效率,要付出努力来写好多线程程序,这是值得的。然而,多线程有两个主要问题:多线程程序难于编写、读取、解释、测试和调试。一些语言,例如JavaScript,并不支持多线程,就算有些语言支持多线程,对它的支持也很弱。但是,如果顺序无关紧要,所有事情都是并行执行的呢?尽管这听起来有些疯狂,但其实并不像听起来那么混乱。让我们来看一下 Elm 的代码来形象的理解它:buildMessage message value = let upperMessage = String.toUpper message quotedValue = “’” ++ value ++ “’” in upperMessage ++ “: " ++ quotedValue这里的 buildMessage 接受参数 message 和 value,然后,生成大写的 message和 带有引号的 value 。注意到 upperMessage 和 quotedValue 是独立的。我们怎么知道的呢?在上面的代码示例中,upperMessage 和 quotedValue 两者都是纯的并且没有一个需要依赖其它的输出。如果它们不纯,我们就永远不知道它们是独立的。在这种情况下,我们必须依赖程序中调用它们的顺序来确定它们的执行顺序。这就是所有命令式语言的工作方式。第二点必须满足的就是一个函数的输出值不能作为其它函数的输入值。如果存在这种情况,那么我们不得不等待其中一个完成才能执行下一个。在本例中,upperMessage 和 quotedValue 都是纯的并且没有一个需要依赖其它的输出,因此,这两个函数可以以任何顺序执行。编译器可以在不需要程序员帮助的情况下做出这个决定。这只有在纯函数式语言中才有可能,因为很难(如果不是不可能的话)确定副作用的后果。在纯函数语言中,执行的顺序可以由编译器决定。考虑到 CPU 无法一再的加快速度,这种做法非常有利的。别一方面,生产商也不断增加CPU内核芯片的数量,这意味着代码可以在硬件层面上并行执行。使用纯函数语言,就有希望在不改变任何代码的情况下充分地发挥 CPU 芯片的功能并取得良好成效。类型注释 (Type Annotations)在静态类型语言中,类型是内联定义的。以下是 Java 代码:public static String quote(String str) { return “’” + str + “’”;}注意类型是如何同函数定义内联在一起的。当有泛型时,它变的更糟:private final Map<Integer, String> getPerson(Map<String, String> people, Integer personId) { // …}这里使用粗体标出了使它们使用的类型,但它们仍然会让函数可读性降低,你必须仔细阅读才能找到变量的名称。对于动态类型语言,这不是问题。在 Javascript 中,可以编写如下代码:var getPerson = function(people, personId) { // …};这样没有任何的的类型信息更易于阅读,唯一的问题就是放弃了类型检测的安全特性。这样能够很简单的传入这些参数,例如,一个 Number 类型的 people 以及一个 Objec t类型的 personId。动态类型要等到程序执行后才能知道哪里问题,这可能是在发布的几个月后。在 Java 中不会出现这种情况,因为它不能被编译。但是,假如我们能同时拥有这两者的优异点呢? JavaScript 的语法简单性以及 Java 的安全性。事实证明我们可以。下面是 Elm 中的一个带有类型注释的函数:add : Int -> Int -> Intadd x y = x + y请注意类型信息是在单独的代码行上面的,而正是这样的分割使得其有所不同。现在你可能认为类型注释有错训。 第一次见到它的时候。 大都认为第一个 -> 应该是一个逗号。可以加上隐含的括号,代码就清晰多了:add : Int -> (Int -> Int)上例 add 是一个函数,它接受类型为 Int 的单个参数,并返回一个函数,该函数接受单个参数 Int类型 并返回一个 Int 类型的结果。以下是一个带括号类型注释的代码:doSomething : String -> (Int -> (String -> String)) doSomething prefix value suffix = prefix ++ (toString value) ++ suffix这里 doSomething 是一个函数,它接受 String 类型的单个参数,然后返回一个函数,该函数接受 Int 类型的单个参数,然后返回一个函数,该函数接受 String 类型的单个参数,并返回一个字符串。注意为什么每个方法都只接受一个参数呢? 这是因为每个方法在 Elm 里面都是柯里化。因为括号总是指向右边,它们是不必要的,简写如下:doSomething : String -> Int -> String -> String当我们将函数作为参数传递时,括号是必要的。如果没有它们,类型注释将是不明确的。例如:takes2Params : Int -> Int -> Stringtakes2Params num1 num2 = – do something非常不同于:takes1Param : (Int -> Int) -> Stringtakes1Param f = – do somethingtakes2Param 函数需要两个参数,一个 Int 和另一个 Int,而takes1Param 函数需要一个参数,这个参数为函数, 这个函数需要接受两个 Int 类型参数。下面是 map 的类型注释:map : (a -> b) -> List a -> List bmap f list = // …这里需要括号,因为 f 的类型是(a -> b),也就是说,函数接受类型 a 的单个参数并返回类型 b 的某个函数。这里类型 a 是任何类型。当类型为大写形式时,它是显式类型,例如 String。当一个类型是小写时,它可以是任何类型。这里 a 可以是字符串,也可以是 Int。如果你看到 (a -> a) 那就是说输入类型和输出类型必须是相同的。它们是什么并不重要,但必须匹配。但在 map 这一示例中,有这样一段 (a -> b)。这意味着它既能返回一个不同的类型,也能返回一个相同的类型。但是一旦 a 的类型确定了,a 在整段代码中就必须为这个类型。例如,如果 a 是一个 Int,b 是一个 String,那么这段代码就相当于:(Int -> String) -> List Int -> List String这里所有的 a 都换成了 Int,所有的 b 都换成了 String。List Int 类型意味着一个值都为 Int 类型的列表, List String 意味着一个值都为 String 类型的列表。如果你已经在 Java 或者其他的语言中使用过泛型,那么这个概念你应该是熟悉的函数式 JavaScriptJavaScript 拥有很多类函数式的特性但它没有纯性,但是我们可以设法得到一些不变量和纯函数,甚至可以借助一些库。但这并不是理想的解决方法。如果你不得不使用纯特性,为何不直接考虑函数式语言?这并不理想,但如果你必须使用它,为什么不从函数式语言中获得一些好处呢?不可变性(Immutability)首先要考虑的是不变性。在ES2015或ES6中,有一个新的关键词叫const,这意味着一旦一个变量被设置,它就不能被重置:const a = 1;a = 2; // 这将在Chrome、Firefox或 Node中抛出一个类型错误,但在Safari中则不会在这里,a 被定义为一个常量,因此一旦设置就不能更改。这就是为什么 a = 2 抛出异常。 const 的缺陷在于它不够严格,我们来看个例子:const a = { x: 1, y: 2};a.x = 2; // 没有异常a = {}; // 报错注意到 a.x = 2 没有抛出异常。const 关键字唯一不变的是变量 a, a 所指向的对象是可变的。那么Javascript中如何获得不变性呢?不幸的是,我们只能通过一个名为 Immutable.js 的库来实现。这可能会给我们带来更好的不变性,但遗憾的是,这种不变性使我们的代码看起来更像 Java 而不是 Javascript。柯里化与组合 (curring and composition)在本系列的前面,我们学习了如何编写柯里化函数,这里有一个更复杂的例子:const f = a => b => c => d => a + b + c + d我们得手写上述柯里化的过程,如下:console.log(f(1)(2)(3)(4)); // prints 10括号如此之多,但这已经足够让Lisp程序员哭了。有许多库可以简化这个过程,我最喜欢的是 Ramda。使用 Ramda 简化如下:const f = R.curry((a, b, c, d) => a + b + c + d);console.log(f(1, 2, 3, 4)); // prints 10console.log(f(1, 2)(3, 4)); // also prints 10console.log(f(1)(2)(3, 4)); // also prints 10函数的定义并没有好多少,但是我们已经消除了对那些括号的需要。注意,调用 f 时,可以指定任意参数。重写一下之前的 mult5AfterAdd10 函数:const add = R.curry((x, y) => x + y);const mult5 = value => value * 5;const mult5AfterAdd10 = R.compose(mult5, add(10));事实上 Ramda 提供了很多辅助函数来做些简单常见的运算,比如R.add以及R.multiply。以上代码我们还可以简化:const mult5AfterAdd10 = R.compose(R.multiply(5), R.add(10));Map, Filter and ReduceRamda 也有自己的 map、filter和 reduce 版本。虽然这些函数存在于数组中。这几个函数是在 Array.prototype 对象中的,而在 Ramda 中它们是柯里化的const isOdd = R.flip(R.modulo)(2);const onlyOdd = R.filter(isOdd);const isEven = R.complement(isOdd);const onlyEven = R.filter(isEven);const numbers = [1, 2, 3, 4, 5, 6, 7, 8];console.log(onlyEven(numbers)); // prints [2, 4, 6, 8]console.log(onlyOdd(numbers)); // prints [1, 3, 5, 7]R.modulo 接受2个参数,被除数和除数。isOdd 函数表示一个数除 2 的余数。若余数为 0,则返回 false,即不是奇数;若余数为 1,则返回 true,是奇数。用 R.filp 置换一下 R.modulo 函数两个参数顺序,使得 2 作为除数。isEven 函数是 isOdd 函数的补集。onlyOdd 函数是由 isOdd 函数进行断言的过滤函数。当它传入最后一个参数,一个数组,它就会被执行。同理,onlyEven 函数是由 isEven 函数进行断言的过滤函数。当我们给函数 onlyEven 和 onlyOd 传入 numbers,isEven 和 isOdd 获得了最后的参数,然后执行最终返回我们期望的数字。Javascript的缺点所有的库和语言增强都已经得到了Javascript 的发展,但它仍然面临着这样一个事实:它是一种强制性的语言,它试图为所有人提供所有的东西。大多数前端开发人员都不得不使用 Javascript,因为这旨浏览器也识别的语言。相反,它们使用不同的语言编写,然后编译,或者更准确地说,是把其它语言转换成 Javascript。CoffeeScript 是这类语言中最早的一批。目前,TypeScript 已经被 Angular2 采用,Babel可以将这类语言编译成 JavaScript,越来越多的开发者在项目中采用这种方式。但是这些语言都是从 Javascript 开始的,并且只稍微改进了一点。为什么不直接从纯函数语言转换到Javascript呢?未来期盼我们不可能知道未来会怎样,但我们可以做一些有根据的猜测。以下是作者的一些看法:能转换成 JavaScript 这类语言会有更加丰富及健壮。已有40多年历史的函数式编程思想将被重新发现,以解决我们当前的软件复杂性问题。目前的硬件,比如廉价的内存,快速的处理器,使得函数式技术普及成为可能。PU不会变快,但是内核的数量会持续增加。可变状态将被认为是复杂系统中最大的问题之一。希望这系列文章能帮助你更好容易更好帮助你理解函数式编程及优势,作者相信函数式编程是未来趋势,大家有时间可以多多了解,接着提升你们的技能,然后未来有更好的出路。原文:https://medium.com/@cscalfani…https://medium.com/@cscalfani…编辑中可能存在的bug没法实时知道,事后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具Fundebug。你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》! ...

December 28, 2018 · 3 min · jiezi

学会使用函数式编程的程序员(第2部分)

本系列的第一篇:学会使用函数式编程的程序员(第1部分)组合函数 (Function Composition)作为程序员,我们是懒惰的。我们不想构建、测试和部署我们编写的一遍又一遍的代码。我们总是试图找出一次性完成工作的方法,以及如何重用它来做其他事情。代码重用听起来很棒,但是实现起来很难。如果代码业务性过于具体,就很难重用它。如时代码太过通用简单,又很少人使用。所以我们需要平衡两者,一种制作更小的、可重用的部件的方法,我们可以将其作为构建块来构建更复杂的功能。在函数式编程中,函数是我们的构建块。每个函数都有各自的功能,然后我们把需要的功能(函数)组合起来完成我们的需求,这种方式有点像乐高的积木,在编程中我们称为 组合函数。看下以下两个函数:var add10 = function(value) { return value + 10;};var mult5 = function(value) { return value * 5;};上面写法有点冗长了,我们用箭头函数改写一下:var add10 = value => value + 10;var mult5 = value => value * 5;现在我们需要有个函数将传入的参数先加上 10 ,然后在乘以 5, 如下:var mult5AfterAdd10 = value => 5 * (value + 10)尽管这是一个非常简单的例子,但仍然不想从头编写这个函数。首先,这里可能会犯一个错误,比如忘记括号。第二,我们已经有了一个加 10 的函数 add10 和一个乘以 5 的函数 mult5 ,所以这里我们就在写已经重复的代码了。使用函数 add10,mult5 来重构 mult5AfterAdd10 :var mult5AfterAdd10 = value => mult5(add10(value));我们只是使用现有的函数来创建 mult5AfterAdd10,但是还有更好的方法。在数学中, f ∘ g 是函数组合,叫作“f 由 g 组合”,或者更常见的是 “f after g”。 因此 (f ∘ g)(x) 等效于f(g(x)) 表示调用 g 之后调用 f。在我们的例子中,我们有 mult5 ∘ add10 或 “add10 after mult5”,因此我们的函数的名称叫做 mult5AfterAdd10。由于Javascript本身不做函数组合,看看 Elm 是怎么写的:add10 value = value + 10mult5 value = value * 5mult5AfterAdd10 value = (mult5 << add10) value在 Elm 中 << 表示使用组合函数,在上例中 value 传给函数 add10 然后将其结果传递给mult5。还可以这样组合任意多个函数:f x = (g << h << s << r << t) x这里 x 传递给函数 t,函数 t 的结果传递给 r,函数 t 的结果传递给 s,以此类推。在Javascript中做类似的事情,它看起来会像 g(h(s(r(t(x))))),一个括号噩梦。Point-Free NotationPoint-Free Notation就是在编写函数时不需要指定参数的编程风格。一开始,这风格看起来有点奇怪,但是随着不断深入,你会逐渐喜欢这种简洁的方式。在 multi5AfterAdd10 中,你会注意到 value 被指定了两次。一次在参数列表,另一次是在它被使用时。// 这个函数需要一个参数mult5AfterAdd10 value = (mult5 << add10) value 但是这个参数不是必须的,因为该函数组合的最右边一个函数也就是 add10 期望相同的参数。下面的 point-free 版本是等效的:// 这也是一个需要1个参数的函数mult5AfterAdd10 = (mult5 << add10)使用 point-free 版本有很多好处。首先,我们不需要指定冗余的参数。由于不必指定参数,所以也就不必考虑为它们命名。由于更简短使得更容易阅读。本例比较简单,想象一下如果一个函数有多个参数的情况。天堂里的烦恼到目前为止,我们已经了解了组合函数如何工作以及如何通过 point-free 风格使函数简洁、清晰、灵活。现在,我们尝试将这些知识应用到一个稍微不同的场景。想象一下我使用 add 来替换 add10:add x y = x + ymult5 value = value * 5现在如何使用这两个函数来组合函数 mult5After10 呢?我们可能会这样写:– 这是错误的!!!mult5AfterAdd10 = (mult5 << add) 10 但这行不通。为什么? 因为 add 需要两个参数。这在 Elm 中并不明显,请尝试用Javascript编写:var mult5AfterAdd10 = mult5(add(10)); // 这个行不通这段代码是错误的,但是为什么?因为这里 add 函数只能获取到两个参数(它的函数定义中指定了两个参数)中的一个(实际只传递了一个参数),所以它会将一个错误的结果传递给 mult5。这最终会产生一个错误的结果。事实上,在 Elm 中,编译器甚至不允许你编写这种格式错误的代码(这是 Elm 的优点之一)。我们再试一次:var mult5AfterAdd10 = y => mult5(add(10, y)); // not point-free这个不是point-free风格但是我觉得还行。但是现在我不再仅仅组合函数。我在写一个新函数。同样如果这个函数更复杂,例如,我想使用一些其他的东西来组合mult5AfterAdd10,我真的会遇到麻烦。由于我们不能将这个两个函数对接将会出现函数组合的作用受限。这太糟糕了,因为函数组合是如此强大。如果我们能提前给add函数一个参数然后在调用 mult5AfterAdd10时得到第二个参数那就更好了。这种转化我们叫做 柯里化。柯里化 (Currying)Currying又称部分求值。一个 Currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值上例我们在组合函数 mult5和 add(in) 时遇到问题的是,mult5 使用一个参数,add 使用两个参数。我们可以通过限制所有函数只取一个参数来轻松地解决这个问题。我只需编写一个使用两个参数但每次只接受一个参数的add函数,函数柯里化就是帮我们这种工作的。柯里化函数一次只接受一个参数。我们先赋值 add 的第1个参数,然后再组合上 mult5,得到 mult5AfterAdd10 函数。当 mult5AfterAdd10 函数被调用的时候,add 得到了它的第 2 个参数。JavaScript 实现方式如下:var add = x => y => x + y此时的 add 函数先后分两次得到第 1 个和第 2 个参数。具体地说,add函数接受单参x,返回一个也接受单参 y的函数,这个函数最终返回 x+y 的结果。现在可以利用这个 add 函数来实现一个可行的 mult5AfterAdd10* :var compose = (f, g) => x => f(g(x));var mult5AfterAdd10 = compose(mult5, add(10));compose 有两个参数 f 和 g,然后返回一个函数,该函数有一个参数 x,并传给函数 f,当函数被调用时,先调用函数 g,返回的结果作为函数 f的参数。总结一下,我们到底做了什么?我们就是将简单常见的add函数转化成了柯里化函数,这样add函数就变得更加自由灵活了。我们先将第1个参数10输入,而当mult5AfterAdd10函数被调用的时候,最后1个参数才有了确定的值。柯里化与重构(Curring and Refactoring)函数柯里化允许和鼓励你分隔复杂功能变成更小更容易分析的部分。这些小的逻辑单元显然是更容易理解和测试的,然后你的应用就会变成干净而整洁的组合,由一些小单元组成的组合。例如,我们有以下两个函数,它们分别将输入字符串用单花括号和双花括号包裹起来:bracketed = function (str) { retrun “{” + str + “}”} doubleBracketed = function (str) { retrun “{{” + str + “}}”} 调用方式如下:var bracketedJoe = bracketed(‘小智’)var doubleBracketedJoe = doubleBracketed(‘小智’)可以将 bracket 和 doubleBracket 转化为更变通的函数:generalBracket = function( prefix , str ,suffix ) { retrun prefix ++ str ++ suffix}但每次我们调用 generalBracket 函数的时候,都得这么传参:var bracketedJoe = generalBracket("{", “小智”, “}")var doubleBracketedJoe = generalBracket(”{{", “小智”, “}}")之前参数只需要输入1个,但定义了2个独立的函数;现在函数统一了,每次却需要传入3个参数,这个不是我们想要的,我们真正想要的是两全其美。因为生成小括号双括号功能但一,重新调整一下 我们将 generalBracket 三个参数中的 prefix,str 各柯里化成一个函数,如下:generalBracket = function( prefix ) { return function( suffix ){ return function(str){ return prefix + str + suffix } }}这样,如果我们要打印单括号或者双括号,如下:// 生成单括号var bracketedJoe = generalBracket(’{’)(’}’)bracketedJoe(‘小智’) // {小智}// 生成双括号var bracketedJoe = generalBracket(’{{’)(’}}’)bracketedJoe(‘小智’) // {{小智}} 常见的函数式函数(Functional Function)函数式语言中3个常见的函数:Map,Filter,Reduce。如下JavaScript代码:for (var i = 0; i < something.length; ++i) { // do stuff } 这段代码存在一个很大的问题,但不是bug。问题在于它有很多重复代码(boilerplate code)。如果你用命令式语言来编程,比如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; // 警告:值被改变!}console.log(things); // [10, 20, 30, 40]这样做法很不对,数值被改变了!在重新修改一次: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,因为这个函数的功能就是将一个数组的每个值映射(map)到新数组的一个新值。var map = (f, array) => {var newArray = [];for (var i = 0; i < array.length; ++i) { newArray[i] = f(array[i]);}return newArray;};函数 f 作为参数传入,那么函数 map 可以对 array 数组的每项进行任意的操作。现在使用 map 重写之前的代码:var things = [1, 2, 3, 4];var newThings = map(v => v * 10, things);这里没有 for 循环!而且代码更具可读性,也更易分析。现在让我们写另一个常见的函数来过滤数组中的元素:var filter = (pred, array) => { var newArray = [];for (var i = 0; i < array.length; ++i) { if (pred(array[i])) newArray[newArray.length] = array[i]; } return newArray;};当某些项需要被保留的时候,断言函数 pred 返回TRUE,否则返回FALSE。使用过滤器过滤奇数:var isOdd = x => x % 2 !== 0;var numbers = [1, 2, 3, 4, 5];var oddNumbers = filter(isOdd, numbers);console.log(oddNumbers); // [1, 3, 5]比起用 for 循环的手动编程,filter 函数简单多了。最后一个常见函数叫reduce。通常这个函数用来将一个数列归约(reduce)成一个数值,但事实上它能做很多事情。在函数式语言中,这个函数称为 fold。var reduce = (f, start, array) => {var acc = start;for (var i = 0; i < array.length; ++i) acc = f(array[i], acc); // f() 有2个参数return acc;});reduce函数接受一个归约函数 f,一个初始值 start,以及一个数组 array。这三个函数,map,filter,reduce能让我们绕过for循环这种重复的方式,对数组做一些常见的操作。但在函数式语言中只有递归没有循环,这三个函数就更有用了。附带提一句,在函数式语言中,递归函数不仅非常有用,还必不可少。原文:https://medium.com/@cscalfani…https://medium.com/@cscalfani…编辑中可能存在的bug没法实时知道,事后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具Fundebug。你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》! ...

December 26, 2018 · 3 min · jiezi

学会使用函数式编程的程序员(第1部分)

在这篇由多部分组成的文章中,接下来将介绍函数式编程的一些概念,这些概念对你学习函数式编程有所帮助。如果你已经懂了什么是函数式编程,这可以加深你的理解。请不要着急。从这一点开始,花点时间阅读并理解代码示例。你甚至可能想在每节课结束后停止阅读,以便让你的观点深入理解,然后再回来完成。最重要的是你要理解。纯函数(Purity)所谓纯函数,就是指这样一个函数,对于相同的输入,永远得到相同的输出,它不依赖外部环境,也不会改变外部环境。如果不满足以上几个条件那就是非纯函数。下面是Javascript中的一个纯函数示例:var z = 10;function add(x, y) { return x + y;}注意,add 函数不涉及z变量。它不从z读取,也不从z写入,它只读取x和y,然后返回它们相加的结果。这是一个纯函数。如果 add 函数确实访问了变量z,那么它就不再是纯函数了。请思考一下下面这个函数:function justTen() { return 10;}如果函数justTen是纯的,那么它只能返回一个常量, 为什么?因为我们没有给它任何参数。 而且,既然是纯函数的,除了自己的输入之外它不能访问任何东西,它唯一可以返回的就是常量。由于不带参数的纯函数不起作用,所以它们不是很有用。所以justTen被定义为一个常数会更好。大多数有用的纯函数必须至少带一个参数。考虑一下这个函数:function addNoReturn(x, y) { var z = x + y}注意这个函数是不返回任何值。它只是把变量x和y相加赋给变量z,但并没有返回。这个也是一个纯函数,因为它只处理输入。它确实对输入的变量进行操作,但是由于它不返回结果,所以它是无用的。所有有用的纯函数都必须返回一些我们期望的结果。让我们再次考虑第一个add函数:注意 add(1, 2) 的返回结果总是 3。这不是奇怪的事情,只是因为 add 函数是纯的。如果 add 函数使用了一些外部值,那么你永远无法预测它的行为。在给定相同输入的情况下,纯函数总是返回相同的结果。由于纯函数不能改变任何外部变量,所以下面的函数都不是纯函数:writeFile(fileName);updateDatabaseTable(sqlCmd); sendAjaxRequest(ajaxRequest);openSocket(ipAddress);所有这些功能都有副作用。当你调用它们时,它们会更改文件和数据库表、将数据发送到服务器或调用操作系统以获取套接字。它们不仅对输入操作同时也对输出进行操作,因此,你永远无法预测这些函数将返回什么。纯函数没有副作用。在Javascript、Java 和 c# 等命令式编程语言中,副作用无处不在。这使得调试非常困难,因为变量可以在程序的任何地方更改。所以,当你有一个错误,因为一个变量在错误的时间被更改为错误的值,这不是很好。此时,你可能会想,“我怎么可能只使用纯函数呢?”函数式编程不能消除副作用,只能限制副作用。由于程序必须与真实环境相连接,所以每个程序的某些部分肯定是不纯的。函数式编程的目标是尽量写更多的纯函数,并将其与程序的其他部分隔离开来。不可变性 (Immutability)你还记得你第一次看到下面的代码是什么时候吗?var x = 1;x = x + 1;教你初中数学的老师看到以上代码,可能会问你,你忘记我给你教的数学了吗? 因为在数学中,x 永远不能等于x + 1。但在命令式编程中,它的意思是,取x的当前值加1,然后把结果放回x中。在函数式编程中,x = x + 1是非法的。所以这里你可以用数学的逻辑还记得在数式编程中这样写是不对的!函数式编程中没有变量。由于历史原因,存储值的变量仍然被称为变量,但它们是常量,也就是说,一旦x取值,这个常量就是x返回的值。别担心,x 通常是一个局部变量,所以它的生命周期通常很短。但只要它还没被销毁,它的值就永远不会改变。下面是Elm中的常量变量示例,Elm是一种用于Web开发的纯函数式编程语言:addOneToSum y z = let x = 1 in x + y + z如果你不熟悉ml风格的语法,让我解释一下。addOneToSum 是一个函数,有两个参数分别为y和z。在let块中,x被绑定到1的值上,也就是说,它在函数的生命周期内都等于1。当函数退出时,它的生命周期结束,或者更准确地说,当let块被求值时,它的生命周期就结束了。在in块中,计算可以包含在let块中定义的值,即 x,返回计算结果 x + y + z,或者更准确地说,返回 1 + y + z,因为 x = 1。你可能又会想 :“我怎么能在没有变量的情况下做任何事情呢?”我们想一下什么时候需要修改变量。通常会想到两种情况:多值更改(例如修改或记录对象中的单个值)和单值更改(例如循环计数器)。函数式编程使用参数保存状态,最好的例子就是递归。是的,是没有循环。“什么没有变量,现在又没有循环? ”我讨厌你! ! !”哈哈,这并不是说我们不能做循环,只是没有特定的循环结构,比如for, while, do, repeat等等。函数式编程使用递归进行循环。这里有两种方法可以在Javascript中执行循环:注意,递归是一种函数式方法,它通过使用一个结束条件 start (start + 1) 和调用自己 accumulator (acc + start) 来实现与 for 循环相同的功能。它不会修改旧的值。相反,它使用从旧值计算的新值。不幸的是,这在 Javascript中 很难想懂,需要你花点时间研究它,原因有二。第一,Javascript的语法相对其它高级语言比较乱,其次,你可能还不习惯递归思维。在Elm,它更容易阅读,如下:sumRange start end acc = if start > end then acc else sumRange (start + 1) end (acc + start) 它是这样运行的:你可能认为 for 循环更容易理解。虽然这是有争议的,而且更可能是一个熟悉的问题,但非递归循环需要可变性,这是不好的。在这里,我还没有完全解释不变性的好处,但是请查看全局可变状态部分,即为什么程序员需要限制来了解更多。我还没有完全解释不可变性(Immutability)在这里的好处,但请查看 为什么程序员需要限制的全局可变状态部分 以了解更多信息。不可变性的好处是,你读取访问程序中的某个值,但只有读权限的,这意味着不用害怕其他人更改该值使自己读取到的值是错误。不可变性的还有一个好处是,如果你的程序是多线程的,那么就没有其他线程可以更改你线程中的值,因为该值是不可变,所以另一个线程想要更改它,它只能从旧线程创建一个新值。不变性可以创建更简单、更安全的代码。重构让我们考虑一下重构,下面是一些Javascript代码:我们以前可能都写过这样的代码,随着时间的推移,开始意识到这两个函数实际上是相同的,函数名称,打印结果不太一样而已。我们不应该复制 validateSsn 来创建 validatePhone,而是应该创建一个函数(共同的部分),通过参数形式实现我们想要的结果。重构后的代码如下:旧代码参数中 ssn 和 phone 现在用 value 表示,正则表达式 /^\d{3}-\d{2}-\d{4}$/ and /^(\d{3})\d{3}-\d{4}$/ 由变量 regex. 表示。最后,消息“SSN”和 “电话号码” 由变量 type 表示。这个有类似的函数都可以使用这个函数来实现,这样可以保持代码的整洁和可维护性。高阶函数许多语言不支持将函数作为参数传递,有些会支持但并不容易。在函数式编程中,函数是一级公民。换句话说,函数通常是另一个函数的值。由于函数只是值,我们可以将它们作为参数传递。即使Javascript不是纯函数语言,也可以使用它进行一些功能性的操作。 所以这里将上面的两个函数重构为单个函数,方法是将验证合法性的函数作为函数 parseFunc 的参数:function validateValueWithFunc(value, parseFunc, type) { if (parseFunc(value)) console.log(‘Invalid ’ + type); else console.log(‘Valid ’ + type);}像函数 parseFunc 接收一个或多个函数作为输入的函数,称为 高阶函数。高阶函数要么接受函数作为参数,要么返回函数,要么两者兼而有之。现在可以调用高阶函数(这在Javascript中有效,因为Regex.exec在找到匹配时返回一个truthy值):validateValueWithFunc(‘123-45-6789’, /^\d{3}-\d{2}-\d{4}$/.exec, ‘SSN’);validateValueWithFunc(’(123)456-7890’, /^(\d{3})\d{3}-\d{4}$/.exec, ‘Phone’);validateValueWithFunc(‘123 Main St.’, parseAddress, ‘Address’);validateValueWithFunc(‘Joe Mama’, parseName, ‘Name’);这比有四个几乎相同的函数要好得多。但是请注意正则表达式,这里有点冗长了。简化一下:var parseSsn = /^\d{3}-\d{2}-\d{4}$/.exec;var parsePhone = /^(\d{3})\d{3}-\d{4}$/.exec;validateValueWithFunc(‘123-45-6789’, parseSsn, ‘SSN’);validateValueWithFunc(’(123)456-7890’, parsePhone, ‘Phone’);validateValueWithFunc(‘123 Main St.’, parseAddress, ‘Address’);validateValueWithFunc(‘Joe Mama’, parseName, ‘Name’);现在看起来好多了。现在,当要验证一个电话号码时,不需要复制和粘贴正则表达式了。但是假设我们有更多的正则表达式需要解析,而不仅仅是 parseSsn 和 parsePhone。每次创建正则表达式解析器时,我们都必须记住在末尾添加 .exec,这很容易被忘记。可以通过创建一个返回exec 的高阶函数来防止这种情况:function makeRegexParser(regex) { return regex.exec;}var parseSsn = makeRegexParser(/^\d{3}-\d{2}-\d{4}$/);var parsePhone = makeRegexParser(/^(\d{3})\d{3}-\d{4}$/);validateValueWithFunc(‘123-45-6789’, parseSsn, ‘SSN’);validateValueWithFunc(’(123)456-7890’, parsePhone, ‘Phone’);validateValueWithFunc(‘123 Main St.’, parseAddress, ‘Address’);validateValueWithFunc(‘Joe Mama’, parseName, ‘Name’);这里,makeRegexParser采用正则表达式并返回exec函数,该函数接受一个字符串。validateValueWithFunc 将字符串 value 传递给 parse 函数,即exec。parseSsn 和 parsePhone 实际上与以前一样,是正则表达式的 exec 函数。当然,这是一个微小的改进,但是这里给出了一个返回函数的高阶函数示例。但是,如果makeRegexParser 要复杂得多,这种更改的好处是很大的。下面是另一个返回函数的高阶函数示例:function makeAdder(constantValue) { return function adder(value) { return constantValue + value; };}函数 makeAdder,接受参数 constantValue 并返回函数 adder,这个函数返回 constantValue 与它传入参数相加结果。下面是它的用法:var add10 = makeAdder(10);console.log(add10(20)); // 打印 30console.log(add10(30)); // 打印 40console.log(add10(40)); // 打印 50我们通过将常量10传递给 makeAdder 来创建一个函数 add10, makeAdder 返回一个函数,该函数将向返回的结果都加 10。注意,即使在 makeAddr 返回之后,函数 adder 也可以访问变量 constantValue。 这里能访问到 constantValue 是因为存在闭包。闭包机制非常重要,因为如果没有它 ,返回函数的函数就不会有很大作用。所以必须了解它们是如何工作。闭包下面是一个使用闭包的函数的示例:function grandParent(g1, g2) { var g3 = 3; return function parent(p1, p2) { var p3 = 33; return function child(c1, c2) { var c3 = 333; return g1 + g2 + g3 + p1 + p2 + p3 + c1 + c2 + c3; }; };}在这个例子中,child 函数可以访问它自身的变量,函数 parent 函数可以访问它的自身变量和函数 grandParent 的变量。而函数 grandParent 只能访问自身的变量。下面是它的一个使用例子:var parentFunc = grandParent(1, 2); // returns parent()var childFunc = parentFunc(11, 22); // returns child()console.log(childFunc(111, 222)); // prints 738// 1 + 2 + 3 + 11 + 22 + 33 + 111 + 222 + 333 == 738在这里,parentFunc 保留了 parent 的作用域,因为 grandParent 返回 parent。类似地,childFunc 保留了 child 的作用域,因为 parentFunc 保留了 parent 的作用域,而 parent 的作用域 保留了 child 的作用域。当一个函数被创建时,它在创建时作用域中的所有变量在函数的生命周期内都是可访问的。一个函数只要还有对它的引用就存在。例如,只要childFunc 还引用 child 的作用域,child 的作用域就存在。闭包具体还看看之前整理的一篇文章:我从来不理解JavaScript闭包,直到有人这样向我解释它…原文:1、https://medium.com/@cscalfani…2、https://medium.com/@cscalfani…你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》! ...

December 25, 2018 · 2 min · jiezi

学会使用函数式编程的程序员(第1部分)

学习函数式编程的第一步也是最重要的一步就是知道它是什么,做什么用的。学开车我们刚开始学开车时,很吃力。当我们看到别人在做这件事的时候,看起来确实很容易。但结果比我们想象的要难。我们在父母的车里练习,直到熟悉了附近的街道,我们才敢在公路上冒险。但是经过反复的练习,我们学会了开车,最终拿到了驾照。有了执照,我们一有机会就把车开出去。每次短暂旅行,我们驾车技术变得越来越好,随之我们的信心逐渐上升。或许有那么一天我们不得不开别人的车或者我们的老车就报废了不得不买一辆新的时候。第一次驾驶一辆不同的车是什么感觉? 就像第一次开车一样吗? 第一次,一切都那么陌生。我们之前坐过车,但只是作为乘客。这一次我们掌握了主动权可以自己开车。但是当我们开第二辆车的时候,我们只是自问几个简单的问题,比如,插钥匙口去在哪个位置,灯在哪里,怎么使用转弯信号,怎么调整侧视镜等等。从那以后,一切都很顺利。但是为什么这次和第一次相比如此容易呢?那是因为新车和旧车很像。它拥有汽车所需要的所有基本的东西,而且它们几乎在同一个地方。新车与旧车有些东西的构造不同,可能还有一些额外的功能,但我们第一次开车甚至第二次都没有使用它们。 但是,最终我们还是学会我们关心的那些内容。所以学习编程语言也是类似学开车一样。第一次是最难的,但一旦你有了第一次经验,后续的就会更容易。当你刚开始学习第二门语言时,你会问这样的问题,“我如何创建一个模块? 如何搜索数组? substring 函数的参数是什么? “你相信你能学会驾驶这门新语言,因为它会让你想起你的旧语言,也许有一些新东西可以让你的生活更轻松。如果你要驾驶宇宙飞船,你不会期望你在路上的驾驶能力会对你有很大的帮助。你的第一个宇宙飞船不管你一辈子开的是一辆车还是几十辆车,想象一下你即将驾驶一艘宇宙飞船。在开始训练的时候,你会预想太空中的场景与地面不现,宇宙飞船架使方式与汽车的驾驶方式也是不同的。但是物理世界没有改变,就像你在同一个宇宙中遨游一样。这和学习函数式编程是一样的。你应该预想到代码风格与你之前所编写不太一样,思路也是有很大差异的。非函数式编程与函数式编程的思维模式大相径庭。当你学会了,你会喜欢上它以至于你可能永远不会回到原有旧思维模式。忘记你所知道的一切人们喜欢说这句话,但这有点真实。 学习函数式编程就像从头开始。有很多相似的概念,但如果你只是希望你必须重新学习所有东西,那就最好了。当你有了想要学习函数式编程的欲望,有会有学习动力,有了动力,当事情变得困难时,你就不会放弃。作为一名程序员,有很多事情是你习惯做的,但是你不能再用函数式编程来做了。就像在你的车里一样,你过去常常倒车离开车道。但是在宇宙飞船里,没有倒车。现在你可能会想,“什么? 没有反向? 我怎么能不倒车呢?”事实证明,在宇宙飞船里你不需要倒车因为它在三维空间里的推动能力。一旦你明白了这一点,你就再也不会想要倒车了。事实上,有一天,你会回想起汽车的局限性。学习函数式编程需要一段时间,所以要有耐心。因此,让我们离开命令式编程这个冰冷的世界,来温习一下函数式编程的温泉。在这篇由多部分组成的文章中,接下来将介绍函数式编程的一些概念,这些概念对你学习函数式编程有所帮助。如果你已经懂了什么是函数式编程,这可以加深你的理解。请不要着急。从这一点开始,花点时间阅读并理解代码示例。你甚至可能想在每节课结束后停止阅读,以便让你的观点深入理解,然后再回来完成。最重要的是你要理解。纯函数(Purity)所谓纯函数,就是指这样一个函数,对于相同的输入,永远得到相同的输出,它不依赖外部环境,也不会改变外部环境。如果不满足以上几个条件那就是非纯函数。下面是Javascript中的一个纯函数示例:var z = 10;function add(x, y) { return x + y;}注意,add 函数不涉及z变量。它不从z读取,也不从z写入,它只读取x和y,然后返回它们相加的结果。这是一个纯函数。如果 add 函数确实访问了变量z,那么它就不再是纯函数了。请思考一下下面这个函数:function justTen() { return 10;}如果函数justTen是纯的,那么它只能返回一个常量, 为什么?因为我们没有给它任何参数。 而且,既然是纯函数的,除了自己的输入之外它不能访问任何东西,它唯一可以返回的就是常量。由于不带参数的纯函数不起作用,所以它们不是很有用。所以justTen被定义为一个常数会更好。大多数有用的纯函数必须至少带一个参数。考虑一下这个函数:function addNoReturn(x, y) { var z = x + y}注意这个函数是不返回任何值。它只是把变量x和y相加赋给变量z,但并没有返回。这个也是一个纯函数,因为它只处理输入。它确实对输入的变量进行操作,但是由于它不返回结果,所以它是无用的。所有有用的纯函数都必须返回一些我们期望的结果。让我们再次考虑第一个add函数:注意 add(1, 2) 的返回结果总是 3。这不是奇怪的事情,只是因为 add 函数是纯的。如果 add 函数使用了一些外部值,那么你永远无法预测它的行为。在给定相同输入的情况下,纯函数总是返回相同的结果。由于纯函数不能改变任何外部变量,所以下面的函数都不是纯函数:writeFile(fileName);updateDatabaseTable(sqlCmd); sendAjaxRequest(ajaxRequest);openSocket(ipAddress);所有这些功能都有副作用。当你调用它们时,它们会更改文件和数据库表、将数据发送到服务器或调用操作系统以获取套接字。它们不仅对输入操作同时也对输出进行操作,因此,你永远无法预测这些函数将返回什么。纯函数没有副作用。在Javascript、Java 和 c# 等命令式编程语言中,副作用无处不在。这使得调试非常困难,因为变量可以在程序的任何地方更改。所以,当你有一个错误,因为一个变量在错误的时间被更改为错误的值,这不是很好。此时,你可能会想,“我怎么可能只使用纯函数呢?”函数式编程不能消除副作用,只能限制副作用。由于程序必须与真实环境相连接,所以每个程序的某些部分肯定是不纯的。函数式编程的目标是尽量写更多的纯函数,并将其与程序的其他部分隔离开来。不可变性 (Immutability)你还记得你第一次看到下面的代码是什么时候吗?var x = 1;x = x + 1;教你初中数学的老师看到以上代码,可能会问你,你忘记我给你教的数学了吗? 因为在数学中,x 永远不能等于x + 1。但在命令式编程中,它的意思是,取x的当前值加1,然后把结果放回x中。在函数式编程中,x = x + 1是非法的。所以这里你可以用数学的逻辑还记得在数式编程中这样写是不对的!函数式编程中没有变量。由于历史原因,存储值的变量仍然被称为变量,但它们是常量,也就是说,一旦x取值,这个常量就是x返回的值。别担心,x 通常是一个局部变量,所以它的生命周期通常很短。但只要它还没被销毁,它的值就永远不会改变。下面是Elm中的常量变量示例,Elm是一种用于Web开发的纯函数式编程语言:addOneToSum y z = let x = 1 in x + y + z如果你不熟悉ml风格的语法,让我解释一下。addOneToSum 是一个函数,有两个参数分别为y和z。在let块中,x被绑定到1的值上,也就是说,它在函数的生命周期内都等于1。当函数退出时,它的生命周期结束,或者更准确地说,当let块被求值时,它的生命周期就结束了。在in块中,计算可以包含在let块中定义的值,即 x,返回计算结果 x + y + z,或者更准确地说,返回 1 + y + z,因为 x = 1。你可能又会想 :“我怎么能在没有变量的情况下做任何事情呢?”我们想一下什么时候需要修改变量。通常会想到两种情况:多值更改(例如修改或记录对象中的单个值)和单值更改(例如循环计数器)。函数式编程使用参数保存状态,最好的例子就是递归。是的,是没有循环。“什么没有变量,现在又没有循环? ”我讨厌你! ! !”哈哈,这并不是说我们不能做循环,只是没有特定的循环结构,比如for, while, do, repeat等等。函数式编程使用递归进行循环。这里有两种方法可以在Javascript中执行循环:注意,递归是一种函数式方法,它通过使用一个结束条件 start (start + 1) 和调用自己 accumulator (acc + start) 来实现与 for 循环相同的功能。它不会修改旧的值。相反,它使用从旧值计算的新值。不幸的是,这在 Javascript中 很难想懂,需要你花点时间研究它,原因有二。第一,Javascript的语法相对其它高级语言比较乱,其次,你可能还不习惯递归思维。在Elm,它更容易阅读,如下:sumRange start end acc = if start > end then acc else sumRange (start + 1) end (acc + start) 它是这样运行的:你可能认为 for 循环更容易理解。虽然这是有争议的,而且更可能是一个熟悉的问题,但非递归循环需要可变性,这是不好的。在这里,我还没有完全解释不变性的好处,但是请查看全局可变状态部分,即为什么程序员需要限制来了解更多。我还没有完全解释不可变性(Immutability)在这里的好处,但请查看 为什么程序员需要限制的全局可变状态部分 以了解更多信息。不可变性的好处是,你读取访问程序中的某个值,但只有读权限的,这意味着不用害怕其他人更改该值使自己读取到的值是错误。不可变性的还有一个好处是,如果你的程序是多线程的,那么就没有其他线程可以更改你线程中的值,因为该值是不可变,所以另一个线程想要更改它,它只能从旧线程创建一个新值。不变性可以创建更简单、更安全的代码。在本文的后续部分中,将讨论高阶函数、函数组合、局部套用等等,尽请期待!原文:https://medium.com/@cscalfani…编辑中可能存在的bug没法实时知道,事后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具Fundebug。你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》! ...

December 24, 2018 · 1 min · jiezi

异构列表(DslAdapter开发日志)

异构列表(DslAdapter开发日志)函数范式, 或者说Haskell的终极追求是尽量将错误"扼杀"在编译期, 使用了大量的手法和技术: 使用大量不可变扼杀异步的不可预计, 以及静态类型和高阶类型 说到静态类型大家应该都不会陌生, 它是程序正确性的强大保证, 这也是本人为什么一直不太喜欢Python, js等动态类型语言的原因静态类型: 编译时即知道每一个变量的类型,因此,若存在类型错误编译是无法通过的。 动态类型: 编译时不知道每一个变量的类型,因此,若存在类型错误会在运行时发生错误。类型检查, 即在编译期通过对类型进行检查的方式过滤程序的错误, 这是我们在使用Java和Kotlin等语言时常用的技术, 但这种技术是有限的, 它并不能通用于所有情况, 因此我们常常反而会回到动态类型, 采用动态类型的方式处理某些问题 本文聚焦于常见的列表容器在某些情况下如何用静态类型的手法进行开发进行讨论编译期错误检查对于函数(方法)的输入错误有两种方式:编译期检查, 比如List<String>中不能保存Integer类型的数据运行期检查, 比如对于列表的下标是否正确, 我们可以在运行的时候检查运行期检查是必须要运行到相应的代码时才会进行相应的检查(无论是实际程序还是测试代码), 这是不安全并且效率低下的, 所以能在编译期检查的问题都尽量在编译期排除掉 编译期的检查中除了语法问题之外最重要的就是类型检查, 但这要求我们提供足够的类型信息DslAdapter实现中遇到的问题DslAdapter是个人开发的一个针对Android RecyclerView的一个扩展库, 专注于静态类型和Dsl的手法, 希望创造一个基于组合子的灵活易用同时又非常安全的Adapter 在早期版本中已经实现了通过Dsl进行混合Adapter的创建:val adapter = RendererAdapter.multipleBuild() .add(layout<Unit>(R.layout.list_header)) .add(none<List<Option<ItemModel>>>(), optionRenderer( noneItemRenderer = LayoutRenderer.dataBindingItem<Unit, ItemLayoutBinding>( count = 5, layout = R.layout.item_layout, bindBinding = { ItemLayoutBinding.bind(it) }, binder = { bind, item, _ -> bind.content = “this is empty item” }, recycleFun = { it.model = null; it.content = null; it.click = null }), itemRenderer = LayoutRenderer.dataBindingItem<Option<ItemModel>, ItemLayoutBinding>( count = 5, layout = R.layout.item_layout, bindBinding = { ItemLayoutBinding.bind(it) }, binder = { bind, item, _ -> bind.content = “this is some item” }, recycleFun = { it.model = null; it.content = null; it.click = null }) .forList() )) .add(provideData(index).let { HListK.singleId(it).putF(it) }, ComposeRenderer.startBuild .add(LayoutRenderer<ItemModel>(layout = R.layout.simple_item, stableIdForItem = { item, index -> item.id }, binder = { view, itemModel, index -> view.findViewById<TextView>(R.id.simple_text_view).text = itemModel.title }, recycleFun = { view -> view.findViewById<TextView>(R.id.simple_text_view).text = "" }) .forList({ i, index -> index })) .add(databindingOf<ItemModel>(R.layout.item_layout) .onRecycle(CLEAR_ALL) .itemId(BR.model) .itemId(BR.content, { m -> m.content + “xxxx” }) .stableIdForItem { it.id } .forList()) .build()) .add(DateFormat.getInstance().format(Date()), databindingOf<String>(R.layout.list_footer) .itemId(BR.text) .forItem()) .build()以上代码实现了一个混合Adapter的创建:|–LayoutRenderer header||–SealedItemRenderer| |–none -> LayoutRenderer placeholder count 5| | | |–some -> ListRenderer| |–DataBindingRenderer 1| |–DataBindingRenderer 2| |–… ||–ComposeRenderer| |–ListRenderer| | |–LayoutRenderer simple item1| | |–LayoutRenderer simple item2| | |–…| || |–ListRenderer| |–DataBindingRenderer item with content1| |–DataBindingRenderer item with content2| |–…||–DataBindingRenderer footer即: Build Dsl –> Adapter, 最后生成了一个混合的val adapter而在使用的时候希望能通过这个val adapter对结构中某些部分进行部分更新 比如上面构造的结构中, 我们希望只在ComposeRenderer中第二个ListRendererinsert 一个元素进去, 并合理调用Adapter的notifyItemRangeInserted(position, count)方法, 并且希望这个操作可以通过Dsl的方式实现, 比如:adapter.updateNow { // 定位ComposeRenderer getLast2().up { // 定位第二个ListRenderer getLast1().up { insert(2, listOf(ItemModel(189, “Subs Title1”, “subs Content1”))) } }}以上Dsl必然是希望有一定的限定的, 比如不能在只有两个元素的Adapter中getLast3(), 也不能在非列表中执行insert() 而这些限制需要被从val adapter推出, 即adapter –> Update Dsl, 这意味着adapter中需要保存其结构的所有信息, 由于我们需要在编译期对结构信息进行提取, 也意味着应该在类型信息中保存所有的结构信息 对于通常的Renderer没有太大的问题, 但对于部分组合其他Renderer的Renderer, (比如ComposeRenderer, 它的作用是按顺序将任意的Renderer组合在一起), 通常的实现方式是将他们统统还原为共通父类(BaseRenderer), 然后看做同样的东西进行操作, 但这个还原操作也同时将各自独特的类型信息给丢失了, 那应该怎么办才能即保证组合的多样性, 同时又不会丢失各自的类型信息?换一种方式描述问题推广到其他领域, 这个问题实际挺常见的, 比如:我们现在有一个用于绘制的基类RenderableBase, 而有两个实现, 一个是绘制圆形的Circle和绘制矩形的Rectangle:graph TBA[RenderableBase]A1[Circle]A2[Rectangle]A –> A1A –> A2我们有一个共通的用于绘制的类Canvas, 保存有所有需要绘制的RenderableBase, 一般情况下我们会通过一个List<RenderableBase>容器的方式保存它们, 将它们还原为通用的父类 但这种方式的问题是这种容器的类型信息中已经丢失了每个元素各自的特征信息, 我们没法在编译期知道或者限定子元素的类型(比如我们并不知道其中有多少个Circle, 也不能限定第一个元素必须为Rectangle) 那是否有办法即保证容器的多样性, 同时又不会丢失各自的类型信息?再换一种方式描述问题对于一个函数(方法), 比如:fun test(s: String): List<String>它其实可以看做声明了两个部分的函数:值函数: 描述了元素s到列表list的态射类型函数: 描述了从类型String到类型List<String>的态射即包括s -> list和String -> List<String> 一般而言这两者是同步的, 或者说类型信息中包括了足够的值相关的信息(值的类型), 但请注意以下函数:fun test2(s: String, i: Int): List<Any?> = listOf(s, i)它声明了(s, i) -> list和(String, Int) -> List<Any?>, 它没有将足够的类型信息保存下来:List中只包括String和Int两种元素List的Size为2List中第一个元素是String, 第二个元素是Int那是否有办法将以上这些信息也合理的保存到容器的类型中呢?一种解决方案异构列表以上的问题注意原因是在于List容器本身, 它本身就是一个保存相同元素的容器, 而我们需要是一个可以保存不同元素的容器 Haskell中有一种这种类型的容器: Heterogeneous List(异构列表), 就实现上来说很简单:Tip: arrow中的实现sealed class HListdata class HCons<out H, out T : HList>(val head: H, val tail: T) : HList()object HNil : HList()我们来看看使用它来构造上一节我们所说的函数应该如何构造:// 原函数fun test2(s: String, i: Int): List<Any?> = listOf(s, i)// 异构列表fun test2(s: String, i: Int): HCons<Int, HCons<String, HNil>> = HCons(i, HCons(s, HNil))同样是构建列表, 异构列表包含了更丰富的类型信息:容器的size为2容器中第一个元素为String, 第二个为Int相比传统列表异构列表的优势完整保存所有元素的类型信息自带容器的size信息完整保存每个元素的位置信息比如, 我们可以限定只能传入一个保存两个元素的列表, 其中第一个元素是String, 第二个是Int:fun test(l: HCons<Int, HCons<String, HNil>>)同时我们也可以确定第几个元素是什么类型:val l: HCons<Int, HCons<String, HNil>> = …l.get0() // 此元素一定是Int类型的由于Size信息被固定了, 传统必须在运行期才能检查的下标是否越界的问题也可以在编译期被检查出来:val l: HCons<Int, HCons<String, HNil>> = …l.get3() // 编译错误, 因为只有两个元素 相比传统列表的难点由于Size信息和元素类型信息是绑定的, 抛弃Size信息的同时就会抛弃元素类型的限制注意类型信息中的元素信息和实际保存的元素顺序是相反的, 因为异构列表是一个FILO(先进后出)的列表由于Size信息是限定的, 针对不同Size的列表的处理需要分开编写对于第一点, 以上面的RenderableBase为例, 比如我们有一个函数可以处理任意Size的异构列表:fun <L : HList> test(l: L)我们反而无法限定每个元素都应该是继承自RenderableBase的, 这意味着HCons<Int, HCons<String, HNil>>这种列表也可以传进来, 这在某些情况下是很麻烦的异构列表中附加高阶类型的处理Tip: 关于高阶类型的内容可以参考这篇文章高阶类型带来了什么继承是OOP的一大难点, 它的缺点在程序抽象度越来越高的过程的越来越凸显. 函数范式中是以组合代替继承, 使得程序有着更强的灵活性由于采用函数范式, 我们不再讨论异构列表如何限定父类, 而是改为讨论异构列表如何限定高阶类型对HList稍作修改即可附加高阶类型的支持:Tip: DslAdapter中的详细实现: HListKsealed class HListK<F, A: HListK<F, A>>class HNilK<F> : HListK<F, HNilK<F>>()data class HConsK<F, E, L: HListK<F, L>>(val head: Kind<F, E>, val tail: L) : HListK<F, HConsK<F, E, L>>()以Option(可选类型)为例:arrow中的详细实现: Optionsealed class Option<out A> : arrow.Kind<ForOption, A>object None : Option<Nothing>()data class Some<out T>(val t: T) : Option<T>()通过修改后的HListK我们可以限定每个元素都是Option, 但并不限定Option内容的类型:// [Option<Int>, Option<String>]val l: HConsK<ForOption, String, HConsK<ForOption, Int, HNilK<ForOption>>> = HConsK(Some(“string”), HConsK(199, HNilK()))修改后的列表即可做到即保留每个元素的类型信息又可以对元素类型进行部分限定它即等价于原生的HList, 同时又有更丰富的功能比如:// 1. 定义一个单位类型data class Id<T>(val a: T) : arrow.Kind<ForId, A>// 类型HListK<ForId, L>即等同于原始的HListfun <L : HListK<ForId, L>> test()// 2. 定义一个特殊类型data class FakeType<T, K : T>(val a: K) : arrow.Kind2<ForFakeType, T, K>// 即可限定列表中每个元素必须继承自RenderableBasefun <L : HListK<Kind<ForFakeType, RenderableBase>, L>> test(l: L) = …fun test2() { val t = FakeType<RenderableBase, Circle>(Circle()) val l = HListK.single(t) test(l)}回到DslAdapter的实现上文中提到的异构列表已经足够我们用来解决文章开头的DslAdapter实现问题了 异构问题解决起来就非常顺理成章了, 以ComposeRenderer为例, 我们使用将子Renderer装入ComposeItem容器的方式限定传入的容器每个元素必须是BaseRenderer的实现, 同时ComposeItem通过泛型的方式尽最大可能保留Renderer的类型信息:data class ComposeItem<T, VD : ViewData<T>, UP : Updatable<T, VD>, BR : BaseRenderer<T, VD, UP>>( val renderer: BR) : Kind<ForComposeItem, Pair<T, BR>>其中可以注意到类型声明中的Kind<ForComposeItem, Pair<T, BR>>, arrow默认的三元高阶类型为Kind<Kind<ForComposeItem, T>, BR>, 这并不符合我们在这里对高阶类型的期望: 我们这里只想限制ForComposeItem, 而T我们希望和BR绑定在一起限定, 所以使用了积类型 Pair将T和BR两个类型绑定到了一起. 换句话说, Pair在这里只起到一个组合类型T和BR的类型粘合剂的作用, 实际并不会被使用到 ComposeItem保存的是在build之后不会改变的数据(比如Renderer), 而使用中会改变的数据以ViewData的形式保存在ComposeItemData:data class ComposeItemData<T, VD : ViewData<T>, UP : Updatable<T, VD>, BR : BaseRenderer<T, VD, UP>>( val viewData: VD, val item: ComposeItem<T, VD, UP, BR>) : Kind<ForComposeItemData, Pair<T, BR>>这里同样使用了Pair作为类型粘结剂的技巧 对于一个ComposeRenderer而言应该保存以下信息:可以渲染的数据类型子Renderer的所有类型信息当前Renderer的ViewData信息以及子Renderer的ViewData信息其中2. 子Renderer的所有类型信息由IL : HListK<ForComposeItem, IL>泛型信息保存3. 当前Renderer的ViewData信息以及子Renderer的ViewData信息由VDL : HListK<ForComposeItemData, VDL>泛型信息保存而1. 可以渲染的数据类型由DL : HListK<ForIdT, DL>(ForIdT等同于上文提到的单位类型Id)于是我们可以得到ComposeRenderer的类型声明:class ComposeRenderer<DL : HListK<ForIdT, DL>, IL : HListK<ForComposeItem, IL>, VDL : HListK<ForComposeItemData, VDL>>子Renderer的所有类型信息(Size, 下标等等)被完整保留, 也就意味着从类型信息我们可以还原出每个子Renderer的完整类型信息一个栗子:构造两个子Renderer:// LayoutRendererval stringRenderer = LayoutRenderer<String>(layout = R.layout.simple_item, count = 3, binder = { view, title, index -> view.findViewById<TextView>(R.id.simple_text_view).text = title + index }, recycleFun = { view -> view.findViewById<TextView>(R.id.simple_text_view).text = "" }) // DataBindingRendererval itemRenderer = databindingOf<ItemModel>(R.layout.item_layout) .onRecycle(CLEAR_ALL) .itemId(BR.model) .itemId(BR.content, { m -> m.content + “xxxx” }) .stableIdForItem { it.id } .forItem()使用ComposeRenderer组合两个Renderer:val composeRenderer = ComposeRenderer.startBuild .add(itemRenderer) .add(stringRenderer) .build()你可以猜出这里composeRenderer的类型是什么吗?答案是:ComposeRenderer< HConsK<ForIdT, String, HConsK<ForIdT, ItemModel, HNilK<ForIdT>>>, HConsK<ForComposeItem, Pair<String, LayoutRenderer<String>>, HConsK<ForComposeItem, Pair<ItemModel, DataBindingRenderer<ItemModel, ItemModel>>, HNilK<ForComposeItem>>>, HConsK<ForComposeItemData, Pair<String, LayoutRenderer<String>>, HConsK<ForComposeItemData, Pair<ItemModel, DataBindingRenderer<ItemModel, ItemModel>>, HNilK<ForComposeItemData>>>>其中完整保留了所有我们需要的类型信息, 因此我们可以通过composeRenderer还原出原来的数据结构:composeRenderer.updater .updateBy { getLast1().up { update(“New String”) } }这里的update(“New String”)方法知道当前定位的是一个stringRenderer, 所以可以使用String更新数据, 如果传入ItemModel就会出错虽然泛型信息非常多而长, 但实际大部分可以通过编译系统自动推测出来, 而对于某些无法被推测的部分也可以通过一些小技巧来简化, 你可以猜到用了什么技巧吗?结语以前我们常常更聚焦于面向过程编程, 但对函数范式或者说Haskell的学习, 类型编程其实也是一个很有趣并且很有用的思考方向 没错, 类型是有相应的计算规则的, 甚至有的编程语言会将类型作为一等对象, 可以进行相互计算(积类型, 和类型, 类型的幂等) 虽然Java或者Kotlin的类型系统并没有如此的强大, 但只要改变一下思想, 通过一些技巧还是可以实现很多像魔法一样的事情(比如另一篇文章中对高阶类型的实现)将Haskell的对类型系统编程应用到Kotlin上有很多有趣的技巧, DslAdapter只是在实用领域上一点小小的探索, 而fpinkotlin则是在实验领域的另外一些探索成果(尤其是第四部分 15.流式处理与增量I/O), 希望之后能有机会分享更多的一些技巧和经验, 也欢迎感兴趣的朋友一同探讨 ...

December 24, 2018 · 4 min · jiezi

有趣的 DApp 设计模式:First-class Asset

本篇文章的作者是 Jan,文章阐述了 Cell 模型中支持的一种非常有趣的 DApp 设计模式:First-class Asset,它让加密资产变成区块链中的「一等公民」。喜欢函数式编程的工程师应该很熟悉一个名词:First-class Function,翻译成中文应该叫「头等函数」或者「一等函数」。First-class Function 指的是一类编程语言,在这些语言中函数是一个完全独立的概念:函数可以被当作值赋给一个变量,可以被当作参数传递给其他函数,也可以被当作返回值从其它函数传出来。在这样的语言中我们可以像操纵数据一样操纵函数,所以在这些语言中函数和数据一样是「一等公民」(First-class Citizen)。First-class Function 是函数式语言的一个关键特性,很多函数式编程的强大能力来源于此。Nervos CKB 使用 Cell 模型来构建整个共同知识库的状态。Cell 模型是一个非常简单但是与现有区块链设计非常不同的状态模型,我们在设计 Cell 模型的时候已经意识到,基于 Cell 模型的 DApp 将拥有一些非常不同的性质,就像函数式编程和面向对象编程会产生风格迥异的设计模式和程序特性一样。在这篇文章中,我想阐述 Cell 模型可以支持的一种非常有趣的 DApp 设计模式,我们把它叫做 First-class Asset,因为通过它我们可以将用户自定义的加密资产变成区块链中的「一等公民」。状态模型的快速入门在 Cell 模型之前,各种区块链使用的状态模型基本上就是两种:UTXO 模型和 Account 模型。使用 UTXO 模型的代表是比特币。UTXO 是未被花费的交易输出(Unspent Transaction Output)的缩写,一个 UTXO 可以简单的理解为是一个比特币,然而和一般的硬币不同,每一个 UTXO 的面值都是不一样的。每个 UTXO 中都通过一段锁脚本(Lock Script)记录了这枚硬币的所有者是谁,同时保证只有所有者能够花费这枚硬币。每一个比特币全节点都会维护当前所有 UTXO 的集合,这个集合我们就称为比特币账本的当前状态(即当前的账本)。每一次比特币转账都是一个从 UTXO 集合中删除几个硬币(属于付款方)然后又增加几个新硬币(属于收款方和 / 或付款方)的过程。由于整个账本状态是基于 UTXO 这个最小单元构建的,我们把它叫做 UTXO 模型。使用 Account 模型的代表是以太坊。Account 就是账户,和银行账户类似,代表了资产的所有者,账户里面最重要的数据是余额(Balance),记录这个账户持有的以太币的数量。账户是资产所有者的代表,所有者可以是人(对应外部账户)或者智能合约(对应合约账户),外部账户通过私钥签名来验证资产所有权,合约账户的所有权通过合约代码来确定,合约代码和状态都保存合约账户内部。外部账户要转账的时候,用户在交易中指明转账数量,账本中的付款方账户余额和收款方账户余额就会做相应的减少和增加。由于整个账本状态是基于账户(Account)这个最小单元构建的,我们把它叫做 Account 模型。First-class CoinUTXO 模型和 Account 模型代表了构建账本状态的两种思路。账本是所有者与资产之间关系的集合。UTXO 模型以资产为基础建模,先构建出「硬币」的概念,再给硬币赋予所有者的属性;Account 模型以所有者为基础建模,先构建出「账户」的概念,再给账户赋予余额的属性。以哪种方式作为基础模型决定了系统中的操作的基本对象是资产还是账户(所有者)。所以我们说,硬币(Coin)是 UTXO 模型中的 First-class Citizen,每一个 UTXO 都是一个具有独立标识符的对象(Transaction ID + Output Index),Coin 是用户直接操作的对象(用户在构造的交易中包含 UTXO),账户是基于 Coin 建立的上层概念(只存在于钱包中)。因此 UTXO 是 First-class Coin。在 Account 模型中,账户是 First-class Citizen,聚合在账户余额中的硬币没有独立的标识符。账户是用户直接操作的对象,资产的转移是由账户作为用户的代理实现的,这一点在接受方是合约账户时体现的最为明显。在这样的模型下,用户定义加密资产(例如 ERC 20)更像是通过第三方记账的方式,而非点对点的方式转移,这个差异会将第三方(这里的第三方指的是托管加密资产的智能合约)引入资产转移流程,增加智能合约的设计复杂度(我们可以把智能合约看作在资产转移时会自动执行的逻辑)。为了降低这种复杂度,Account 模型中的交易需要加入特殊的逻辑(Value 字段),但是这样的特殊逻辑只有助于原生资产,同时造成对原生资产和用户自定义资产的不同代码路径。对于这些问题,Kelvin Fitcher 写过一篇 Looking at ownership in the EVM 进行了很好的分析,在此不再赘述。有了这些背景,我们应该更容易理解 CKB 的这一设计理念了:有了 Cell 模型,我们能够简化设计,并在 Nervos CKB 上实现作为「一等公民」的用户定义资产(User Defined Assets),简称 First-class Assets.First-class Assets 与 UTXO 一样,具有独立标识符,可以被用户及脚本直接引用和操作。First-class State如何实现 First-class Assets 呢?无论用何种方式,我们都需要记录所有者和资产之间的关系。这些关系记录,本质上是经过共识的状态。要有 First-class Assets, 必须先有First-class State,而这正是 Cell 模型的出发点。Nervos CKB 的名字来自于 Common Knowledge Base(共同知识库)的缩写。我们之所以把 Nervos 网络中的区块链称为「共同知识库」,是因为它的责任是持续不断的对网络的共同状态形成全球共识,换句话说,CKB 是一个由全球共识维护的状态库。一个状态库的基本模型,很自然的是将整个状态划分为更小的状态单元组织起来。这些更小的状态单元,就是 Cell。由于 Cell 是一种状态单元,有独立的标识符(Transaction ID + Cell Output Index),可以被直接引用,作为参数传递给脚本,它是 CKB 中的「一等公民」,也就是说状态是 CKB 中的「一等公民」。Cell 不仅仅是一种 First-class State,而且是最简单的一种 First-class State:一个 Cell 中只有 Capacity,Data,Lock 以及 Contract(可选,Contract 可以是一段代码或者指向一个 Code Cell 的 Reference)四个字段。如下图所示,Cell 的所有者可以直接更新 Cell 中保存的状态,不需要经过任何中间方,而在 Account 模型中用户只能通过合约代码(账户中的 Code)来操作账户内的状态,状态实际上是托管在合约手中的。值得指出的是,有了 Cell,CKB 实际上就获得了一种有状态的编程模型。一种普遍的观点是,以太坊编程模型的表达能力来自图灵完备的虚拟机,实际上通过账户使得智能合约能够保存计算状态是一个大过 EVM的优点(图灵不完备的语言也有很强大的表达能力:https://en.wikipedia.org/wiki…)。CKB 通过 Cell 和 CKB-VM(Simple Yet Powerful! 这得另外写一篇文章了)的组合实现了一种新的有状态的智能合约编程模型。这个编程模型更加适合 Layer 2,因为通过分析 Layer 2 协议的共同模式我们可以看到,协议层之间的交互对象应该是状态对象(State Transaction)而不是事件对象(Event Transaction),Layer 1 应该是一个状态层而不是计算层。CKB 编程模型的另一个特点是,不区分数据(状态)和代码。这句话的意思是,与 Account 模型不同,合约的状态和代码都可以储存在 Cell 的 Data 字段中,保存代码的 Cell 可以被其它 Cell 引用(因为它们是First-class State!),合约的状态和代码不需要绑定在一起,存放在一个地方。开发者可以通过一条简单的指令把代码 Cell 或者数据 Cell 的内容载入运行时内存,然后根据需要自行将其解释为代码执行或者数据来读写。有了这些底层支持,我们就可以将一个合约的代码和状态分开保存在不同的地方:Code Cell 的 Code(Data)字段存放代码,而 State Cell 的 State(Data)的字段则保存状态;在 State Cell 中通过 Contract ref 引用 Code Cell 来建立对自身保存的 State 的业务逻辑约束,通过 Lock ref 引用另外一个 Code Cell 来表达 State Cell 的所有权。每一个 State Cell 可以属于不同的用户,因此在 Cell 模型下独立的用户状态是非常容易实现的模式(在 Account 模型下,合约状态往往由多个用户状态混合构成,例如在一个 ERC 20 合约中,Alice 和 Bob 的 Token 余额都记录在同一个合约的内部状态里面)。如果想对 CKB-VM 上的合约编写有更多了解,请看这两篇文章:Hello CKB!An Introduction to Nervos CKB-VM有了这样一种编程模型,我们就能构造 First-class Asset 了。First-class AssetCKB 中的用户定义资产(User Defined Asset)可以这样来构造:设计资产定义合约(Asset Definition),规定资产的主要约束(例如总数量,发行者,交易前后数量不变等);保存合约代码到 Asset Definition Cell 中;在满足发行权限的情况下,发行者发行资产,并将资产状态保存在另外的 State Cell中。State Cell 的 Contract 字段引用保存了资产定义的 Code Cell,保证 State Cell 的变化受到资产定义的约束;Asset Cell 的持有者可以通过更新 Lock 来改变 Asset Cell 的所有者。可以看到,在这样的设计中,用户定义的资产是作为独立对象存在于系统中的,每一份资产都是一个 Cell,每一份资产都拥有自己的标识符。我们完全可以认为 Asset Cell 是 UTXO 的通用化版本。这样的 First-class Asset 有如下优点:Asset Cell 可以被引用,可以直接作为其它合约的参数传入。只要引用 Asset Cell 的 Input 有正确的用户授权,合约就可以正常的使用用户的 Asset Cell;资产定义与资产状态分离。Asset Definition Cell 的所有者是资产的发行者,而 Asset Cell 是属于每个用户的。Asset Cell 的授权逻辑和业务逻辑分离,所有权完全由自己的 Lock 决定,与 Asset Definition 的逻辑无关,这意味着 First-class Asset 不是托管在资产发行者、开发者或是资产定义合约的手中,而是真正完全属于用户的;用户的资产相互隔离,用户资产状态独立。CKB 的经济模型关注状态存储激励问题:用户在区块链上保存状态不仅需要支付写入费用,而且应该承担与存储时间成正比的存储成本。如果用户的资产状态混合在一个地方保存(例如 ERC 20),这些状态的存储成本有谁来支付将是一个问题。(CKB Economics Paper 正在努力写作中…);只要 Asset Definition Cell 的 Lock 逻辑允许,资产定义可以独立更新。上面的示意图只是在 CKB 上实现 First-class Asset 的一种方式。除了上面讨论的方面,还有一些有趣的细节,例如,Asset Definition Cell 是不是可以有属于自己的状态?Asset Definition Cell 以及 Asset Cell 的 Capacity 应该由谁来提供?对于这些问题,我们已经有了一些非常漂亮的想法。这些细节的设计、讨论和实现是我们现在正在进行的工作。SummaryCell 模型是一个高度抽象的模型,事实上,你不仅可以在 Cell 上实现First-class Asset,也可以在 Cell 上模拟 Account。通过这篇文章的介绍我们可以看出,Cell 模型是一个不同于 UTXO 模型和 Account 模型的新设计。除了状态模型的不同,CKB 还将计算(也就是状态生成)转移到了链外,在链上只需要对状态进行验证的逻辑。独特的状态模型和计算验证分离这两点决定了 CKB 的编程模型上必然会出现新的 DApp 范式和设计模式。从 CKB 白皮书完成到现在将近一年的时间中,我们看到越来越多的人开始关注和讨论 First-class State 和 First-class Asset 这两种新的思路(虽然大家用的名词各自都不一样),这些进展让我们非常兴奋。如果你有兴趣对 First-class State 和 First-class Asset 进行更多的探讨,或是在CKB的编程模型上有什么有趣的想法,欢迎联系我们讨论 ~CKB 的代码已经完全开源,这篇文章介绍的内容在代码中都已经实现。欢迎给我们的代码提出各种意见:https://github.com/nervosnetw… (CKB 上用 Ruby 脚本编程的示例,理解 CKB 上编程模型的最佳入口)https://github.com/nervosnetw…https://github.com/nervosnetw…感谢 Ian Yang,Xuejie Xiao,Kevin Wang 在 CKB 和 Cell 模型设计中提供的帮助 ~原文作者:Jan ...

December 24, 2018 · 2 min · jiezi

【译】再见,面向对象编程(一)

Charles Scalfani原文:https://medium.com/@cscalfani…我使用面向对象语言编程已经十几年了。我是用的第一个OO语言是C++,然后是Smalltak,最后是.NET和Java。我迫切地想从面向对象的三大支柱,集成,封装和多态上得到收益。我急于从这个到我面前的新领地得到对于重用的承诺。我对于将现实对象映射到类的想法非常兴奋,并希望整件事能平滑迁移。我想太多了。继承,第一个跌落的支柱最早,继承看起来是面向对象范式的最大收益。所有对新手灌输的关于形状继承的简化例子看起来逻辑上很合理。我照单全收并且发现了新东西。香蕉猴子雨林问题我带着信仰和需要解决的问题,开始构建类继承和写代码,一切都很好。我永远不会忘记那一天,当我打算开始从一个现有类使用继承来重用的时候,这是我一直在等待的时刻。一个新项目来了,我想到在我上一个工程里的那个类。没问题,重用来搞定。我从老工程里找到那个类并拷过来使用。但是。。。 不只是那个类。我需要父类。但。。 但先这样。啊。。。等下。。。看起来我们需要这个父类的父类。。。然后。。。 我们需要所有父类。好吧。。好吧。。我来解决这个。没问题。我去。现在不能编译。为什么?哦,我知道了。。。 这个对象包含了其他对象。所以我也需要那些。 没问题。等等。。。我不只是需要那个对象。我需要对象的父类和他父类的父类,然后每个包含的对象和他们所有的父类。。。晕。Joe Armstrong,Erlang之父曾说过:面向对象语言的问题是他们隐式的包含了他们周围的环境。你需要一个香蕉但是你得到的是一个拿着香蕉的大猩猩和整个雨林。香蕉猴子雨林解决方案我可以通过不写太深的继承来解决这个问题。但复用的关键就是继承,任何我对这个机制上的限制都直接限制了重用,是吧?是的。所以可怜的面向对象程序员,who’s had a healthy helping of the Kool-aid, to do?组合和委托,后面说这个。钻石问题以下问题迟早会遇到,取决于使用的语言。大部分OO语言不支持这个,尽管这个看起来符合逻辑。让OO语言支持这个有多难?想象下以下伪代码:Class PoweredDevice {}Class Scanner inherits from PoweredDevice { function start() { }}Class Printer inherits from PoweredDevice { function start() { }}Class Copier inherits from Scanner, Printer {}注意Scanner类和Printer类都实现了一个start功能。所以Copier累继承了哪一个start功能?Scanner?还是Printer?不可能两个都实现。钻石问题的解决方案方案很简单。不要这么做。是的。大部分OO语言不让你这么做。但是,如果我的建模就是这样呢?我需要我的重用!那么你必须使用组合和委托。Class PoweredDevice {}Class Scanner inherits from PoweredDevice { function start() { }}Class Printer inherits from PoweredDevice { function start() { }}Class Copier { Scanner scanner Printer printer function start() { printer.start() }}注意现在Copier类包含了Printer和Scanner的实例。他将start功能委托给Printer类的实现。他也可以简单委托给Scanner。这个问题也让继承范式开始出现问题。脆弱的基类问题所以现在我保证我的继承关系比较扁平,并不会出现环状引用。没有钻石问题。现在一切正常,直到。。。一天,我的代码运行正常,但后一天就不工作了。我没有变更我的代码。那么,这可能是个bug。。。 但等下。。。 有些东西确实变了。。。但那个变动不在我的代码里。这个变动是在我继承的类里面。为什么基类的变动会导致我的代码有问题?我们先设想有个基类(我用Java写的,你不懂Java应该也可以比较容易的理解):import java.util.ArrayList; public class Array{ private ArrayList<Object> a = new ArrayList<Object>(); public void add(Object element) { a.add(element); } public void addAll(Object elements[]) { for (int i = 0; i < elements.length; ++i) a.add(elements[i]); // this line is going to be changed }}重要:注意注释的那段代码。这段代码后面的变更会破坏逻辑。这个类的接口有两个功能, add()和addAll()。add()会加一个单独的元素, addAll()会调用add方法来增加多个元素。这是衍生类:public class ArrayCount extends Array{ private int count = 0; @Override public void add(Object element) { super.add(element); ++count; } @Override public void addAll(Object elements[]) { super.addAll(elements); count += elements.length; }}ArrayCount类是Array类的一个具体实现。唯一的行为区别是ArrayCount保存了元素的数量(count)。让我们看下两个类的细节。Array add()添加一个元素到本地的ArrayList。Array addAll()为每个元素循环调用本地的ArrayList。ArrayCount add()调用父类的add()并且增加数量count。ArrayCount addAll()调用父类的addAll()然后根据元素的数量增加数量count。目前看起来都正常。现在打破逻辑了。基类注释的代码变更成以下这样: public void addAll(Object elements[]) { for (int i = 0; i < elements.length; ++i) add(elements[i]); // this line was changed }基类所有者关心的部分,功能还是按设想一样运转正常。并且所有自动化测试仍然可以通过。但所有者显然没有关注到派生类。所以派生类的作者被粗暴干扰了。现在ArrayCount addAll()调用父类的addAll(),其内部调用add()的逻辑已经被派生类覆盖了。这样会导致数量count在每次派生类调用add()时增加,然后在派生类调用addAll()时再被增加一次。这被计数了两次。如果是这样,并且已经发生了,派生类的作者必须知道积累是被如何实现的。他们必须在每次基类变更时被通知到,因为这可能会导致派生类在不可预见的情况下工作。太糟了!这个巨大的问题永久影响了继承范式的稳定性。脆弱的基类解决方案这次一样,包含和委托可以解决。使用包含和委托,我们从白盒编程转化成黑盒编程。白盒编程时,我们需要关注基类的实现。黑盒编程时,由于我们无法通过覆盖基类方法的方式来注入代码,我们可以完全忽略其实现。我们只需要关心接口。这个趋势有点危险。。。继承应该是重用最重要的手段。OO语言没有设计成让包含和委托方便使用。他们是被设计成让继承方便易用。如果你像我一样,你会开始对这个继承的问题开始惊奇。但更重要的是,这会让你对于继承的信心开始动摇。继承问题每次当我进入一家新公司,我都会对于找个地方放我公司文档的地方开始纠结,比如,员工手册。我是建一个目录叫“文档”然后在里面建个目录叫“公司”?或者我建一个目录叫“公司”然后在里面建个目录叫“文档”?都可以。但是哪一个是正确的? 是最好的?目录继承的想法是基类(父母)更加通用,派生类(子类)会更加具体。而且我们自己会在继承链上做更加具象化的版本。(看上面形状继承的例子)但当一个父类和子类可以互相调换位置时,这个模型明显哪里出了问题。继承问题解决方案现在的问题是。。。分类继承不工作了。所以继承方式好在哪里?包含。如果你看下现实世界,你可以看到包含(或排他所有权)继承到处都是。而你找不到的是分类继承。让那个先等一会。面向对象范式来源于于现实世界,对象被另一个对象填入。但他使用了一个有问题的模型。分类继承,没有现实世界的基础。现实世界使用的是包含继承。一个容器包含继承的很好的例子是你的袜子。他们在袜子的抽屉里,然后被你衣服的抽屉包进去,然后又被你的卧室包含,然后又被你的房子包含。你硬盘的目录是另一个容器包含继承的例子。他们保存文件。所以我们如何对他们分类?如果你考虑下公司目录,其实我放在哪里没什么太大关系。我可以把他们放在一个叫“文档”的目录或放在一个叫“东西”的目录。我分类的方式是使用tag标签。我使用以下标签来给文件打标:文档公司手册标签没有顺序或继承。(这也解决了钻石问题)tag与接口类似,你可以有多种类型与文档关联。看到这么多问题,看起来继承范式已经完了。再见,继承。微信公众号「麦芽面包」,id「darkjune_think」开发者/科幻爱好者/硬核主机玩家/业余翻译家/书虫交流Email: zhukunrong@yeah.net ...

December 23, 2018 · 1 min · jiezi

函数式编程,真香

最开始接触函数式编程的时候是在小米工作的时候,那个时候看老大以前写的代码各种 compose,然后一些 ramda 的一些工具函数,看着很吃力,然后极力吐槽函数式编程,现在回想起来,那个时候的自己真的是见识短浅,只想说,‘真香’。最近在研究函数式编程,真的是在学习的过程中感觉自己的思维提升了很多,抽象能力大大的提高了,让我深深的感受到了函数式编程的魅力。所以我打算后面用 5 到 8 篇的篇幅,详细的介绍一下函数式编程的思想,基础、如何设计、测试等。今天这篇文章主要介绍函数式编程的思想。函数式编程有用吗?什么是函数式编程?函数式编程的优点。面向对象编程(OOP)通过封装变化使得代码更易理解。函数式编程(FP)通过最小化变化使得代码更易理解。– Michacel Feathers(Twitter)总所周知 JavaScript 是一种拥有很多共享状态的动态语言,慢慢的,代码就会积累足够的复杂性,变得笨拙难以维护。面向对象设计能帮我们在一定程度上解决这个问题,但是还不够。由于有很多的状态,所以处理数据流和变化的传递显得尤为重要,不知道你们知道响应式编程与否,这种编程范式有助于处理 JavaScript 的异步或者事件响应。总之,当我们在设计应用程序的时候,我们应该考虑是否遵守了以下的设计原则。可扩展性–我是否需要不断地重构代码来支持额外的功能?易模块化–如果我更改了一个文件,另一个文件是否会受到影响?可重用性–是否有很多重复的代码?可测性–给这些函数添加单元测试是否让我纠结?易推理性–我写的代码是否非结构化严重并难以推理?我这能这么跟你说,一旦你学会了函数式编程,这些问题迎刃而解,本来函数式编程就是这个思想,一旦你掌握了函数式,然后你再学习响应式编程那就比较容易懂了,这是我亲身体会的。我之前在学 Rxjs 的时候是真的痛苦,说实话,Rxjs 是我学过最难的库了,没有之一。在经历过痛苦的一两个月之后,有些东西还是不能融会贯通,知道我最近研究函数式编程,才觉得是理所当然。毫无夸张,我也尽量在后面的文章中给大家介绍一下 Rxjs,这个话题我也在公司分享过。什么是函数式编程?简单来说,函数式编程是一种强调以函数使用为主的软件开发风格。看到这句我想你还是一脸懵逼,不知道函数式编程是啥,不要着急,看到最后我相信你会明白的。还有一点你要记住,函数式编程的目的是使用函数来抽象作用在数据之上的控制流和操作,从而在系统中消除副作用并减少对状态的改变。下面我们通过例子来简单的演示一下函数式编程的魅力。现在的需求就是输出在网页上输出 “Hello World”。可能初学者会这么写。document.querySelector(’#msg’).innerHTML = ‘<h1>Hello World</h1>‘这个程序很简单,但是所有代码都是死的,不能重用,如果想改变消息的格式、内容等就需要重写整个表达式,所以可能有经验的前端开发者会这么写。function printMessage(elementId, format, message) { document.querySelector(elementId).innerHTML = &lt;${format}&gt;${message}&lt;/${format}&gt;}printMessage(‘msg’, ‘h1’, ‘Hello World’)这样确实有所改进,但是任然不是一段可重用的代码,如果是要将文本写入文件,不是非 HTML,或者我想重复的显示 Hello World。那么作为一个函数式开发者会怎么写这段代码呢?const printMessage = compose(addToDom(‘msg’, h1, echo))printMessage(‘Hello World’)解释一下这段代码,其中的 h1 和 echo 都是函数,addToDom 很明显也能看出它是函数,那么我们为什么要写成这样呢?看起来多了很多函数一样。其实我们是讲程序分解为一些更可重用、更可靠且更易于理解的部分,然后再将他们组合起来,形成一个更易推理的程序整体,这是我们前面谈到的基本原则。compose 简单解释一下,他会让函数从最后一个参数顺序执行到第一个参数,compose 的每个参数都是函数,不明白的可以查一下,在 redux 的中间件部分这个函数式精华。可以看到我们是将一个任务拆分成多个最小颗粒的函数,然后通过组合的方式来完成我们的任务,这跟我们组件化的思想很类似,将整个页面拆分成若干个组件,然后拼装起来完成我们的整个页面。在函数式编程里面,组合是一个非常非常非常重要的思想。好,我们现在再改变一下需求,现在我们需要将文本重复三遍,打印到控制台。var printMessaage = compose(console.log, repeat(3), echo)printMessage(‘Hello World’)可以看到我们更改了需求并没有去修改内部逻辑,只是重组了一下函数而已。可以看到函数式编程在开发中具有声明模式。为了充分理解函数式编程,我们先来看下几个基本概念。声明式编程纯函数引用透明不可变性声明式编程函数式编程属于声明是编程范式:这种范式会描述一系列的操作,但并不会暴露它们是如何实现的或是数据流如何传过它们。我们所熟知的 SQL 语句就是一种很典型的声明式编程,它由一个个描述查询结果应该是什么样的断言组成,对数据检索的内部机制进行了抽象。我们再来看一组代码再来对比一下命令式编程和声明式编程。// 命令式方式var array = [0, 1, 2, 3]for(let i = 0; i < array.length; i++) { array[i] = Math.pow(array[i], 2)}array; // [0, 1, 4, 9]// 声明式方式[0, 1, 2, 3].map(num => Math.pow(num, 2))可以看到命令式很具体的告诉计算机如何执行某个任务。而声明式是将程序的描述与求值分离开来。它关注如何用各种表达式来描述程序逻辑,而不一定要指明其控制流或状态关系的变化。为什么我们要去掉代码循环呢?循环是一种重要的命令控制结构,但很难重用,并且很难插入其他操作中。而函数式编程旨在尽可能的提高代码的无状态性和不变性。要做到这一点,就要学会使用无副作用的函数–也称纯函数纯函数纯函数指没有副作用的函数。相同的输入有相同的输出,就跟我们上学学的函数一样,常常这些情况会产生副作用。改变一个全局的变量、属性或数据结构改变一个函数参数的原始值处理用户输入抛出一个异常屏幕打印或记录日志查询 HTML 文档,浏览器的 Cookie 或访问数据库举一个简单的例子var counter = 0function increment() { return ++counter;}这个函数就是不纯的,它读取了外部的变量,可能会觉得这段代码没有什么问题,但是我们要知道这种依赖外部变量来进行的计算,计算结果很难预测,你也有可能在其他地方修改了 counter 的值,导致你 increment 出来的值不是你预期的。对于纯函数有以下性质:仅取决于提供的输入,而不依赖于任何在函数求值或调用间隔时可能变化的隐藏状态和外部状态。不会造成超出作用域的变化,例如修改全局变量或引用传递的参数。但是在我们平时的开发中,有一些副作用是难以避免的,与外部的存储系统或 DOM 交互等,但是我们可以通过将其从主逻辑中分离出来,使他们易于管理。现在我们有一个小需求:通过 id 找到学生的记录并渲染在浏览器(在写程序的时候要想到可能也会写到控制台,数据库或者文件,所以要想如何让自己的代码能重用)中。// 命令式代码function showStudent(id) { // 这里假如是同步查询 var student = db.get(id) if(student !== null) { // 读取外部的 elementId document.querySelector(${elementId}).innerHTML = ${student.id},${student.name},${student.lastname} } else { throw new Error(’not found’) }}showStudent(‘666’)// 函数式代码// 通过 find 函数找到学生var find = curry(function(db, id) { var obj = db.get(id) if(obj === null) { throw new Error(’not fount’) } return obj})// 将学生对象 formatvar csv = (student) => ${student.id},${student.name},${student.lastname}// 在屏幕上显示var append = curry(function(elementId, info) { document.querySelector(elementId).innerHTML = info})var showStudent = compose(append(’#student-info’), csv, find(db))showStudent(‘666’)如果看不懂 curry (柯里化)的先不着急,这是一个对于新手来说比较难理解的一个概念,在函数式编程里面起着至关重要的作用。可以看到函数式代码通过较少这些函数的长度,将 showStudent 编写为小函数的组合。这个程序还不够完美,但是已经可以展现出相比于命令式的很多优势了。灵活。有三个可重用的组件声明式的风格,给高阶步骤提供了一个清晰视图,增强了代码的可读性另外是将纯函数与不纯的行为分离出来。我们看到纯函数的输出结果是一致的,可预测的,相同的输入会有相同的返回值,这个其实也被称为引用透明。引用透明引用透明是定义一个纯函数较为正确的方法。纯度在这个意义上表面一个函数的参数和返回值之间映射的纯的关系。如果一个函数对于相同的输入始终产生相同的结果,那么我们就说它是引用透明。这个概念很容易理解,简单的举两个例子就行了。// 非引用透明var counter = 0function increment() { return ++counter}// 引用透明var increment = (counter) => counter + 1其实对于箭头函数在函数式编程里面有一个高大上的名字,叫 lambda 表达式,对于这种匿名函数在学术上就是叫 lambda 表达式,现在在 Java 里面也是支持的。不可变数据不可变数据是指那些创建后不能更改的数据。与许多其他语言一样,JavaScript 里有一些基本类型(String,Number 等)从本质上是不可变的,但是对象就是在任意的地方可变。考虑一个简单的数组排序代码:var sortDesc = function(arr) { return arr.sort(function(a, b) { return b - a })}var arr = [1, 3, 2]sortDesc(arr) // [1, 2, 3]arr // [1, 2, 3]这段代码看似没什么问题,但是会导致在排序的过程中会产生副作用,修改了原始引用,可以看到原始的 arr 变成了 [1, 2, 3]。这是一个语言缺陷,后面会介绍如何克服。总结使用纯函数的代码绝不会更改或破坏全局状态,有助于提高代码的可测试性和可维护性函数式编程采用声明式的风格,易于推理,提高代码的可读性。函数式编程将函数视为积木,通过一等高阶函数来提高代码的模块化和可重用性。可以利用响应式编程组合各个函数来降低事件驱动程序的复杂性(这点后面可能会单独拿一篇来进行讲解)。欢迎关注个人公众号【前端桃园】,公号更新频率比掘金快。 ...

December 19, 2018 · 2 min · jiezi

面试官问:能否模拟实现JS的call和apply方法

之前写过两篇《面试官问:能否模拟实现JS的new操作符》和《面试官问:能否模拟实现JS的bind方法》其中模拟bind方法时是使用的call和apply修改this指向。但面试官可能问:能否不用call和apply来实现呢。意思也就是需要模拟实现call和apply的了。附上之前写文章写过的一段话:已经有很多模拟实现call和apply的文章,为什么自己还要写一遍呢。学习就好比是座大山,人们沿着不同的路登山,分享着自己看到的风景。你不一定能看到别人看到的风景,体会到别人的心情。只有自己去登山,才能看到不一样的风景,体会才更加深刻。先通过MDN认识下call和applyMDN 文档:Function.prototype.call()语法fun.call(thisArg, arg1, arg2, …)thisArg在fun函数运行时指定的this值。需要注意的是,指定的this值并不一定是该函数执行时真正的this值,如果这个函数处于非严格模式下,则指定为null和undefined的this值会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象。arg1, arg2, …指定的参数列表返回值返回值是你调用的方法的返回值,若该方法没有返回值,则返回undefined。MDN 文档:Function.prototype.apply()func.apply(thisArg, [argsArray])thisArg可选的。在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。argsArray可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。返回值调用有指定this值和参数的函数的结果。直接先看例子1call 和 apply 的异同相同点:1、call和apply的第一个参数thisArg,都是func运行时指定的this。而且,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。2、都可以只传递一个参数。不同点:apply只接收两个参数,第二个参数可以是数组也可以是类数组,其实也可以是对象,后续的参数忽略不计。call接收第二个及以后一系列的参数。看两个简单例子1和2**:// 例子1:浏览器环境 非严格模式下var doSth = function(a, b){ console.log(this); console.log([a, b]);}doSth.apply(null, [1, 2]); // this是window // [1, 2]doSth.apply(0, [1, 2]); // this 是 Number(0) // [1, 2]doSth.apply(true); // this 是 Boolean(true) // [undefined, undefined]doSth.call(undefined, 1, 2); // this 是 window // [1, 2]doSth.call(‘0’, 1, {a: 1}); // this 是 String(‘0’) // [1, {a: 1}]// 例子2:浏览器环境 严格模式下’use strict’;var doSth2 = function(a, b){ console.log(this); console.log([a, b]);}doSth2.call(0, 1, 2); // this 是 0 // [1, 2]doSth2.apply(‘1’); // this 是 ‘1’ // [undefined, undefined]doSth2.apply(null, [1, 2]); // this 是 null // [1, 2]typeof 有7种类型(undefined number string boolean symbol object function),笔者都验证了一遍:更加验证了相同点第一点,严格模式下,函数的this值就是call和apply的第一个参数thisArg,非严格模式下,thisArg值被指定为 null 或 undefined 时this值会自动替换为指向全局对象,原始值则会被自动包装,也就是new Object()。重新认识了call和apply会发现:它们作用都是一样的,改变函数里的this指向为第一个参数thisArg,如果明确有多少参数,那可以用call,不明确则可以使用apply。也就是说完全可以不使用call,而使用apply代替。也就是说,我们只需要模拟实现apply,call可以根据参数个数都放在一个数组中,给到apply即可。模拟实现 apply既然准备模拟实现apply,那先得看看ES5规范。ES5规范 英文版,ES5规范 中文版。apply的规范下一个就是call的规范,可以点击打开新标签页去查看,这里摘抄一部分。Function.prototype.apply (thisArg, argArray) 当以 thisArg 和 argArray 为参数在一个 func 对象上调用 apply 方法,采用如下步骤:1.如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异常。2.如果 argArray 是 null 或 undefined, 则返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。3.返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。4.如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常。5~8 略9.提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果。apply 方法的 length 属性是 2。在外面传入的 thisArg 值会修改并成为 this 值。thisArg 是 undefined 或 null 时它会被替换成全局对象,所有其他值会被应用 ToObject 并将结果作为 this 值,这是第三版引入的更改。结合上文和规范,如何将函数里的this指向第一个参数thisArg呢,这是一个问题。这时候请出例子3:// 浏览器环境 非严格模式下var doSth = function(a, b){ console.log(this); console.log(this.name); console.log([a, b]);}var student = { name: ‘轩辕Rowboat’, doSth: doSth,};student.doSth(1, 2); // this === student // true // ‘轩辕Rowboat’ // [1, 2]doSth.apply(student, [1, 2]); // this === student // true // ‘轩辕Rowboat’ // [1, 2]可以得出结论1:在对象student上加一个函数doSth,再执行这个函数,这个函数里的this就指向了这个对象。那也就是可以在thisArg上新增调用函数,执行后删除这个函数即可。知道这些后,我们试着容易实现第一版本:// 浏览器环境 非严格模式function getGlobalObject(){ return this;}Function.prototype.applyFn = function apply(thisArg, argsArray){ // apply 方法的 length 属性是 2。 // 1.如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异常。 if(typeof this !== ‘function’){ throw new TypeError(this + ’ is not a function’); } // 2.如果 argArray 是 null 或 undefined, 则 // 返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。 if(typeof argsArray === ‘undefined’ || argsArray === null){ argsArray = []; } // 3.如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常 . if(argsArray !== new Object(argsArray)){ throw new TypeError(‘CreateListFromArrayLike called on non-object’); } if(typeof thisArg === ‘undefined’ || thisArg === null){ // 在外面传入的 thisArg 值会修改并成为 this 值。 // ES3: thisArg 是 undefined 或 null 时它会被替换成全局对象 浏览器里是window thisArg = getGlobalObject(); } // ES3: 所有其他值会被应用 ToObject 并将结果作为 this 值,这是第三版引入的更改。 thisArg = new Object(thisArg); var __fn = ‘__fn’; thisArg[__fn] = this; // 9.提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果 var result = thisArg__fn; delete thisArg[__fn]; return result;};实现第一版后,很容易找出两个问题:[ ] 1.fn 同名覆盖问题,thisArg对象上有__fn,那就被覆盖了然后被删除了。针对问题1解决方案一:采用ES6 Sybmol() 独一无二的。可以本来就是模拟ES3的方法。如果面试官不允许用呢。解决方案二:自己用Math.random()模拟实现独一无二的key。面试时可以直接用生成时间戳即可。// 生成UUID 通用唯一识别码// 大概生成 这样一串 ‘18efca2d-6e25-42bf-a636-30b8f9f2de09’function generateUUID(){ var i, random; var uuid = ‘’; for (i = 0; i < 32; i++) { random = Math.random() * 16 | 0; if (i === 8 || i === 12 || i === 16 || i === 20) { uuid += ‘-’; } uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)) .toString(16); } return uuid;}// 简单实现// ‘’ + new Date().getTime();如果这个key万一这对象中还是有,为了保险起见,可以做一次缓存操作。比如如下代码:var student = { name: ‘轩辕Rowboat’, doSth: ‘doSth’,};var originalVal = student.doSth;var hasOriginalVal = student.hasOwnProperty(‘doSth’);student.doSth = function(){};delete student.doSth;// 如果没有,originalVal则为undefined,直接赋值新增了一个undefined,这是不对的,所以需判断一下。if(hasOriginalVal){ student.doSth = originalVal;}console.log(‘student:’, student); // { name: ‘轩辕Rowboat’, doSth: ‘doSth’ }[ ] 2.使用了ES6扩展符…解决方案一:采用eval来执行函数。eval把字符串解析成代码执行。MDN 文档:eval语法eval(string)参数string表示JavaScript表达式,语句或一系列语句的字符串。表达式可以包含变量以及已存在对象的属性。返回值执行指定代码之后的返回值。如果返回值为空,返回undefined解决方案二:但万一面试官不允许用eval呢,毕竟eval是魔鬼。可以采用new Function()来生成执行函数。MDN 文档:Function语法new Function ([arg1[, arg2[, …argN]],] functionBody)参数arg1, arg2, … argN被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的JavaScript标识符的字符串,或者一个用逗号分隔的有效字符串的列表;例如“×”,“theValue”,或“A,B”。functionBody一个含有包括函数定义的JavaScript语句的字符串。接下来看两个例子:简单例子:var sum = new Function(‘a’, ‘b’, ‘return a + b’);console.log(sum(2, 6));// 稍微复杂点的例子:var student = { name: ‘轩辕Rowboat’, doSth: function(argsArray){ console.log(argsArray); console.log(this.name); }};// var result = student.doSth([‘Rowboat’, 18]);// 用new Function()生成函数并执行返回结果var result = new Function(‘return arguments[0][arguments[1]](arguments[2][0], arguments[2][1])’)(student, ‘doSth’, [‘Rowboat’, 18]);// 个数不定// 所以可以写一个函数生成函数代码:function generateFunctionCode(argsArrayLength){ var code = ‘return arguments[0][arguments[1]](’; for(var i = 0; i < argsLength; i++){ if(i > 0){ code += ‘,’; } code += ‘arguments[2][’ + i + ‘]’; } code += ‘)’; // return arguments[0][arguments[1]](arg1, arg2, arg3…) return code;}你可能不知道在ES3、ES5中 undefined 是能修改的可能大部分人不知道。ES5中虽然在全局作用域下不能修改,但在局部作用域中也是能修改的,不信可以复制以下测试代码在控制台执行下。虽然一般情况下是不会的去修改它。function test(){ var undefined = 3; console.log(undefined); // chrome下也是 3}test();所以判断一个变量a是不是undefined,更严谨的方案是typeof a === ‘undefined’或者a === void 0;这里面用的是void,void的作用是计算表达式,始终返回undefined,也可以这样写void(0)。更多可以查看韩子迟的这篇文章:为什么用「void 0」代替「undefined」解决了这几个问题,比较容易实现如下代码。使用 new Function() 模拟实现的apply// 浏览器环境 非严格模式function getGlobalObject(){ return this;}function generateFunctionCode(argsArrayLength){ var code = ‘return arguments[0][arguments[1]](’; for(var i = 0; i < argsArrayLength; i++){ if(i > 0){ code += ‘,’; } code += ‘arguments[2][’ + i + ‘]’; } code += ‘)’; // return arguments[0][arguments[1]](arg1, arg2, arg3…) return code;}Function.prototype.applyFn = function apply(thisArg, argsArray){ // apply 方法的 length 属性是 2。 // 1.如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异常。 if(typeof this !== ‘function’){ throw new TypeError(this + ’ is not a function’); } // 2.如果 argArray 是 null 或 undefined, 则 // 返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。 if(typeof argsArray === ‘undefined’ || argsArray === null){ argsArray = []; } // 3.如果 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常 . if(argsArray !== new Object(argsArray)){ throw new TypeError(‘CreateListFromArrayLike called on non-object’); } if(typeof thisArg === ‘undefined’ || thisArg === null){ // 在外面传入的 thisArg 值会修改并成为 this 值。 // ES3: thisArg 是 undefined 或 null 时它会被替换成全局对象 浏览器里是window thisArg = getGlobalObject(); } // ES3: 所有其他值会被应用 ToObject 并将结果作为 this 值,这是第三版引入的更改。 thisArg = new Object(thisArg); var fn = ‘’ + new Date().getTime(); // 万一还是有 先存储一份,删除后,再恢复该值 var originalVal = thisArg[__fn]; // 是否有原始值 var hasOriginalVal = thisArg.hasOwnProperty(__fn); thisArg[__fn] = this; // 9.提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果。 // ES6版 // var result = thisArg__fn; var code = generateFunctionCode(argsArray.length); var result = (new Function(code))(thisArg, __fn, argsArray); delete thisArg[__fn]; if(hasOriginalVal){ thisArg[__fn] = originalVal; } return result;};利用模拟实现的apply模拟实现callFunction.prototype.callFn = function call(thisArg){ var argsArray = []; var argumentsLength = arguments.length; for(var i = 0; i < argumentsLength - 1; i++){ // argsArray.push(arguments[i + 1]); argsArray[i] = arguments[i + 1]; } console.log(‘argsArray:’, argsArray); return this.applyFn(thisArg, argsArray);}// 测试例子var doSth = function (name, age){ var type = Object.prototype.toString.call(this); console.log(typeof doSth); console.log(this === firstArg); console.log(’type:’, type); console.log(’this:’, this); console.log(‘args:’, [name, age], arguments); return ’this–’;};var name = ‘window’;var student = { name: ‘轩辕Rowboat’, age: 18, doSth: ‘doSth’, __fn: ‘doSth’,};var firstArg = student;var result = doSth.applyFn(firstArg, [1, {name: ‘Rowboat’}]);var result2 = doSth.callFn(firstArg, 1, {name: ‘Rowboat’});console.log(‘result:’, result);console.log(‘result2:’, result2);细心的你会发现注释了这一句argsArray.push(arguments[i + 1]);,事实上push方法,内部也有一层循环。所以理论上不使用push性能会更好些。面试官也可能根据这点来问时间复杂度和空间复杂度的问题。// 看看V8引擎中的具体实现:function ArrayPush() { var n = TO_UINT32( this.length ); // 被push的对象的length var m = %_ArgumentsLength(); // push的参数个数 for (var i = 0; i < m; i++) { this[ i + n ] = %_Arguments( i ); // 复制元素 (1) } this.length = n + m; // 修正length属性的值 (2) return this.length;};行文至此,就基本结束了,你可能还发现就是写的非严格模式下,thisArg原始值会包装成对象,添加函数并执行,再删除。而严格模式下还是原始值这个没有实现,而且万一这个对象是冻结对象呢,Object.freeze({}),是无法在这个对象上添加属性的。所以这个方法只能算是非严格模式下的简版实现。最后来总结一下。总结通过MDN认识call和apply,阅读ES5规范,到模拟实现apply,再实现call。就是使用在对象上添加调用apply的函数执行,这时的调用函数的this就指向了这个thisArg,再返回结果。引出了ES6 Symbol,ES6的扩展符…、eval、new Function(),严格模式等。事实上,现实业务场景不需要去模拟实现call和apply,毕竟是ES3就提供的方法。但面试官可以通过这个面试题考察候选人很多基础知识。如:call、apply的使用。ES6 Symbol,ES6的扩展符…,eval,new Function(),严格模式,甚至时间复杂度和空间复杂度等。读者发现有不妥或可改善之处,欢迎指出。另外觉得写得不错,可以点个赞,也是对笔者的一种支持。// 最终版版 删除注释版,详细注释看文章// 浏览器环境 非严格模式function getGlobalObject(){ return this;}function generateFunctionCode(argsArrayLength){ var code = ‘return arguments[0][arguments[1]](’; for(var i = 0; i < argsArrayLength; i++){ if(i > 0){ code += ‘,’; } code += ‘arguments[2][’ + i + ‘]’; } code += ‘)’; return code;}Function.prototype.applyFn = function apply(thisArg, argsArray){ if(typeof this !== ‘function’){ throw new TypeError(this + ’ is not a function’); } if(typeof argsArray === ‘undefined’ || argsArray === null){ argsArray = []; } if(argsArray !== new Object(argsArray)){ throw new TypeError(‘CreateListFromArrayLike called on non-object’); } if(typeof thisArg === ‘undefined’ || thisArg === null){ thisArg = getGlobalObject(); } thisArg = new Object(thisArg); var fn = ‘’ + new Date().getTime(); var originalVal = thisArg[__fn]; var hasOriginalVal = thisArg.hasOwnProperty(__fn); thisArg[__fn] = this; var code = generateFunctionCode(argsArray.length); var result = (new Function(code))(thisArg, __fn, argsArray); delete thisArg[__fn]; if(hasOriginalVal){ thisArg[__fn] = originalVal; } return result;};Function.prototype.callFn = function call(thisArg){ var argsArray = []; var argumentsLength = arguments.length; for(var i = 0; i < argumentsLength - 1; i++){ argsArray[i] = arguments[i + 1]; } return this.applyFn(thisArg, argsArray);}扩展阅读《JavaScript设计模式与开发实践》- 第二章 第 2 章 this、call和applyJS魔法堂:再次认识Function.prototype.call不用call和apply方法模拟实现ES5的bind方法JavaScript深入之call和apply的模拟实现关于作者:常以轩辕Rowboat为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。个人博客segmentfault个人主页掘金个人主页知乎github ...

November 30, 2018 · 6 min · jiezi

如何学习一门计算机编程语言

序言计算机编程是一个实践性很强的“游戏”,对于新入门者,好多人都在想,哪一门编程语言最好,我该从哪开始呢?我的回答是:语言不重要,理解编程思想才是最关键的!所有编程语言都支持的那一部分语言特性(核心子集)才是最核心的部分。所以从实际情况出发,选一门你看着顺眼,目前比较贴近你要做的工作或学习计划的计算机语言,开始你的编程之旅吧。观点阐述语言的核心子集包括哪些部分基本数据类型及运算符,这包括常量、变量、数组(所有的语言都支持一种基本数据结构)的定义与使用;数学运算符与逻辑运行符等知识。分支与循环,这是一门语言中的流程控制部分。基本库函数的使用,编程不可能从零开始,每门语言都有一个基本函数库,帮我们处理基本输入输出、文件读写等能用操作。业界有一个二八规律,其实编程也一样,大家回头看看,我们写了那么多代码,是不是大部分都属于这门语言的核心子集部分?也就是说,我们只要掌握了一门语言的核心子集,就可以开始工作了。常用编程范式面向过程编程(最早的范式,即命令式)面向对象编程(设计模式的概念是从它的实践活动中总结出来的)函数式编程(以纯函数基础,可以任意组合函数,实现集合到集合的流式数据处理)声明式编程(以数据结构的形式来表达程序执行的逻辑)事件驱动编程(其分布式异步事件模式,常用来设计大规模并发应用程序)面向切面编程(避免重复,分离关注点)我们要尽量多的了解不同的编程范式,这样能拓展我们的思路。学习语言的时候,有时可以同时学时两门编程语言,对比学习两门语言的同一概念,让我们能够更容易且深入的理解它。我学习javascript的闭包时,开始怎么也理解不了;我就找了本python书,对比着学,才慢慢的理解了。编程语言分类编译型语言 VS 解释型语言编译型:C、C++、Pascal、Object-C、swift解释型:JavaScript、Python、Erlang、PHP、Perl、Ruby混合型:java、C#,C#,javascript(基于V8)动态结构语言 VS 静态结构语言动态语言:Python、Ruby、Erlang、JavaScript、swift、PHP、SQL、Perl静态语言:C、C++、C#、Java、Object-C强类型语言 VS 弱类型语言强类型:Java、C#、Python、Object-C、Ruby弱类型:JavaScript、PHP、C、C++(有争议,介于强弱之间)各种类型的语言,我们都要有所了解,这样才能够全面的理解编程语言中的各种特性,在面对特定的问题时,才能做出正确的选择。通过实际项目来学习语言(以Typescript为例)项目需求:统一处理不同图形(圆形、长方形、矩形等)的面积计算。面向对象三大原则1.Circle类讲解数据封装概念,将半径与名称封装在类内部,并提供访问方法export default class Circle { private r: number private name: string constructor(r: number) { this.r = r this.name = ‘Circle’ } getName(): string { return this.name } area(): number { return Math.pow(this.r, 2) * PI }}2.长方形与矩形讲解继承概念//rectangle.tsexport default class Rectangle { private a: number private b: number private name: string constructor(a: number, b: number, name?: string) { this.a = a this.b = b if (name === undefined) this.name = ‘Rectangle’ else this.name = name } getName(): string { return this.name } area(): number { return this.a * this.b }}//square.tsexport default class Square extends Rectangle { constructor(a: number) { super(a, a, ‘Square’) }}3.实例统一处理不同的形状一起计算面积,讲解多态概念let shapes = Array<any>()shapes.push(new Circle(2))shapes.push(new Rectangle(5, 4))shapes.push(new Square(3))shapes.forEach((element) => { console.log(shape name: ${element.getName()}; shape area: ${element.area()})})接口概念阐述加入接口,规范形状对外部分操作要求,让错误提早到编译阶段被发现export default interface IShape { getName(): string; area(): number}函数式编程讲解用实例来说明怎样理解函数是一等公民,去掉我们习以为常的函数外层包裹let printData = function(err: any, data: string): void { if (err) console.log(err) else console.log(data)}let doAjax = function (data: string, callback: Function): void { callback(null, data)}doAjax(‘hello’, printData)异步处理中的经验分享在实践过程,处理异步调用容易误解的一个重要概念,异步函数执行的具体流程是什么样的?let pf = function(data: string, n: number, callback: Function) { console.log(begin run ${data}) setTimeout(() => { console.log(end run ${data}) callback(null, data) }, n)}let p = Promise.promisify(pf);(async () => { let ps = Array<any>() ps.push(p(‘1111’, 2000)) ps.push(p(‘2222’, 1000)) ps.push(p(‘3333’, 3000)) await Promise.all(ps)})()视频课程地址以上是《运用typescript进行node.js后端开发精要》视频课程的概要,有兴趣的童鞋可以去观看视频。传送门: 快来学习Typescript,加入会编程、能编程、乐编程的行列吧!资源地址https://github.com/zhoutk/sifou ...

November 9, 2018 · 1 min · jiezi

JS函数式编程 - 函子和范畴轮

在前面几篇介绍了函数式比较重要的一些概念和如何用函数组合去解决相对复杂的逻辑。是时候开始介绍如何控制副作用了。数据类型我们来看看上一篇最后例子:const split = curry((tag, xs) => xs.split(tag))const reverse = xs => xs.reverse()const join = curry((tag, xs) => xs.join(tag))const reverseWords = compose(join(’’), reverse, split(’’))reverseWords(‘Hello,world!’);这里其实reverseWords还是很难阅读,你不知道他入参是啥,返回值又是啥。你如果不去看一下代码,一开始在使用他的时候,你应该是比较害怕的。 “我是不是少传了一个参数?是不是传错了参数?返回值真的一直都是一个字符串吗?”。这也是类型系统的重要性了,在不断了解函数式后,你会发现,函数式编程和类型是密切相关的。如果在这里reverseWords的类型明确给出,就相当于文档了。但是,JavaScript是动态类型语言,我们不会去明确的指定类型。不过我们可以通过注释的方式加上类型:// reverseWords: string => stringconst reverseWords = compose(join(’’), reverse, split(’’))上面就相当于指定了reverseWords是一个接收字符串,并返回字符串的函数。JS 本身不支持静态类型检测,但是社区有很多JS的超集是支持类型检测的,比如Flow还有TypeScript。当然类型检测不光是上面所说的自文档的好处,它还能在预编译阶段提前发现错误,能约束行为等。当然我的后续文章还是以JS为语言,但是会在注释里面加上类型。范畴论相关概念范畴论其实并不是特别难,不过是些抽象点的概念。而且我们不需要了解的特别深,函数式编程很多概念是从范畴论映射过来的。了解范畴论相关概念有助于我们理解函数式编程。另外,相信我,只要你小学初中学过一元函数和集合,看懂下面的没有问题。定义范畴的定义:一组对象,是需要操作的数据的一个集合一组态射,是数据对象上的映射关系,比如 f: A -> B态射组合,就是态射能够几个组合在一起形成一个新的态射图片出处:https://en.wikipedia.org/wiki…一个简单的例子,上图来自维基百科。上面就是一个范畴,一共有3个数据对象A,B,C,然后f和g是态射,而gof是一组态射组合。是不是很简单?其中态射可以理解是函数,而态射的组合,我们可以理解为函数的组合。而里面的一组对象,不就是一个具有一些相同属性的数据集嘛。函子(functor)函子是用来将两个范畴关联起来的。图片出处:https://ncatlab.org/nlab/show…对应上图,比如对于范畴 C 和 D ,函子 F : C => D 能够:将 C 中任意对象X 转换为 D 中的 F(X); 将 C 中的态射 f : X => Y 转换为 D 中的 F(f) : F(X) => F(Y)。你可以发现函子可以:转换对象转换态射构建一个函子(functor)Container正如上面所说,函子能够关联两个范畴。而范畴里面必然是有一组数据对象的。这里引入Container,就是为了引入数据对象:class Container { constructor (value) { this.$value = value } // (value) => Container(value) static of(value) { return new Container(value) }}我们声明了一个Container的类,然后给了一个静态的of方法用于去生成这个Container的实例。这个of其实还有个好听的名字,卖个关子,后面介绍。我们来看一下使用这个Container的例子:// Container(123)Container.of(123)// Container(“Hello Conatiner!")Container.of(“Hello Conatiner!”)// Container(Conatiner(“Test !”))Container.of(Container.of(“Test !”))正如上面看到的,Container是可以嵌套的。我们仔细看一下这个Contaienr:$value的类型不确定,但是一旦赋值之后,类型就确定了一个Conatiner只会有一个value我们虽然能直接拿到$value,但是不要这样做,不然我们要个container干啥呢第一个functor让我们回看一下定义,函子是用来将两个范畴关联起来的。所以我们还需要一个态射(函数)去把两个范畴关联起来:class Container { constructor (value) { this.$value = value } // (value) => Container(value) static of(value) { return new Container(value) } // (fn: x=>y) => Container(fn(value)) map(fn) { return new Container(fn(this.$value)) } }先来用一把:const concat = curry((str, xs) => xs.concat(str))const prop = curry((prop, xs) => xs[prop])// Container(‘TEST’)Container.of(’test’).map(s => s.toUpperCase())// Container(10)Container.of(‘bombs’).map(concat(’ away’)).map(prop(’length’)); 不晓得上面的curry是啥的看第二篇文章。你可能会说:“哦,这是你说的functor,那又有啥用呢?”。接下来,就讲一个应用。不过再讲应用前先讲一下这个of,其实上面这种functor,叫做pointed functor, ES5里面的Array就应用了这种模式:Array.of。他是一种模式,不仅仅是用来省略构建对象的new关键字的。我感觉和scala里面的compaion object有点类似。Maybe type在现实的代码中,存在很多数据是可选的,返回的数据可能是存在的也可能不存在:type Person = { info?: { age?: string }}上面是flow里面的类型声明,其中?代表这个数据可能存在,可能不存在。我相信像上面的数据结构,你在接收后端返回的数据的时候经常遇到。假如我们要取这个age属性,我们通常是怎么处理的呢?当然是加判断啦:const person = { info: {} }const getAge = (person) => { return person && person.info && person.info.age}getAge(person) // undefined你会发现为了取个age,我们需要加很多的判断。当数据中有很多是可选的数据,你会发现你的代码充满了这种类型判断。心累不?Okey,Maybe type就是为了解决这个问题的,先让我们实现一个:class Maybe { static of(x) { return new Maybe(x); } get isNothing() { return this.$value === null || this.$value === undefined; } constructor(x) { this.$value = x; } map(fn) { return this.isNothing ? this : Maybe.of(fn(this.$value)); } get() { if (this.isNothing) { throw new Error(“Get Nothing”) } else { return this.$value } } getOrElse(optionValue) { if (this.isNothing) { return optionValue } else { return this.$value } }}应用一波:type Person = { info?: { age?: string }}const prop = curry((tag, xs) => xs[tag])const map = curry((fn, f) => f.map(fn))const person = { info: {} }// safe get ageMaybe.of(person.info).map(prop(“age”)) // Nothing// safe get age Point free styleconst safeInfo = xs => Maybe.of(person.info)const getAge = compose(map(prop(‘age’)), safeInfo)getAge(person) // Nothing来复盘一波,上面的map依然是一个functor(函子)。不过呢,在做类型转换的时候加上了逻辑:map(fn) { return this.isNothing ? this : Maybe.of(fn(this.$value));}所以也就是上面的转换关系可以表示为:其实一看图就出来了,“哦,你把判断移动到了map里面。有啥用?”。ok,罗列一下好处:更安全将判断逻辑进行封装,代码更简洁声明式代码,没有各种各样的判断其实,不确定性,也是一种副作用。对于可选的数据,我们在运行时是很难确定他的真实的数据类型的,我们用Maybe封装一下其实本身就是封装这种不确定性。这样就能保证我们的一个入参只有可能会返回一种输出了。先就这,下一篇介绍另外两个函子的应用(其实不应该叫应用),Either和IO。 ...

October 29, 2018 · 2 min · jiezi

JS函数式编程 - 函数组合与柯里化

我们都知道单一职责原则,其实面向对象的SOLID中的S(SRP, Single responsibility principle)。在函数式当中每一个函数就是一个单元,同样应该只做一件事。但是现实世界总是复杂的,当把现实世界映射到编程时,单一的函数就没有太大的意义。这个时候就需要函数组合和柯里化了。链式调用如果用过jQuery的都晓得啥是链式调用,比如$(’.post’).eq(1).attr(‘data-test’, ’test’).javascript原生的一些字符串和数组的方法也能写出链式调用的风格:‘Hello, world!’.split(’’).reverse().join(’’) // “!dlrow ,olleH"首先链式调用是基于对象的,上面的一个一个方法split, reverse, join如果脱离的前面的对象"Hello, world!“是玩不起来的。而在函数式编程中方法是独立于数据的,我们可以把上面以函数式的方式在写一遍:const split = (tag, xs) => xs.split(tag)const reverse = xs => xs.reverse()const join = (tag, xs) => xs.join(tag)join(’’,reverse(split(’’,‘Hello, world!’))) // “!dlrow ,olleH"你肯定会说,你是在逗我。这比链式调用好在哪儿了?这里还是依赖于数据的啊,没有传递`‘Hello, world!’,你这一串一串的函数组合也转不起来啊。这里唯一的好处也就是那几个单独的方法可以复用了。莫慌,后面还有那么多内容我怎么也会给你优化(忽悠)好的。再进行改造前,我们先介绍两个概念,部分应用和柯里化。部分应用部分应用是一种处理函数参数的流程,他会接收部分参数,然后返回一个函数接收更少的参数。这个就是部分应用。我们用bind来实现一把:const addThreeArg = (x, y, z) => x + y + z;const addTwoArg = addThreeNumber.bind(null, 1)const addOneArg = addThreeNumber.bind(null, 1, 2)addTwoArg(2, 3) // 6addOneArg(7) // 10上面利用bind生成了另外两个函数,分别接受剩下的参数,这就是部分应用。当然你也可以通过其他方式实现。部分应用存在的问题部分应用主要的问题在于,它返回的函数类型无法直接推断。正如前面所说,部分应用返回一个函数接收更少的参数,而没有规定返回的参数具体是多少个。这也就是一些隐式的东西,你需要去查看代码。才知道返回的函数接收多少个参数。柯里化柯里化定义:你可以调一个函数,但是不一次将所有参数传给它。这个函数会返回一个函数去接收下一个参数。const add = x => y => x + yconst plusOne = add(1)plusOne(10) // 11柯里化的函数返回一个只接收一个参数的函数,返回的函数类型可以预测。当然在实际开发中,有很多的函数都不是柯里化的,我们可以使用一些工具函数来转化:const curry = (fn) => { // fn可以是任何参数的函数 const arity = fn.length; return function $curry(…args) { if (args.length < arity) { return $curry.bind(null, …args); } return fn.call(null, …args); };};也可以用开源库Ramda里提供的curry方法。哦,柯里化。有什么用呢?举个例子const currySplit = curry((tag, xs) => xs.split(tag))const split = (tag, xs) => xs.split(tag)// 我现在需要一个函数去split “,“const splitComma = currySplit(’,’) //by curryconst splitComma = string => split(’,’, string)可以看到柯里化的函数生成新函数时,和数据完全没有关系。对比两个生成新函数的过程,没有柯里化的相对而言就有一点啰嗦了。函数组合先给代码:const compose = (…fns) => (…args) => fns.reduceRight((res, fn) => [fn.call(null, …res)], args)[0];其实compose做的事情一共两件:接收一组函数,返回一个函数,不立即执行函数组合函数,将传递给他的函数从左到右组合。可能有同学对上面的reduceRight不是很熟悉,我给个2元和3元的例子:const compose = (f, g) => (…args) => f(g(…args))const compose3 = (f, g, z) => (…args) => f(g(z(…args)))函数调用是从左到右,数据流也是一样的从左到右。当然你可以定义从右到左的,不过从语义上来说就不那么表意了。好,现在让我们来优化一下最开始的例子:const split = curry((tag, xs) => xs.split(tag))const reverse = xs => xs.reverse()const join = curry((tag, xs) => xs.join(tag))const reverseWords = compose(join(’’), reverse, split(’’))reverseWords(‘Hello,world!’);是不是简洁易于理解多了。这里的reverseWords也是我们之前讲过的Pointfree的代码风格。不依赖数据和外部状态,就是组合在一起的一个函数。Pointfree我在上一篇介绍过JS函数式编程 - 概念,也阐述了其优缺点,有兴趣的小伙伴可以看看。函数组合的结合律先回顾一下小学知识加法结合律:a+(b+c)=(a+b)+c。我就不解释了,你们应该能理解。回过来看函数组合其实也存在结合律的:compose(f, compose(g, h)) === compose(compose(f, g), h);这个对于我们编程有一个好处,我们的函数组合可以随意组合并且缓存:const split = curry((tag, xs) => xs.split(tag))const reverse = xs => xs.reverse()const join = curry((tag, xs) => xs.join(tag))const getReverseArray = compose(reverse, split(’’))const reverseWords = compose(join(’’), getReverseArray)reverseWords(‘Hello,world!’);脑图补充:OK,下一篇介绍一下范畴轮,和函子。 ...

October 14, 2018 · 1 min · jiezi

lodash源码分析之数据类型获取的兼容性

焦虑和恐惧的区别是,恐惧是对世界上的存在的恐惧,而焦虑是在"我"面前的焦虑。——萨特《存在与虚无》本文为读 lodash 源码的第十九篇,后续文章会更新到这个仓库中,欢迎 star:pocket-lodashgitbook也会同步仓库的更新,gitbook地址:pocket-lodash前言在前文《lodash源码分析之获取数据类型》已经解释了获取数据类型的方法,但是在有些环境下,一些 es6 新增的对象获取到的类型都为 [object Object] ,这样就没办法做细致的区分。例如在 IE11 中,通过 Object.prototype.toString 获取到的 DataView 对象类型为 [object Object]。 因此在 getTag 中,lodash 针对这些对象做了一些兼容性的事情。依赖import baseGetTag from ‘./baseGetTag.js’《lodash源码分析之获取数据类型》源码分析const dataViewTag = ‘[object DataView]‘const mapTag = ‘[object Map]‘const objectTag = ‘[object Object]‘const promiseTag = ‘[object Promise]‘const setTag = ‘[object Set]‘const weakMapTag = ‘[object WeakMap]’/** Used to detect maps, sets, and weakmaps. */const dataViewCtorString = ${DataView}const mapCtorString = ${Map}const promiseCtorString = ${Promise}const setCtorString = ${Set}const weakMapCtorString = ${WeakMap}let getTag = baseGetTag// Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6.if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) || (getTag(new Map) != mapTag) || (getTag(Promise.resolve()) != promiseTag) || (getTag(new Set) != setTag) || (getTag(new WeakMap) != weakMapTag)) { getTag = (value) => { const result = baseGetTag(value) const Ctor = result == objectTag ? value.constructor : undefined const ctorString = Ctor ? ${Ctor} : ’’ if (ctorString) { switch (ctorString) { case dataViewCtorString: return dataViewTag case mapCtorString: return mapTag case promiseCtorString: return promiseTag case setCtorString: return setTag case weakMapCtorString: return weakMapTag } } return result }}getTag 的源码很简单,处理的是 DataView、Map、Set、Promise、WeakMap 等对象,下面就关键的几点说明一下。函数的toString方法const dataViewCtorString = ${DataView}const mapCtorString = ${Map}const promiseCtorString = ${Promise}const setCtorString = ${Set}const weakMapCtorString = ${WeakMap}我们都知道,DataView 这些其实都是构造函数,函数有 toString 的方法,调用后返回的是 function DataView() { [native code] } 这样的格式,因为其实例调用 Object.prototype.toString 在某些环境下返回的是 [object Object],而构造函数的 toString 返回的字符串中,包含了构造函数名,可以通过这点来区分。实例中构造函数的获取 const Ctor = result == objectTag ? value.constructor : undefined const ctorString = Ctor ? ${Ctor} : ‘‘每个实例中都包含一个 constructor 的属性,这个属性指向的是实例的构造函数,在获取到这个构造函数后,就可以调用它的 toString 方法,然后就可以比较了。Promise.resolvegetTag(Promise.resolve()) != promiseTag在条件判断时,使用了 Promise.resolve() ,这样使用的目的是获取到 promise 对象,因为 Promise 是一个函数函数,如果直接调用 Object.prototype.toString,返回的是 [object Function]。License署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0)最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见: 作者:对角另一面 ...

September 24, 2018 · 2 min · jiezi

手动实现一个compose函数

在redux中合并reducer的时候有用到compose这个函数将多个reducer合成一个,那么这个compose函数该怎么实现呢?function compose(…fns) { //fns是传入的函数 const fn = fns.pop(); return (…args) => { fn(…args); if (fns.length > 0) { compose(…fns); } };}

September 3, 2018 · 1 min · jiezi