大家好,本文介绍了本系列涉及到的函数式编程的主要知识点,为正式开发做好了准备。

函数式编程的优点

1.粒度小

相比面向对象编程以类为单位,函数式编程以函数为单位,粒度更小。

正所谓:

我只想要一个香蕉,而面向对象却给了我整个森林

2.性能好

大部分人认为函数式编程差,主要基于下面的理由(参考 JavaScript 函数式编程存在性能问题么?):

1)柯西化、函数组合等操作增加时间开销

2)map、reduce等操作,会进行多次遍历,增加时间开销

3)Immutable数据每次操作都会被拷贝为新的数据,增加时间和内存开销

而我说性能好,是指通过“Reason的编译优化+Immutable/Mutable结合使用+递归/迭代结合使用”,可以解决这些问题:

1)由于Bucklescript编译器在编译时的优化,柯西化等操作和Immutable数据被编译成了优化过的js代码,大幅减小了时间开销

2)由于Reason支持Mutable和for,while迭代操作,所以可以在性能热点使用它们,提高性能。

3.擅长处理数据,适合3D领域编程

通过高阶函数、柯西化、组合等工具,函数式编程可以像流水线一样对数据进行管道操作,非常方便。

3D程序有大量的数据要操作,从函数式编程的角度来看:

3D程序=数据+逻辑

因此,我们可以:

使用Immutable/Mutable、Data Oriented等思想和数据结构表达数据;

使用函数表达逻辑;

使用组合、柯西化等工具,把数据和逻辑关联起来。

更多讨论

FP之优点

函数式编程(Functional Programming)相比面向对象编程(Object-oriented Programming)有哪些优缺点?

本系列使用的函数式编程语言

我们使用Reason语言,它是从Ocaml而来的,属于非纯函数式编程语言。

而我们熟知的Haskell,属于纯函数式编程语言。

Reason学习文档

为什么不用纯函数式编程语言

1.更高的性能

Reason支持Mutable、迭代操作,提高了性能

2.更简单易用

1)允许非纯操作,所以不需要使用Haskell中的各种Monad

2)严格求值相对于惰性求值更简单。

搭建Reason开发环境

详见Reason的介绍和搭建Reason开发环境

本系列涉及的函数式编程知识点

数据

  • Immutable

介绍

创建不可变数据之后,对其任何的操作,都会返回一个拷贝后的新数据。

示例

Reason的变量默认为immutable:

let a = 1;

/* a为immutable */

Reason也有专门的不可变数据结构,如Tuple,List,Record。

这里以Record为例,它类似于Javascript中的Object:

首先定义Record的类型:

type person = {
age: int,
name: string
};

然后定义Record的值:

let me = {
age: 5,
name: "Big Reason"
};

使用这个Record,如修改"age"的值:

let newMe = {
...me,
age: 10
}; Js.log(newMe === me); /* false */

newMe是从me拷贝而来,任何对newMe的修改,都不会影响me。

在Wonder中的应用

在编辑器中的应用

编辑器的所有数据都是Immutable的,这样的好处是:

1)不用关心数据之间的关联关系,因为每个数据都是独立的

2)不用担心状态被修改,减少了很多bug

3)实现Redo/Undo功能时非常简单,直接把Immutable的数据压入History的栈里即可,不用深拷贝/恢复数据。

在引擎中的应用

大部分函数的局部变量都是Immutable的(如使用tuple,record结构)。

相关资料

Reason->Let Binding

Reason->Record

facebook immutable.js 意义何在,使用场景?

Introduction to Immutable.js and Functional Programming Concepts

  • Mutable

介绍

对可变数据的任何操作,都会直接修改原数据。

示例

Reason通过"ref"关键字,标志变量为Mutable。

let foo = ref(5);

let five = foo^; 

foo := 6;   //foo===five===6

Reason也可以通过"mutable"关键字,标志Record的字段为Mutable。

type person = {
name: string,
mutable age: int
};
let baby = {name: "Baby Reason", age: 5};
baby.age = baby.age + 1; /* 修改原数据baby的age为6 */

