• 用函数式编程,从0开发3D引擎和编辑器(二):函数式编程准备


    (本文已过期,请看从0开发3D引擎(五):函数式编程及其在引擎中的应用
    大家好,本文介绍了本系列涉及到的函数式编程的主要知识点,为正式开发做好了准备。

    函数式编程的优点

    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);
    
    //将foo的值取出来,设置到five这个Immutable变量中
    let five = foo^; 
    
    //修改foo的值为6,five的值仍然为5
    foo := 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引擎和编辑器(三):初步需求分析

  • 相关阅读:
    堆排序算法
    二叉树的创建、遍历(递归和非递归实现)、交换左右子数、求高度(c++实现)
    hdoj1010 奇偶剪枝+DFS
    常见排序算法c++总结
    B
    C
    D
    E
    G
    F
  • 原文地址:https://www.cnblogs.com/chaogex/p/10807469.html
Copyright © 2020-2023  润新知