<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      從0開發(fā)3D引擎(五):函數(shù)式編程及其在引擎中的應(yīng)用

      大家好,本文介紹我們?yōu)槭裁词褂煤瘮?shù)式編程來開發(fā)引擎,以及它在引擎中的相關(guān)的知識點(diǎn)。

      上一篇博文

      從0開發(fā)3D引擎(四):搭建測試環(huán)境

      下一篇博文

      從0開發(fā)3D引擎(六):函數(shù)式反應(yīng)式編程及其在引擎中的應(yīng)用

      函數(shù)式編程的優(yōu)點(diǎn)與缺點(diǎn)

      優(yōu)點(diǎn)

      (1)粒度小
      面向?qū)ο缶幊桃灶悶閱挝唬瘮?shù)式編程以函數(shù)為單位,粒度更小。

      我只想要一個(gè)香蕉,而面向?qū)ο髤s給了我整個(gè)森林

      (2)擅長處理數(shù)據(jù),適合3D領(lǐng)域的編程
      通過高階函數(shù)、柯西化、函數(shù)組合等工具,函數(shù)式編程可以像流水線一樣對數(shù)據(jù)進(jìn)行管道操作,非常方便。

      而3D程序正好要處理大量的數(shù)據(jù),從函數(shù)式編程的角度來看:
      3D程序=數(shù)據(jù)+邏輯

      因此,我們可以這樣使用函數(shù)式編程范式來進(jìn)行3D編程:

      • 使用Immutable/Mutable數(shù)據(jù)結(jié)構(gòu)、Data Oriented思想來表達(dá)數(shù)據(jù)
      • 使用函數(shù)來表達(dá)邏輯
      • 使用組合、柯西化等操作作為工具,把數(shù)據(jù)和邏輯關(guān)聯(lián)起來,進(jìn)行管道操作

      現(xiàn)代的3D引擎越來越傾向于面向數(shù)據(jù)進(jìn)行設(shè)計(jì),從而獲得更佳的性能,如Unity新版本有很多Data Oriented的思想;
      也越來越傾向于使用函數(shù)式編程范式,如Frostbite使用Frame Graph來封裝現(xiàn)代圖形API(DX12),而Frame Graph是面向數(shù)據(jù)的,有函數(shù)式風(fēng)格的編碼框架。

      缺點(diǎn)

      (1)存在性能問題

      • Reduce、Map、Filter等操作需要遍歷多次,會增加時(shí)間開銷
        我們可以通過下面的方法來優(yōu)化:
        a)減少不必要的Map、Reduce等操作;
        b)使用transducer來合并這些操作。具體可以參考Understanding transducer in Javascript

      • 柯西化、組合等操作會增加時(shí)間開銷

      • 每次操作Immutable數(shù)據(jù),都需要復(fù)制它為新的數(shù)據(jù),增加了時(shí)間和內(nèi)存開銷

      為什么使用Reason語言

      本系列使用Reason語言來實(shí)現(xiàn)函數(shù)式編程。

      Reason語言可以解決前面提到的性能問題:

      • Bucklescript編譯器在編譯時(shí)進(jìn)行了很多優(yōu)化,使柯西化、組合等操作和Immutable數(shù)據(jù)被編譯成了優(yōu)化過的js代碼,大幅減小了時(shí)間開銷和內(nèi)存開銷
        更多編譯器的優(yōu)化以及與Typescript的比較可參考:
        架構(gòu)最快最好的To JS編譯器

      • Reason支持Mutable變量、for/while進(jìn)行迭代遍歷、非純函數(shù)
        在性能熱點(diǎn)處可以使用它們來提高性能,而在其它地方則盡量使用Immutable數(shù)據(jù)、遞歸遍歷和純函數(shù)來提高代碼的可讀性和健壯性。

      另外,Reason屬于“非純函數(shù)式編程語言”,為什么不使用Haskell這種“純函數(shù)式編程語言”呢?
      因?yàn)橐韵聨c(diǎn)原因:
      (1)獲得更高的性能
      在性能熱點(diǎn)處使用非純操作(如使用Mutable變量),提高性能。
      (2)更簡單易用
      Reason允許非純函數(shù),不需要像Haskell一樣使用各種Monad來隔離副作用,保持“純函數(shù)”;
      Reason使用嚴(yán)格求值,相對于Haskell的惰性求值更簡單。

      函數(shù)式編程學(xué)習(xí)資料

      • JS 函數(shù)式編程指南
        這本書作為我學(xué)習(xí)函數(shù)式編程的第一本書,講得很簡單易懂,非常容易上手,推薦~

      • Awesome FP JS
        收集了函數(shù)式編程相關(guān)的資料。

      • F# for fun and profit
        這個(gè)博客講了很多F#相關(guān)的函數(shù)式編程的知識點(diǎn),介紹了怎樣基于類型來設(shè)計(jì)、怎樣處理錯(cuò)誤等,非常全面和通俗易懂,強(qiáng)力推薦~
        Reason語言基于Ocaml語言,而Ocaml語言與F#語言都屬于ML語言類別的,很多概念和語法都類似,所以讀者在該博客學(xué)到的內(nèi)容,也可以直接應(yīng)用到Reason。

      引擎中相關(guān)的函數(shù)式編程知識點(diǎn)

      本文從以下幾個(gè)方面進(jìn)行介紹:

      數(shù)據(jù)

      因?yàn)槲覀儾皇褂萌肿兞浚峭ㄟ^形參傳入函數(shù)需要的變量,所以所有的變量都是函數(shù)的局部變量。

      我們把與引擎相關(guān)的需要持久化的數(shù)據(jù),聚合在一起成為一個(gè)Record類型的數(shù)據(jù),命名為“State”。該Record的一些成員是可變的(用來存放性能優(yōu)先的數(shù)據(jù)),另外的成員是不可變的。

      關(guān)于Record數(shù)據(jù)結(jié)構(gòu),可以參考Record

      不可變數(shù)據(jù)

      介紹
      不能直接修改不可變數(shù)據(jù)的值。
      創(chuàng)建不可變數(shù)據(jù)之后,對其任何的操作,都會返回一個(gè)復(fù)制后的新數(shù)據(jù)。

      示例
      變量默認(rèn)為不可變的(Immutable):

      //a為immutable變量
      let a = 1;
      //導(dǎo)致編譯錯(cuò)誤
      a = 2;
      

      Reason也有專門的不可變數(shù)據(jù)結(jié)構(gòu),如Tuple、List、Record。
      其中,Record類似于Javascript中的Object,我們以它為例,來看下如何使用不可變數(shù)據(jù)結(jié)構(gòu):
      首先定義Record的類型:

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

      然后定義Record的值,它被編譯器推導(dǎo)為person類型:

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

      最后操作這個(gè)Record,如修改“age”的值:

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

      newMe是從me復(fù)制而來的。任何對newMe的修改,都不會影響me。
      (這里Reason進(jìn)行了優(yōu)化,只復(fù)制了修改的age字段,沒有復(fù)制name字段 )

      在引擎中的應(yīng)用

      大部分?jǐn)?shù)據(jù)都是不可變的(是不可變變量,或者是Tuple,Record等數(shù)據(jù)結(jié)構(gòu)),這樣的優(yōu)點(diǎn)是:
      1)不用關(guān)心數(shù)據(jù)之間的關(guān)聯(lián)關(guān)系,因?yàn)槊總€(gè)數(shù)據(jù)都是獨(dú)立的
      2)不可變數(shù)據(jù)不能被修改

      相關(guān)資料
      Reason->Let Binding
      Reason->Record
      facebook immutable.js 意義何在,使用場景?
      Introduction to Immutable.js and Functional Programming Concepts

      可變數(shù)據(jù)

      介紹
      對可變數(shù)據(jù)的任何操作,都會直接修改原數(shù)據(jù)。

      示例
      Reason使用"ref"關(guān)鍵字定義Mutable變量:

      let foo = ref(5);
      
      //將foo的值取出來,設(shè)置到five這個(gè)Immutable變量中
      let five = foo^; 
      
      //修改foo的值為6,five的值仍然為5
      foo := 6;
      

      Reason也可以通過"mutable"關(guān)鍵字,定義Record的字段為Mutable字段:

      type person = {
        name: string,
        mutable age: int
      };
      let baby = {name: "Baby Reason", age: 5};
      //修改原數(shù)據(jù)baby->age的值為6
      baby.age = baby.age + 1; 
      

      在引擎中的應(yīng)用
      因?yàn)椴僮骺勺償?shù)據(jù)不需要拷貝,沒有垃圾回收的開銷,所以在性能熱點(diǎn)處常常使用可變數(shù)據(jù)。

      相關(guān)資料
      Reason->Mutable

      函數(shù)

      函數(shù)是第一等公民,函數(shù)即是數(shù)據(jù)。

      相關(guān)資料:
      如何理解在 JavaScript 中 "函數(shù)是第一等公民" 這句話?
      Reason->Function

      純函數(shù)

      介紹

      純函數(shù)是這樣一種函數(shù),即相同的輸入,永遠(yuǎn)會得到相同的輸出,而且沒有任何可觀察的副作用。

      示例

      let a = 1;
      
      /* func2是純函數(shù) */
      let func2 = value => value;
      
      /* func1是非純函數(shù),因?yàn)橐昧送獠孔兞?a" */
      let func1 = () => a;
      

      在引擎中的應(yīng)用

      腳本組件的鉤子函數(shù)(如init,update,dispose等函數(shù),這些函數(shù)會在主循環(huán)的特定時(shí)間點(diǎn)被調(diào)用,從而執(zhí)行函數(shù)中用戶的邏輯)屬于純函數(shù),這樣是為了:
      1)在導(dǎo)入/導(dǎo)出為Scene Graph文件時(shí),能夠正確序列化
      當(dāng)導(dǎo)出為Scene Graph文件時(shí),序列化鉤子函數(shù)為字符串,保存在文件中;
      當(dāng)導(dǎo)入Scene Graph文件時(shí),反序列化字符串為函數(shù)。如果鉤子函數(shù)不是純函數(shù)(如調(diào)用了外部變量),則在此時(shí)會報(bào)錯(cuò)(因?yàn)橥獠孔兞坎]有定義在字符串中,所以會找不到該變量)。
      2)支持多線程
      可以通過序列化的方式將鉤子函數(shù)傳到獨(dú)立于主線程的腳本線程,從而在該線程中被執(zhí)行,實(shí)現(xiàn)多線程執(zhí)行腳本,提高性能。

      雖然純函數(shù)優(yōu)點(diǎn)很多,但引擎中大多數(shù)的函數(shù)都是非純函數(shù),這是因?yàn)椋?br> 1)為了提高性能
      2)為了簡單,允許副作用,從而避免使用Monad

      相關(guān)資料
      第 3 章:純函數(shù)的好處

      高階函數(shù)

      介紹
      高階函數(shù)的輸入或者輸出為函數(shù)。

      示例

      //func1是高階函數(shù),因?yàn)樗膮?shù)是函數(shù)
      let func1 = func => func(1);
      
      let func2 = value => value * 2;
      
      //a=2
      let a = func1(func2);   
      

      在引擎中的應(yīng)用

      函數(shù)之間常常有一些重復(fù)或者類似的邏輯,可以通過提出一個(gè)私有的高階函數(shù)來消除重復(fù)。具體示例如下:
      重構(gòu)前:

      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有重復(fù)邏輯
      
      

      重構(gòu)后:

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

      相關(guān)資料
      理解 JavaScript 中的高階函數(shù)

      柯西化

      介紹

      只傳遞給函數(shù)一部分參數(shù)來調(diào)用它,讓它返回一個(gè)函數(shù)去處理剩下的參數(shù)。
      你可以一次性地調(diào)用curry 函數(shù),也可以每次只傳一個(gè)參數(shù)分多次調(diào)用。

      示例

      let func1 = (value1, value2) => value1 + value2;
      
      //傳入第一個(gè)參數(shù),func2只有一個(gè)參數(shù)value2
      let func2 = func1(1);
      
      //a=3
      let a = func2(2);
      

      在引擎中的應(yīng)用

      應(yīng)用的地方太多了,此處省略。

      相關(guān)資料
      第 4 章: 柯里化(curry)
      Currying

      公有/私有函數(shù)

      介紹
      模塊Module中的函數(shù),有些是暴露給外部訪問的,我們稱其為“公有函數(shù)”;另外的函數(shù)是內(nèi)部私有的,我們稱其為“私有函數(shù)”。

      可以通過創(chuàng)建Module對應(yīng)的.rei文件,來定義要暴露的公有函數(shù)。

      我們沒有使用這種方法,而是通過約定函數(shù)的名稱:
      以下劃線“_”開頭的函數(shù)是私有函數(shù),其它函數(shù)是公有函數(shù)。

      示例

      module Test = {
        //私有函數(shù)
        let _func1 = v => v;
        //公有函數(shù)
        let func2 = v => v * 2;
      };
      

      在引擎中的應(yīng)用
      引擎中的函數(shù)都是用這種命名約定,來區(qū)分公有函數(shù)和私有函數(shù)。

      相關(guān)資料
      Module -> “Every .rei file is a signature”

      類型

      Reason是強(qiáng)類型語言,編譯時(shí)會檢查類型是否正確。

      本系列希望通過盡可能強(qiáng)的類型約束,來達(dá)到“編譯通過即程序正確,減少大量的測試工作”的目的。

      關(guān)于Reason類型帶來的好處,參考架構(gòu)最快最好的To JS編譯器

      更好的類型安全: typescript是一個(gè)JS的超集,它存在很多歷史包袱。而微軟引入typescript更多的是作為一個(gè)工具來使用的比如IDE的代碼補(bǔ)全,相對安全的代碼重構(gòu)。而這個(gè)類型的準(zhǔn)確從第一天開始就不是它的設(shè)計(jì)初衷,以至于Facebook自己設(shè)計(jì)了一個(gè)相對更準(zhǔn)確地類型系統(tǒng)Flow. 而OCaml的類型系統(tǒng)是已經(jīng)被形式化的證明過正確的。也就是說從理論上BuckleScript 能夠保證一旦編譯通過是不會有運(yùn)行時(shí)候類型錯(cuò)誤的,而typescript遠(yuǎn)遠(yuǎn)做不到這點(diǎn)。
      更多的類型推斷,更好的語言特性:用過typescript的人都知道,typescript的類型推斷很弱,基本上所有參數(shù)都需要顯示的標(biāo)注類型。不光是這點(diǎn),像對函數(shù)式編程的支持,高階類型系統(tǒng)GADT的支持幾乎是沒有。而OCaml本身是一個(gè)比Elm,PureScript還要強(qiáng)大的多的語言,它自身有一個(gè)非常高階的module system,是為數(shù)不多的對dependent type提供支持的語言,polymorphic variant。而且pattern match的編譯器也是優(yōu)化過的。

      相關(guān)資料
      The "Understanding F# types" series

      基本類型

      介紹
      Reason包含int、float、string等基本類型。

      示例

      //定義a為string類型
      type a = string;
      //定義str變量的類型為a
      let str:a = "zzz";
      

      在引擎中的應(yīng)用

      應(yīng)用廣泛,包括以下的使用場景:
      1)類型驅(qū)動設(shè)計(jì)
      2)領(lǐng)域建模
      3)枚舉

      相關(guān)資料

      Reason->Type
      Algebraic type sizes and domain modelling

      Discriminated Union類型

      介紹
      Discriminated Union類型可以接受參數(shù),還可以組合其它的類型。

      示例

      //result為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!")
      ];
      

      在引擎中的應(yīng)用

      作為本文后面講到的“容器”的實(shí)現(xiàn),用于領(lǐng)域建模

      相關(guān)資料
      Reason->Type Argument
      Reason->Null, Undefined & Option
      Discriminated Unions

      抽象類型

      介紹
      抽象類型只給出類型名字,沒有具體的定義。

      示例

      //value為抽象類型
      type value;
      

      在引擎中的應(yīng)用

      包括以下的使用場景:
      1)如果不需要類型的具體定義,則將該類型定義為抽象類型

      如在封裝WebGL API的FFI中(什么是FFI?),因?yàn)椴恍枰馈癢ebGL的上下文”包含哪些方法和屬性,所以將其定義為抽象類型。

      示例代碼如下:

      //抽象類型
      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
      
      //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)如果一個(gè)數(shù)據(jù)可能為多個(gè)類型,則定義一個(gè)抽象類型和它與這“多個(gè)類型”之間相互轉(zhuǎn)換的FFI,然后把該數(shù)據(jù)設(shè)為該抽象類型

      如腳本->屬性->value字段可以為int或者float類型,因此將value設(shè)為抽象類型,并且定義抽象類型和int、float類型之間的轉(zhuǎn)換FFI。

      示例代碼如下:

      
      type scriptAttributeType =
        | Int
        | Float;
      
      
      //抽象類型
      type scriptAttributeValue;
      
      type scriptAttributeField = {
        type_: scriptAttributeType,
        //定義value字段為該抽象類型
        value: scriptAttributeValue
      };
      
      //定義抽象類型scriptAttributeValue和int,float類型相互轉(zhuǎn)換的FFI
      
      external intToScriptAttributeValue: int => scriptAttributeValue = "%identity";
      
      external floatToScriptAttributeValue: float => scriptAttributeValue =
        "%identity";
      
      external scriptAttributeValueToInt: scriptAttributeValue => int = "%identity";
      
      external scriptAttributeValueToFloat: scriptAttributeValue => float =
        "%identity";
        
        
      //client code
      
      //創(chuàng)建scriptAttributeField,設(shè)置value的數(shù)據(jù)
      
      let scriptAttributeField = {
          type_: Int,
          value:intToScriptAttributeValue(10) 
      };
      
      
      
      //修改scriptAttributeField->value
      
      let newScriptAttributeField = {
          ...scriptAttributeField,
          value: (scriptAttributeValueToInt(scriptAttributeField.value) + 1) |> intToScriptAttributeValue
      };
      

      相關(guān)資料
      抽象類型(Abstract Types)

      過程

      組合

      介紹
      多個(gè)函數(shù)可以組合起來,使前一個(gè)函數(shù)的返回值作為后一個(gè)函數(shù)的輸入,從而對數(shù)據(jù)進(jìn)行管道處理。

      示例

      let func1 = value => value1 + 1;
      
      let func2 = value => value1 + 2;
      
      //13
      10 |> func1 |> func2;
      

      在引擎中的應(yīng)用

      把多個(gè)函數(shù)組合成job,再把多個(gè)job組合成一個(gè)管道操作,處理每幀的邏輯。

      我們從組合的角度來分析下引擎的結(jié)構(gòu):

      job = 多個(gè)函數(shù)的組合
      
      引擎=初始化+主循環(huán)
      
      //而初始化和主循環(huán)的每一幀,都是由多個(gè)job組合而成的管道操作:
      初始化 = create_canvas |> create_gl |> ...
      
      每一次循環(huán) = tick |> dispose |> reallocate_cpu_memory |> update_transform |> ...
      

      相關(guān)資料

      第 5 章: 代碼組合(compose)

      迭代和遞歸

      介紹

      遍歷操作可以分成兩類:
      迭代
      遞歸

      例如廣度優(yōu)先遍歷是迭代操作,而深度優(yōu)先遍歷是遞歸操作

      Reason支持用for、while循環(huán)實(shí)現(xiàn)迭代操作,用“rec”關(guān)鍵字定義遞歸函數(shù)。
      Reason支持尾遞歸優(yōu)化,可將其編譯成迭代操作。所以我們應(yīng)該在需要遍歷很多次的地方,用尾遞歸進(jìn)行遍歷。

      示例

      //func1為尾遞歸函數(shù)
      let rec func1 = (value, result) => {
          value > 3 ? result : func1(value + 1, result + value);
      };
      
      //0+1+2+3=6
      func1(1, 0);
      

      在引擎中的應(yīng)用

      幾乎所有的遍歷都是尾遞歸遍歷(因?yàn)橄鄬τ诘a更可讀),只有在少數(shù)使用Mutable和少數(shù)性能熱點(diǎn)的地方,使用迭代遍歷

      相關(guān)資料
      什么是尾遞歸?
      Reason->Recursive Functions
      Reason->Imperative Loops

      模式匹配

      介紹
      使用switch代替if/else來處理程序分支。

      示例

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

      在引擎中的應(yīng)用

      主要用在下面三種場景:

      1)取出容器的值

      type a = 
          | A(int)
          | B(string);
          
      let aValue = 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 => ...
      }
      

      相關(guān)資料
      Reason->Pattern Matching!
      模式匹配

      容器

      介紹

      為了領(lǐng)域建模,或者為了隔離副作用來保證純函數(shù),需要把值封裝到容器中,使外界只能操作容器,不能直接操作值。

      示例

      1)領(lǐng)域建模示例

      比如我們要開發(fā)一個(gè)圖書管理系統(tǒng),需要對“書”進(jìn)行建模。
      書有書號、頁數(shù)這兩個(gè)數(shù)據(jù),有小說書、技術(shù)書兩種類型。
      建模為:

      type bookId = int;
      
      type pageNum = int;
      
      //book為Discriminated Union Type
      //book作為容器,定義了兩個(gè)Union Type:Novel、Technology
      type book = 
          | Novel(bookId, pageNum)
          | Technology(bookId, pageNum);
          
      

      現(xiàn)在我們創(chuàng)建一本小說,一本技術(shù)書,以及它們的集合list:

      let novel = Novel(0, 100);
      
      let technology = Technology(1, 200);
      
      let bookList = [
          novel,
          technology
      ];
      

      對“書”這個(gè)容器進(jìn)行操作:

      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
      
      //得到新的技術(shù)書,它的頁數(shù)為集合中所有書的總頁數(shù)
      let newTechnology =
      bookList
      |> List.fold_left((totalPage, book) => totalPage + getPage(book), 0)
      |> setPage(_, technology);
      
      

      在引擎中的應(yīng)用

      包含以下使用場景:
      1)領(lǐng)域建模
      2)錯(cuò)誤處理
      3)處理空值
      使用Option這個(gè)容器包裝空值。

      相關(guān)資料

      Railway Oriented Programming
      The "Map and Bind and Apply, Oh my!" series
      強(qiáng)大的容器
      Monad
      Applicative Functor

      多態(tài)

      GADT

      介紹
      全稱為Generalized algebraic data type,可以用來實(shí)現(xiàn)函數(shù)參數(shù)多態(tài)。

      示例
      重構(gòu)前,需要定義多個(gè)isXXXEqual函數(shù)來處理每種類型:

      let isIntEqual = (source: int, target: int) => source == target;
      
      let isStringEqual = (source: string, target: string) => source == target;
        
      //true
      isIntEqual(1, 1);
      
      //true
      isStringEqual("aaa", "aaa");
      

      使用GADT重構(gòu)后,只需要一個(gè)isEqual函數(shù)來處理所有的類型:

      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
        };
      
      //true
      isEqual(Int, 1, 1);
      
      //true
      isEqual(String, "aaa", "aaa");
      

      在引擎中的應(yīng)用

      包含以下使用場景:
      1)契約檢查
      使用GADT定義一個(gè)assertEqual方法來判斷兩個(gè)任意類型的變量是否相等,從而不需要assertStringEqual,assertIntEqual等方法。

      相關(guān)資料
      Why GADTs matter for performance(需要FQ)
      維基百科->Generalized algebraic data type

      Module Functor

      介紹

      module作為參數(shù),傳遞給functor,得到一個(gè)新的module。

      它類似于面向?qū)ο蟮摹袄^承”,可以通過函子functor,在基module上擴(kuò)展出新的module。

      示例

      module type Comparable = {
        type t;
      
        let equal: (t, t) => bool;
      };
      
      //module functor
      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函數(shù),該方法調(diào)用了A.equal函數(shù)
      module B = MakeAdd(A);
      
      //list == [2]
      let list = B.add(1, 2, []);    
      //list == [2]
      let list = list |> B.add(1, 1);
      

      在引擎中的應(yīng)用

      引擎中有些module有相同的模式,可把它們放到提出的基module中,然后通過functor復(fù)用基module。

      相關(guān)資料
      Reason->Module Functions

      類型搭橋

      背景
      想象有兩個(gè)世界:“普通世界”和“提升世界”,“提升世界”與“普通世界”非常像,“普通世界”的所有類型在“提升世界”中都有對應(yīng)的類型。
      例如,“普通世界”有int和string類型,對應(yīng)于“提升世界”就是e(int)和e(string)類型:
      image

      (圖來自Understanding map and apply
      (TODO 把圖中的“<>”改為“()”,“Int”改為“int”,“E”改為“e”)

      同樣的,“普通世界”有類型簽名為“int=>string”的函數(shù),對應(yīng)于“提升世界”就是類型簽名為“e(int)=>e(string)”的函數(shù):
      image

      (圖來自Understanding map and apply

      什么是“提升世界”?
      一個(gè)Discriminated Union類型對應(yīng)一個(gè)“提升世界”。
      如Discriminated Union類型result:

      type result('a, 'b) =
        | Ok('a)
        | Error('b);
      

      它對應(yīng)一個(gè)“提升世界”:“Result世界”。
      “普通世界”的類型int對應(yīng)于“Result世界”的類型為result(int, 'b)。

      又如option對應(yīng)一個(gè)“提升世界”:“Option世界”。
      “普通世界”的類型Int對應(yīng)于“Option世界”的類型為option(int)。

      又如list對應(yīng)一個(gè)“提升世界”:“List世界”。
      “普通世界”的類型int對應(yīng)于“List世界”的類型為list(int)。

      什么是類型搭橋

      兩個(gè)函數(shù)組合時(shí),第一個(gè)函數(shù)的返回會作為第二個(gè)函數(shù)的輸入。如果它們的類型處于不同的世界(如一個(gè)是option(t)類型,另一個(gè)是t類型),那么需要升降類型到同一個(gè)世界,這樣才能組合。對于這個(gè)類型升降的過程,我稱之為“類型搭橋”。

      相關(guān)資料
      The "Map and Bind and Apply, Oh my!" series

      有下面幾種操作來升降類型:

      return

      常用名:return, pure, unit, yield, point

      它做了什么:提升類型到“提升世界”

      類型簽名: a => e(a)

      介紹

      “return”把類型從“普通世界”提升到“提升世界”:
      image

      (圖來自Understanding map and apply

      示例

      //option增加return函數(shù) 
      module Option = {
          ...
          let return = x => Some(x);
      };
      
      
      
      //client code
      
      let func = (opt) => {
          switch(opt){
          | Some(x) => x * 2;
          | None => 0
          }
      };
      
      //a=2
      let a = 1 |> Option.return |> func;
      
      
      

      在引擎中的應(yīng)用

      處理錯(cuò)誤的Result模塊實(shí)現(xiàn)了succeed和fail函數(shù),它們屬于“return”操作:
      Result.re

      type t('a, 'b) =
        | Success('a)
        | Fail('b);
      
      let succeed = x => Success(x);
      
      let fail = x => Fail(x);
      

      如果一個(gè)函數(shù)沒有發(fā)生錯(cuò)誤,則調(diào)用Result.succeed,把返回值包裝在Success中;否則調(diào)用Result.fail,把錯(cuò)誤信息包裝在Fail中。

      相關(guān)資料
      Understanding map and apply

      map

      常用名:map, fmap, lift, Select

      它做了什么:提升函數(shù)到“提升世界”

      類型簽名: (a=>b) => e(a) => e(b)

      介紹

      “map”把函數(shù)從“普通世界”提升到“提升世界”:
      image

      (圖來自Understanding map and apply

      示例

      //option增加map函數(shù)
      module Option = {
          ...
          let map = (func, option) => {
              switch(option){
              | Some(x) => Some(x |> func)
              | None => None
              }
          };
      };
      
      
      
      //client code
      
      //定義在“普通世界”的函數(shù)
      //類型簽名 : int => int
      let add1 = x => x + 1;
      
      //使用map,提升函數(shù)到“Option世界”
      let add1IfSomething = Option.map(add1)
      
      //a=Some(3)
      let a = Some(2) |> add1IfSomething;
      

      在引擎中的應(yīng)用

      處理錯(cuò)誤的Result模塊實(shí)現(xiàn)了map函數(shù):
      Result.re

      type t('a, 'b) =
        | Success('a)
        | Fail('b);
      
      let either = (successFunc, failureFunc, twoTrackInput) =>
        switch (twoTrackInput) {
        | Success(s) => successFunc(s)
        | Fail(f) => failureFunc(f)
        };
        
      let map = (oneTrackFunc, twoTrackInput) =>
        either(result => result |> oneTrackFunc |> succeed, fail, twoTrackInput);
      

      引擎用到Result.map的地方很多,例如可以使用Result.map來處理Success中包含的值,將其轉(zhuǎn)換為另一個(gè)值。
      示例代碼如下:

      let func1 = x => x |> Result.succeed;
      
      let func2 = result => result |> Result.map(x => x * 2);
      
      //a=Success(2)
      let a = 1 |> func1 |> func2;
      

      相關(guān)資料
      Understanding map and apply

      bind

      常用名:bind, flatMap, andThen, collect, SelectMany

      它做了什么:使穿越世界(manadic)的函數(shù)可以被組合

      類型簽名: (a=>e(b)) => e(a) => e(b)

      介紹

      我們經(jīng)常要處理在“普通世界”和“提升世界”穿越的函數(shù)。

      例如:一個(gè)把字符串解析成int的函數(shù),可能會返回option(int)而不是int類型,因此它穿越了“普通世界”和“Option世界”;一個(gè)讀取文件的函數(shù)可能會返回ienumerable(string)類型;一個(gè)接收網(wǎng)頁的函數(shù)可能返回promise(string)類型。

      這種穿越世界的函數(shù),它們的類型簽名可以被識別為:a => e(b)。
      它們的輸入是“普通世界”的類型,而輸出則是“提升世界”的類型。
      不幸的是,這意味著不能直接組合這些函數(shù):
      image

      (圖來自Understanding bind

      “bind”把穿越世界的函數(shù)(通常稱為“monadic”函數(shù))轉(zhuǎn)換為“提升世界”的函數(shù):e(a) => e(b):
      image

      (圖來自Understanding bind

      這么做的好處是,轉(zhuǎn)換后的函數(shù)完全在“提升世界”,因此可以被直接組合。

      例如,一個(gè)類型簽名為“a => e(b)”的函數(shù)不能直接與類型簽名為“b => e(c)”的函數(shù)組合。但當(dāng)bind后者以后,后者的類型簽名變?yōu)椤癳(b) => e(c)”,這樣就可以與前者進(jìn)行組合了:
      image

      (圖來自Understanding bind

      通過bind,能讓多個(gè)mondadic函數(shù)組合在一起。

      示例

      //option增加bind方法
      module Option = {
          ...
          let bind = (func, option) => {
              switch(option){
              | Some(x) => x |> func
              | None => None
              }
          };
      };
      
      
      
      //client code
      
      //類型簽名:string => option(int)
      let parseStr = str => {
          switch(str){
          | "-1" => Some(-1)
          | "0" => Some(0)
          | "1" => Some(1)
          | "2" => Some(2)
          // etc
          | _ => None
          };
      }
      
      type orderQty =
        | OrderQty(int);
      
      //類型簽名:int => option(orderQty)
      let toOrderQty = (qty) =>
        if (qty >= 1) {
          Some(OrderQty(qty));
        } else {
          None;
        };
        
      //使用bind轉(zhuǎn)換toOrderQty函數(shù),使它能與parseInt函數(shù)組合
      //類型簽名:string => option(orderQty)
      let parseOrderQty = str => str |> parseStr |> Option.bind(toOrderQty);
      

      在引擎中的應(yīng)用

      處理錯(cuò)誤的Result模塊實(shí)現(xiàn)了bind函數(shù):
      Result.re

      type t('a, 'b) =
        | Success('a)
        | Fail('b);
      
      let either = (successFunc, failureFunc, twoTrackInput) =>
        switch (twoTrackInput) {
        | Success(s) => successFunc(s)
        | Fail(f) => failureFunc(f)
        };
        
      let bind = (switchFunc, twoTrackInput) =>
        either(switchFunc, fail, twoTrackInput);
      

      引擎用到Result.bind的地方很多,例如:
      在主循環(huán)中,獲得數(shù)據(jù)state的函數(shù)unsafeGetState的類型簽名為:unit => Result.t(state, Js.Exn.t),處理主循環(huán)邏輯的loopBody函數(shù)的類型簽名為:state => Result.t(state, Js.Exn.t)。
      需要對loopBody函數(shù)進(jìn)行Result.bind,從而使它們能夠組合。
      相關(guān)代碼為:

      let rec _loop = () =>
        DomExtend.requestAnimationFrame((time: float) => {
          StateData.unsafeGetState()
          |> Result.bind(Director.loopBody)
          ...
        });
      

      相關(guān)資料
      Understanding bind

      traverse

      常用名:mapM, traverse, for

      它做了什么:轉(zhuǎn)換一個(gè)穿越世界的函數(shù)為另一個(gè)穿越世界的函數(shù),轉(zhuǎn)換后的函數(shù)用來在遍歷集合時(shí)處理每個(gè)元素

      類型簽名:(a=>e(b)) => list(a) => e(list(b))

      介紹

      我們來看看一個(gè)經(jīng)常會遇到的場景:處理一個(gè)集合(如list),該集合元素為“提升世界”的類型。

      舉例來說:
      我們有一個(gè)類型簽名為“string => option(int)”的函數(shù)parseStr和一個(gè)集合:list(string),我們想要解析該集合所有的字符串。我們可以使用“map”:list |> List.map(parseStr),將list(string)轉(zhuǎn)換為list(option(int))。
      但我們真正想要的不是“l(fā)ist(option(int))”,而是“option(list(int))”,即用option來包裝解析后的列表list。
      這可以通過traverse來做到,我們把list對應(yīng)的“提升世界”稱為“Traversable世界”(因?yàn)榭梢允褂胻raverse來遍歷它),把option對應(yīng)的“提升世界”稱為“Applicative世界”。使用“traverse”操作遍歷list,得到option(list('b))類型,如下圖所示:
      image

      示例

      介紹中的示例的相關(guān)代碼如下:

      module List = {
        ...
        //注:可以將該函數(shù)作為模版。如果Applicative世界變?yōu)槠渌澜纾鏡esult世界,只需要把returnFunc替換為Result.succeed,Js.Option.andThen替換為Result.bind
        let traverseOptionM = (traverseFunc, list) => {
          let returnFunc = Js.Option.some;
      
          list
          |> List.fold_left(
               (resultArr, value) =>
                 //andThen相當(dāng)于bind
                 Js.Option.andThen(
                   (. h) =>
                     Js.Option.andThen((. t) => returnFunc(t @ [h]), resultArr),
                   traverseFunc(value),
                 ),
               returnFunc([]),
             );
        };
      };
      
      //client code
      
      //類型簽名:string => option(int)
      let parseStr = str => {
        switch (str) {
        | "" => None
        | str => Some(str |> Js.String.length)
        };
      };
      
      //a=None
      let a = ["", "aaa", "bbbb"] |> List.traverseOptionM(parseStr);
      //b=Some([3,4])
      let b = ["aaa", "bbbb"] |> List.traverseOptionM(parseStr);
      

      關(guān)于traverse具體實(shí)現(xiàn)的分析,詳見Understanding traverse and sequence

      在引擎中的應(yīng)用

      引擎的List、Array實(shí)現(xiàn)了traverseResultM函數(shù),用來遍歷處理集合中Result類型(存在錯(cuò)誤的函數(shù)會返回Result類型)的元素。

      相關(guān)資料
      Understanding traverse and sequence

      sequence

      常用名:sequence

      它做了什么:交換Traversable世界和Applicative世界的位置

      類型簽名:list(e(a)) => e(list(a))

      介紹

      sequence交換Traversable世界和Applicative世界的位置:
      Traversable世界下降
      Applicative世界上升

      如下圖所示:
      image

      示例

      如果已經(jīng)實(shí)現(xiàn)了traverseResultM,那么可以從中很簡單地導(dǎo)出sequence。

      示例代碼為:

      module List = {
        ...
        let id = x => x;
        
        let sequenceResultM = x => traverseResultM(id, x);
      };
      
      //client code
      
      //與traverse示例中的parseStr函數(shù)一樣
      let parseStr = ...
      
      
      let a =
        ["1", "22", "333"]
        //得到[Some(1), Some(2), Some(3)]
        |> List.map(parseStr)
        //得到Some([1,2,3])
        |> List.sequenceResultM;
      
      let b =
        ["", "22", "333"]
        //得到[None, Some(2), Some(3)]
        |> List.map(parseStr)
        //得到None
        |> List.sequenceResultM;
      

      在引擎中的應(yīng)用

      引擎對元組Tuple實(shí)現(xiàn)了sequenceResultM函數(shù),用來交換Tuple和Result的位置。

      相關(guān)資料
      Understanding traverse and sequence

      錯(cuò)誤處理

      使用Result

      介紹

      我們使用Result來處理錯(cuò)誤,它相對于“拋出異常”的錯(cuò)誤處理方式,有下面的優(yōu)點(diǎn):
      1、分離“發(fā)生錯(cuò)誤”和“處理錯(cuò)誤”的邏輯,延遲到后面來統(tǒng)一處理錯(cuò)誤
      2、對流程更加可控,如可以在第一個(gè)錯(cuò)誤發(fā)生后繼續(xù)執(zhí)行后面的程序
      3、顯示地標(biāo)示出錯(cuò)誤
      通過函數(shù)返回Result類型,在類型的編譯檢查時(shí)可確保使用錯(cuò)誤得到了處理

      示例

      首先增加Result.re這個(gè)模塊,在其中定義一個(gè)Discriminated Union類型,使其包含Success和Fail的數(shù)據(jù)(設(shè)置為泛型參數(shù)'a和'b,使其可為任意類型,增加通用型):

      type t('a, 'b) =
        | Success('a)
        | Fail('b);
      

      接著定義return(此處為succeed和fail函數(shù))、map、bind等函數(shù):

      let succeed = x => Success(x);
      
      let fail = x => Fail(x);
      
      let either = (successFunc, failureFunc, twoTrackInput) =>
        switch (twoTrackInput) {
        | Success(s) => successFunc(s)
        | Fail(f) => failureFunc(f)
        };
      
      let bind = (switchFunc, twoTrackInput) =>
        either(switchFunc, fail, twoTrackInput);
        
      let map = (oneTrackFunc, twoTrackInput) =>
        either(result => result |> oneTrackFunc |> succeed, fail, twoTrackInput);
      

      最后定義處理Result的函數(shù):

      let getSuccessValue = (handleFailFunc: 'f => unit, result: t('s, 'f)): 's =>
        switch (result) {
        | Success(s) => s
        | Fail(f) => handleFailFunc(f)
        };
      

      我們來看下如何使用Result處理錯(cuò)誤:

      //func使用Result包裝錯(cuò)誤
      let func = (x) => {
          x > 0 ? Result.succeed(x) : Result.fail(Js.Exn.raiseError("x should > 0"));
      };
      
      //處理錯(cuò)誤的函數(shù):拋出異常
      let throwError: Js.Exn.t => unit = [%raw err => {|
      throw err;
      |}];
      
      //拋出異常
      let a = func(-1) |> Result.getSuccessValue(throwError);
      
      //正常執(zhí)行,b=1
      let b = func(1) |> Result.getSuccessValue(throwError);
      

      在引擎中的應(yīng)用

      引擎使用Result來處理錯(cuò)誤。

      相關(guān)資料
      Railway oriented programming

      參考資料

      用函數(shù)式編程,從0開發(fā)3D引擎和編輯器(二):函數(shù)式編程準(zhǔn)備

      posted @ 2020-01-09 18:20  楊元超  閱讀(1447)  評論(0)    收藏  舉報(bào)
      主站蜘蛛池模板: 精品无码国产污污污免费| 国产麻豆精品手机在线观看| 无码国产偷倩在线播放| 精品偷拍一区二区三区| 国产精品一二三入口播放| 一区二区三区午夜无码视频| 新民市| 国产真人无遮挡免费视频| 亚洲中文字幕精品第一页| 欧美日本中文| 亚洲第一极品精品无码久久| 一亚洲一区二区中文字幕| 国产精品国产三级国快看| 日日躁夜夜躁狠狠久久av| 果冻传媒一区二区天美传媒| 一区二区三区四区亚洲自拍| 饥渴的熟妇张开腿呻吟视频| 美女禁区a级全片免费观看| 成人午夜免费无码视频在线观看| 偷拍专区一区二区三区| 国产精品无码aⅴ嫩草| 男女啪啪免费观看网站| 国产真正老熟女无套内射| 特级做a爰片毛片免费看无码| 久久人与动人物a级毛片| 99国产欧美另类久久久精品| 好紧好湿好黄的视频| 日本高清一区免费中文视频| 亚洲成在人天堂一区二区| 亚洲成人av免费一区| 精品人妻大屁股白浆无码| 中文字幕无码成人免费视频| 亚洲美免无码中文字幕在线| 久久天天躁综合夜夜黑人鲁色| 69人妻精品中文字幕| 亚洲熟妇在线视频观看| 日本一道一区二区视频| 玉溪市| 国产成人精品永久免费视频| 人妻系列无码专区69影院| 扶绥县|