在Wonder中的应用

因为操作Mutable数据不会造成拷贝,没有垃圾回收cg的开销,所以在性能热点处,常常使用Mutable数据。

相关资料

Reason->Mutable

函数

函数是第一公民,函数是数据。

相关资料:

如何理解在 JavaScript 中 "函数是第一等公民" 这句话?

Reason->Function

  • 纯函数

介绍

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

示例

let a = 1;

/* func2是纯函数 */
let func2 = value => value; /* func1是非纯函数,因为使用了外部变量"a" */
let func1 = () => a;

在Wonder中的应用

脚本的钩子函数(如init,update,dispose等函数)属于纯函数(但不能算严格的纯函数),这样是为了:

1)能够正确序列化

脚本会先序列化为字符串,保存在文件中(如编辑器导出的包中);

然后在导入该文件时(如编辑器导入包),将脚本字符串反序列化为函数(执行:eval('(' + funcStr + ')'))。如果脚本的钩子函数不是纯函数(如调用了外部变量),则会报错。

2)支持多线程

目前脚本是在主线程执行的,但因为它是纯函数,所以未来可以放在单独的脚本线程中执行,提高性能。

注意

虽然纯函数好处很多,但Wonder中大多数的函数都是非纯函数,这是因为:

1)为了性能

2)为了简单易用,所以允许副作用,很少使用容器

相关资料

第 3 章:纯函数的好处

  • 高阶函数

介绍

函数能够作为数据,成为高阶函数的参数或者返回值。

示例

let func1 = func => func(1);

let func2 = value => value * 2;

func1(func2);   /* func1是高阶函数,因为func2是func1的参数 */

在Wonder中的应用

多个函数中常常有一些共同的逻辑,需要消除重复,可以通过提出一个私有的高阶函数来解决。具体示例如下:

重构前:

let add1 = value => value + 2;

let add2 = value => value + 10;

let minus1 = value => value - 10;

let minus2 = value => value - 200;

let compute1 = value => value |> add1 |> minus1;

let compute2 = value => value |> add2 |> minus2;

/* compute1,compute2有重复逻辑 */

重构后:

...

let _compute = (value, (addFunc, minusFunc)) =>
value |> addFunc |> minusFunc; let compute1 = value => _compute(value, (add1, minus1)); let compute2 = value => _compute(value, (add2, minus2));

相关资料

理解 JavaScript 中的高阶函数

  • 柯西化

介绍

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

你可以一次性地调用 curry 函数,也可以每次只传一个参数分多次调用。

示例

let func1 = (value1, value2) => value1 + value2;

let func2 = func1(1);

func2(2);   /* 3 */

在Wonder中的应用

应用的地方太多了,此处省略。

相关资料

第 4 章: 柯里化(curry)

Currying

类型

相关资料

The "Understanding F# types" series

  • 基本类型

介绍

Reason是强类型语言,包含int、float、string等基本类型。

示例

type a = string;   /* 定义a为string类型 */

let str:a = "zzz";   /* 变量str为a类型 */

在Wonder中的应用

类型在wonder中应用广泛,包括以下的使用场景:

1)类型驱动设计

2)领域建模

3)枚举

相关资料

Reason->Type

Algebraic type sizes and domain modelling

  • Discriminated Union Type

介绍

类型可以接受参数,还可以组合其它的类型。

示例

type result('a, 'b) =
| Ok('a)
| Error('b); type myPayload = {data: string}; let payloadResults: list(result(myPayload, string)) = [
Ok({data: "hi"}),
Ok({data: "bye"}),
Error("Something wrong happened!")
];

在Wonder中的应用

1)作为容器的实现

2)是实现本文后面的Recursive Type的基础

相关资料

Reason->Type Argument

Reason->Null, Undefined & Option

Discriminated Unions

  • 抽象类型

介绍

有时候我们想定义一个类型,它不是某一个具体的类型,可以将其定义为抽象类型。

示例

type value;

type a = value; /* a为value类型 */

在Wonder中的应用

包括以下的使用案例:

1)在封装WebGL api的FFI中(什么是FFI?),把WebGL的上下文定义为抽象类型。

示例代码如下:

/* FFI */

/* 抽象类型 */
type webgl1Context; [@bs.send]
external getWebgl1Context : ('canvas, [@bs.as "webgl"] _) => webgl1Context = "getContext"; [@bs.send.pipe: webgl1Context]
external viewport : (int, int, int, int) => unit = ""; /* client code */ /* canvasDom是canvas的dom,此处省略了获取它的代码 */
/* gl是webgl1Context类型 */
/* 编译后的js代码为:var gl = canvasDom.getContext("webgl"); */
let gl = getWebgl1Context(canvasDom); /* 编译后的js代码为:gl.viewport(0,0,100,100); */
gl |> viewport(0,0,100,100);

2)脚本->属性->value可以为int或者float类型,因此将value设为抽象类型,并且定义抽象类型和int、float类型之间的转换FFI。

示例代码如下:


type scriptAttributeType =
| Int
| Float; /* 抽象类型 */
type scriptAttributeValue; type scriptAttributeField = {
type_: scriptAttributeType,
value: scriptAttributeValue
}; /* 定义scriptAttributeValue和int,float类型相互转换的FFI */ external intToScriptAttributeValue: int => scriptAttributeValue = "%identity"; external floatToScriptAttributeValue: float => scriptAttributeValue =
"%identity"; external scriptAttributeValueToInt: scriptAttributeValue => int = "%identity"; external scriptAttributeValueToFloat: scriptAttributeValue => float =
"%identity"; /* client code */ /* 创建scriptAttributeField,设置value的数据(int类型) */ let scriptAttributeField = {
type_: Int,
value:intToScriptAttributeValue(10)
}; /* 修改scriptAttributeField->value */ let newScriptAttributeField = {
...scriptAttributeField,
value: (scriptAttributeValueToInt(scriptAttributeField.value) + 1) |> intToScriptAttributeValue
};

相关资料

抽象类型(Abstract Types)

  • Recursive Type

介绍

从类型定义上看,可以看成是Discriminated Union Type,只是其中至少有一个union type为自身类型,即递归地指向自己。

示例

还是看代码好理解点,具体示例如下:

type nodeId = int;

/* tree是Recursive Type,它的文件夹节点包含了子节点,而子节点的类型为自身 */
type tree =
| LeafNode(nodeId)
| FolderNode(
nodeId,
array(tree),
);

在Wonder中的应用

在编辑器中的应用

Recursive Type常用在树中,如编辑器的资产树的类型就是Recursive Type。

相关资料

The "Recursive types and folds" series

Map as a Recursion Scheme in OCaml

过程

  • 组合

介绍

多个函数可以组合起来,使得前一个函数的返回值是后一个函数的输入,从而对数据进行管道处理。

示例

let func1 = value => value1 + 1;

let func2 = value => value1 + 2;

10 |> func1 |> func2;   /* 13 */

在Wonder中的应用

在引擎中的应用

组合可以应用在多个层面,如函数层面和job层面。

job = 多个函数的组合

我们来看下job组合的应用示例:

从时间序列上来看:

引擎=初始化+主循环

而初始化和每一次循环,都是多个job组合而成的管道操作:

初始化 = create_canvas |> create_gl |> ...

每一次循环 = tick |> dispose |> reallocate_cpu_memory |> update_transform |> ...

相关资料

第 5 章: 代码组合(compose)

  • 递归

介绍

遍历操作可以分成两类:

迭代

递归

递归就是指函数调用自己,满足终止条件时结束。如深度优先遍历是递归操作,而广度优先遍历是迭代操作。

注意:

尽量写成尾递归,这样Reason会将其编译成迭代操作。

示例

let rec func1 = (value, result) => {
value > 3 ? result : func1(value + 1, result + value);
}; func1(1, 0); /* 0+1+2+3=6; */

在Wonder中的应用

几乎所有的遍历都是尾递归,只有在少数使用Mutable和少数性能热点的地方,使用迭代操作(使用for或while命令)。

相关资料

什么是尾递归?

Reason->Recursive Functions

  • 模式匹配

介绍

使用switch结构代替if else处理程序分支。

示例

let func1 = value => {
switch(value){
| 0 => 10
| _ => 100
}
}; func1(0); /* 10 */
func1(2); /* 100 */

在Wonder中的应用

主要用在下面三种场景:

1)取出容器的值

type a =
| A(int)
| B(string); switch(a){
| A(value) => value
| B(value) => value
};

2)处理Option

let a = Some(1);

switch(a){
| None => ...
| Some(value) => ...
}

3)处理枚举类型

type a =
| A
| B; switch(a){
| A => ...
| B => ...
}

相关资料

Reason->Pattern Matching!

模式匹配

异步

  • 函数反应式编程

介绍

处理异步,主要有以下的方法:

1)回调函数

缺点:过多的回调导致嵌套层次太深,容易陷入回调地狱,不易维护。

2)Promise

3)await,aync

4)使用函数反应式编程的流

优点:能够使用组合,像管道处理一样处理各种流,符合函数式编程的思维。

Wonder使用流来处理异步,其中也用到了Promise,不过都被封装成了流。

示例

使用most库实现FRP,因为它的性能比Rxjs更好。

/*
输出:
next:2
next:4
next:6
complete
*/
let subscription =
Most.from([|1, 2, 3|])
|> Most.map(value => value * 2)
|> Most.subscribe({
"next": value => Js.log2("next:", value),
"error": e => Js.log2("error:", e##message),
"complete": () => Js.log("complete"),
});

在Wonder中的应用

凡是异步操作,如事件处理、多线程等,都用流来处理。

相关资料

你一直都错过的反应型编程

函数式反应型编程 (FRP) —— 实时互动应用开发的新思路

函数式响应型编程(Functional Reactive Programming)会在什么问题上有优势?

容器

  • 容器

介绍

为了领域建模,或者为了保证纯函数而隔离副作用,需要把值封装到容器中。外界只能操作容器,不直接操作值。

示例

1)领域建模示例

比如我们要开发一个图书管理系统,需要对“书”进行建模。

书有书号、页数这两个数据,有小说书、技术书两种类型。

建模为:

type bookId = int;

type pageNum = int;

type book =
| Novel(bookId, pageNum)
| Technology(bookId, pageNum);

现在我们创建一本小说,一本技术书,以及它们的集合:

let novel = Novel(0, 100);

let technology = Technology(1, 200);

let bookList = [
novel,
technology
];

对“书”这个容器进行操作:

let getPage = (book) =>
switch(book){
| Novel(_, page) => page
| Technology(_, page) => page
}; let setPage = (page, book) =>
switch(book){
| Novel(bookId, _) => Novel(bookId, page)
| Technology(bookId, _) => Technology(bookId, page)
}; /* client code */ /* 将技术书的页数设置为集合中所有书的总页数 */
let newTechnology =
bookList
|> List.fold_left((totalPage, book) => totalPage + getPage(book), 0)
|> setPage(_, technology);

在Wonder中的应用

包含以下使用场景:

1)领域建模

2)错误处理

3)处理空值

使用Option包装空值。

相关资料

Railway Oriented Programming

The "Map and Bind and Apply, Oh my!" series

强大的容器

Monad

Applicative Functor

多态

  • GADT

介绍

全称为Generalized algebraic data type,可以用来实现函数参数多态。

示例

重构前,需要对应每种类型,定义一个isXXXEqual函数:

let isIntEqual = (source: int, target: int) => source == target;

let isStringEqual = (source: string, target: string) => source == target;

isIntEqual(1, 1); /*true*/

isStringEqual("aaa", "aaa"); /*true*/

使用GADT重构后,对应多个类型,只有一个isEqual函数:

type isEqual(_) =
| Int: isEqual(int)
| Float: isEqual(float)
| String: isEqual(string); let isEqual = (type g, kind: isEqual(g), source: g, target: g) =>
switch (kind) {
| _ => source == target
}; isEqual(Int, 1, 1); /*true*/ isEqual(String, "aaa", "aaa"); /*true*/

在Wonder中的应用

1)契约检查

如需要判断两个变量是否相等,则使用GADT,定义一个assertEqual方法替换assertStringEqual,assertIntEqual等方法。

相关资料

Why GADTs matter for performance(需要翻墙)

维基百科->Generalized algebraic data type

  • Module Functor

介绍

module可以作为参数,传递给functor,返回一个新的module。

类似于面向对象的“继承”,可以使用函子functor,在基module上扩展出新的module。

示例

module type Comparable = {
type t; let equal: (t, t) => bool;
}; module MakeAdd = (Item: Comparable) => {
let add = (x: Item.t, newItem: Item.t, list: list(Item.t)) =>
Item.equal(x, newItem) ? list : [newItem, ...list];
}; module A = {
type t = int;
let equal = (x1, x2) => x1 == x2;
}; /* module B有add函数,该方法调用了A.equal函数 */
module B = MakeAdd(A); let list = B.add(1, 2, []); /* list == [2] */
let list = list |> B.add(1, 1); /* list == [2] */

在Wonder中的应用

在编辑器中的应用

1)错误处理

错误被包装为容器Result;

由于容器Result中的值的类型不一样,所以将Result分成RelationResult、SameDataResult。

这两类Result有共同的模式,因此可以提出基module:Result,然后增加MakeRelationResult、MakeSameDataResult这两个module functor。它们将Result作为参数,返回新的module:RelationResult、SameDataResult,从而消除重复。

相关资料

Reason->Module Functions

函数式编程学习资料

JS 函数式编程指南

这本书作为我学习函数式编程的第一本书,非常容易上手,作者讲得很简单易懂,推荐~

Awesome FP JS

收集了函数式编程相关的资料。

F# for fun and profit

这个博客讲了很多F#相关的函数式编程的知识,非常推荐!

如果你正在使用Reason或者Ocaml或者F#语言,建议到该博客中学习!

欢迎浏览上一篇博文:用函数式编程,从0开发3D引擎和编辑器(一)

欢迎浏览下一篇博文:用函数式编程,从0开发3D引擎和编辑器(三):初步需求分析

用函数式编程,从0开发3D引擎和编辑器(二):函数式编程准备的更多相关文章

  1. 用函数式编程,从0开发3D引擎和编辑器(一)

    介绍 大家好,欢迎你踏上3D编程之旅- 本系列的素材来自我们的产品:Wonder-WebGL 3D引擎和编辑器 的整个开发过程,探讨了在从0开始构建3D引擎和编辑器的过程中,每一个重要的功能点.设计方 ...

  2. 用函数式编程,从0开发3D引擎和编辑器(三):初步需求分析

    大家好,本文介绍了Wonder的高层需求和本系列对应的具体功能点. 确定Wonder高层需求 业务目标 Wonder是web端3D开发的解决方案,包括引擎.编辑器,致力于打造开放.分享.互助的生态. ...

  3. 从0开发3D引擎(十二):使用领域驱动设计,从最小3D程序中提炼引擎(第三部分)

    目录 上一篇博文 继续实现 实现"DirectorJsAPI.init" 实现"保存WebGL上下文"限界上下文 实现"初始化所有Shader&quo ...

  4. 从0开发3D引擎(五):函数式编程及其在引擎中的应用

    目录 上一篇博文 函数式编程的优点与缺点 优点 缺点 为什么使用Reason语言 函数式编程学习资料 引擎中相关的函数式编程知识点 数据 不可变数据 可变数据 函数 纯函数 高阶函数 柯西化 参考资料 ...

  5. 从0开发3D引擎(六):函数式反应式编程及其在引擎中的应用

    目录 上一篇博文 介绍函数式反应式编程 函数式反应式编程学习资料 函数式反应式编程的优点与缺点 优点 缺点 异步处理的其它方法 为什么使用Most库 引擎中相关的函数式反应式编程知识点 参考资料 大家 ...

  6. 从0开发3D引擎(一):开篇

    介绍 大家好,本系列带你踏上Web 3D编程之旅- 本系列是实战类型,从0开始带领读者写出"良好架构.良好扩展性.最小功能集合(MVP)" 的3D引擎. 本系列的素材来自我们的产品 ...

  7. 从0开发3D引擎(十一):使用领域驱动设计,从最小3D程序中提炼引擎(第二部分)

    目录 上一篇博文 本文流程 回顾上文 解释基本的操作 开始实现 准备 建立代码的文件夹结构,约定模块文件的命名规则 模块文件的命名原则 一级和二级文件夹 api_layer的文件夹 applicati ...

  8. 从0开发3D引擎(四):搭建测试环境

    目录 上一篇博文 了解自动化测试 单元测试 集成测试 端对端测试 通过打印日志来调试 了解运行测试 断点调试 通过Spector.js测试WebGL 通过log调试Shader 移动端测试 了解性能测 ...

  9. 从0开发3D引擎(七):学习Reason语言

    目录 上一篇博文 介绍Reason Reason的优势 如何学习Reason? 介绍Reason的部分知识点 大家好,本文介绍Reason语言以及学习Reason的方法. 上一篇博文 从0开发3D引擎 ...

随机推荐

  1. 执行yaml.load()出现警告信息:YAMLLoadWarning: callingyaml.load() without Loader=..

    执行yaml.load()出现警告信息:YAMLLoadWarning: callingyaml.load() without Loader=... 原因: yaml5.1版本后弃用了yaml.loa ...

  2. web前端之css基础

    CSS选择器 元素选择器 p{color:red;} ID选择器 #li{ background-color:red; } 类选择器 .c1{ font-size:15px; } 注意: 样式类名不要 ...

  3. Spring Cloud Alibaba(五)RocketMQ 异步通信实现

    本文探讨如何使用 RocketMQ Binder 完成 Spring Cloud 应用消息的订阅和发布. 介绍 RocketMQ 是一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的.高 ...

  4. jenkins System error

    背景 在使用WAR包安装jenkins后,启动tomcat,显示启动成功,但最后提示信息如下: 04-Dec-2018 03:28:21.563 WARNING [Computer.threadPoo ...

  5. Knative Serverless 之道:如何 0 运维、低成本实现应用托管?

    作者 | 牛秋霖(冬岛)  阿里云容器平台技术专家 关注"阿里巴巴云原生"公众号,回复关键词"1205"即可观看 Knative-Demo 演示视频. 导读:S ...

  6. 负载均衡集群介绍、LVS介绍、LVS调度算法、LVS NAT模式搭建

    7月4日任务 18.6 负载均衡集群介绍18.7 LVS介绍18.8 LVS调度算法18.9/18.10 LVS NAT模式搭建 扩展lvs 三种模式详解 http://www.it165.net/a ...

  7. VUE的中v-if和v-shou的区别

    v-if的特点:每次都会重新删除或创建元素 v-shou的特点:每次执行都只是切换了元素的display:none的属性 v-if的缺点: 每次使用都会有较高性能消耗(频繁的切换元素建议不适用,建议使 ...

  8. 【限时免费】从入门到实战,5节课玩转Kafka!赢音箱、书籍好礼!

    欢迎添加华为云小助手微信(微信号:HWCloud002 或 HWCloud003),输入关键字"加群",加入华为云线上技术讨论群:输入关键字"最新活动",获取华 ...

  9. 一统江湖的大前端(8)- velocity.js 运动的姿势(上)

    [摘要] 介绍CSS动画和JS动画的基本特点,以及轻量级动画库velocity.js的基本用法. 示例代码托管在:http://www.github.com/dashnowords/blogs 博客园 ...

  10. 每个开发人员都应该知道的11个Linux命令

    本文主要挑选出读者有必要首先学习的 11 个 Linux 命令,如果不熟悉的读者可以在虚拟机或云服务器上实操下,对于开发人员来说,能熟练掌握 Linux 做一些基本的操作是必要的! 事不宜迟,这里有 ...