├── .gitignore ├── doc ├── ADT.md ├── ADT_zh-Hant.md ├── Algeff.md ├── ChurchE.md ├── CoC.md ├── CoData.md ├── CoData_zh-Hant.md ├── Continuation.md ├── DepsInj.md ├── EvalStrategy.md ├── GADT.md ├── GADT_zh-Hant.md ├── HKT.md ├── Lambda.md ├── Lifting.md ├── Monad.md ├── Monoid.md ├── Monoid_zh-Hant.md ├── Mu.md ├── Parsec.md ├── ParserM.md ├── PiSigma.md ├── STLC.md ├── ScottE.md ├── StateMonad.md ├── SysFO.md ├── SystemF.md ├── TableDriven.md └── YCombinator.md ├── gen.js ├── html ├── ADT.html ├── Algeff.html ├── ChurchE.html ├── CoC.html ├── CoData.html ├── Continuation.html ├── DepsInj.html ├── EvalStrategy.html ├── GADT.html ├── HKT.html ├── Lambda.html ├── Lifting.html ├── Monad.html ├── Monoid.html ├── Mu.html ├── Parsec.html ├── ParserM.html ├── PiSigma.html ├── STLC.html ├── ScottE.html ├── StateMonad.html ├── SysFO.html ├── SystemF.html ├── TableDriven.html └── YCombinator.html ├── index.html ├── licence ├── package.json ├── readme.md └── readme_zh-Hant.md /.gitignore: -------------------------------------------------------------------------------- 1 | doc/*.png 2 | node_modules 3 | package-lock.json -------------------------------------------------------------------------------- /doc/ADT.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:代数数据类型 2 | 3 | ### By 「玩火」,改写「CWKSC」 4 | 5 | > 前置技能:C# (interface, class) 6 | 7 | ## Product type 积类型 8 | 9 | 积类型是指同时包括多个值的类型,例如 C# 中的 class 包括多个字段: 10 | 11 | 12 | ```csharp 13 | public class Student 14 | { 15 | string name; 16 | int id; 17 | } 18 | ``` 19 | 20 | `Student` 的类型中既有 `string` 类型的值也有 `int` 类型的值,可以表示为 `string` 和 `int` 的「积」,即 `string * int` 21 | 22 | ## Sum type 和类型 23 | 24 | 和类型是指可以是某一些类型之一的类型,在 C# 中可以用继承来表示: 25 | 26 | ```csharp 27 | public interface ISchoolPerson { } 28 | public class Student : ISchoolPerson 29 | { 30 | string name; 31 | int id; 32 | } 33 | public class Teacher : ISchoolPerson 34 | { 35 | string name; 36 | string office; 37 | } 38 | ``` 39 | 40 | `SchoolPerson` 可能是 `Student` 也可能是 `Teacher` ,可以表示为 `Student` 和 `Teacher` 的「和」,即 `String * int + String * String` 。而使用时只需要用 C# 中的 `is` 就能知道当前的 `StudentPerson` 具体是 `Student` 还是 `Teacher` 41 | 42 | ```csharp 43 | ISchoolPerson student = new Student(); 44 | ISchoolPerson teacher = new Teacher(); 45 | 46 | (student is ISchoolPerson) // True 47 | (student is Student) // True 48 | (student is Teacher) // False 49 | 50 | (teacher is ISchoolPerson) // True 51 | (teacher is Student) // False 52 | (teacher is Teacher) // True 53 | ``` 54 | 55 | ## ADT, Algebraic Data Type 代数数据类型 56 | 57 | 由和类型与积类型组合构造出的类型就是代数数据类型,其中代数指的就是和与积的操作 58 | 59 | ### Bool 布尔 60 | 61 | 利用和类型的枚举特性与积类型的组合特性,我们可以构造出 C# 中本来很基础的基础类型,比如枚举布尔的两个量来构造布尔类型: 62 | 63 | ```csharp 64 | public interface Bool { } 65 | public class True : Bool { } 66 | public class False : Bool { } 67 | ``` 68 | 69 | 然后用 `t is True` 就可以用来判定 `t` 作为 `Bool` 的值是不是 `True` 70 | 71 | `enum` 也有相同的效果: 72 | 73 | ```csharp 74 | public enum Bool { True, False } 75 | ``` 76 | 77 | ### Natural number 自然数 78 | 79 | 比如利用 S 的数量表示的自然数: 80 | 81 | ```csharp 82 | public interface Nat { } 83 | public class Z : Nat { } 84 | public class S : Nat 85 | { 86 | public Nat value; 87 | public S(Nat v) { value = v; } 88 | } 89 | ``` 90 | 91 | 这里提一下自然数的皮亚诺构造,一个自然数要么是 0 (也就是上面的 `Z` ) 要么是比它小一的自然数 +1 (也就是上面的 `S` ) ,例如 3 可以用 `new S(new S(new S(new Z))` 来表示 92 | 93 | ```csharp 94 | public static int CountNat(Nat number) 95 | { 96 | int count = 0; 97 | while(!(number is Z)) 98 | { 99 | number = ((S)number).value; 100 | count++; 101 | } 102 | return count; 103 | } 104 | 105 | Nat number = new S(new S(new S(new Z()))); 106 | CountNat(number) // 3 107 | ``` 108 | 109 | ### Linked List 链表 110 | 111 | 再比如链表: 112 | 113 | ```csharp 114 | public interface List { } 115 | public class Nil : List { } 116 | public class Cons : List { 117 | public T value; 118 | public List next; 119 | public Cons(T v, List n) { 120 | value = v; 121 | next = n; 122 | } 123 | } 124 | ``` 125 | 126 | `[1, 3, 4]` 就表示为 `new Cons(1, new Cons(3, new Cons(4, new Nil())))` 127 | 128 | ```csharp 129 | public static void PrintList(List list) 130 | { 131 | Console.Write('['); 132 | while (!(list is Nil)) 133 | { 134 | Cons cons = (Cons)list; 135 | Console.Write(cons.value + ", "); 136 | list = cons.next; 137 | } 138 | Console.Write("Nil]"); 139 | } 140 | 141 | List list = 142 | new Cons(1, new Cons(3, new Cons(4, new Nil()))); 143 | PrintList(list); // [1, 3, 4, Nil] 144 | ``` 145 | 146 | 更奇妙的是代数数据类型对应着数据类型可能的实例数量 147 | 148 | 很显然积类型的实例数量来自各个字段可能情况的组合也就是各字段实例数量相乘,而和类型的实例数量就是各种可能类型的实例数量之和 149 | 150 | 比如 `Bool` 的类型是 `1 + 1 ` 而其实例只有 `True` 和 `False` ,而 `Nat` 的类型是 `1 + 1 + 1 + ...` 其中每一个 1 都代表一个自然数,至于 `List` 的类型就是 `1 + x(1 + x(...))` 也就是 `1 + x^2 + x^3 ...` 其中 `x` 就是 `List` 所存对象的实例数量 151 | 152 | ## 实际运用 153 | 154 | ADT 最适合构造树状的结构,比如解析 JSON 出的结果需要一个聚合数据结构。 155 | 156 | ```csharp 157 | public interface JsonValue { } 158 | public class JsonBool : JsonValue { bool value; } 159 | public class JsonInt : JsonValue { int value; } 160 | public class JsonString : JsonValue { string value; } 161 | public class JsonArray : JsonValue { List value; } 162 | public class JsonMap : JsonValue { Dictionary value; } 163 | ``` 164 | -------------------------------------------------------------------------------- /doc/ADT_zh-Hant.md: -------------------------------------------------------------------------------- 1 | # 十分鐘魔法練習:代數數據類型 2 | 3 | ### By 「玩火」,改寫「CWKSC」 4 | 5 | > 前置技能:C# 基礎 6 | 7 | ## Product type 積類型 8 | 9 | 積類型是指同時包括多個值的類型,例如 C# 中的 class 包括多個字段: 10 | 11 | 12 | ```csharp 13 | public class Student 14 | { 15 | string name; 16 | int id; 17 | } 18 | ``` 19 | 20 | `Student` 的類型中既有 `string` 類型的值也有 `int` 類型的值,可以表示為 `string` 和 `int` 的「積」,即 `string * int` 21 | 22 | ## Sum type 和類型 23 | 24 | 和類型是指可以是某一些類型之一的類型,在 C# 中可以用繼承來表示: 25 | 26 | ```csharp 27 | public interface ISchoolPerson { } 28 | public class Student : ISchoolPerson 29 | { 30 | string name; 31 | int id; 32 | } 33 | public class Teacher : ISchoolPerson 34 | { 35 | string name; 36 | string office; 37 | } 38 | ``` 39 | 40 | `SchoolPerson` 可能是 `Student` 也可能是 `Teacher` ,可以表示為 `Student` 和 `Teacher` 的「和」,即 `String * int + String * String` 。而使用時只需要用 C# 中的 `is` 就能知道當前的 `StudentPerson` 具體是 `Student` 還是 `Teacher` 41 | 42 | ```csharp 43 | ISchoolPerson student = new Student(); 44 | ISchoolPerson teacher = new Teacher(); 45 | 46 | (student is ISchoolPerson) // True 47 | (student is Student) // True 48 | (student is Teacher) // False 49 | 50 | (teacher is ISchoolPerson) // True 51 | (teacher is Student) // False 52 | (teacher is Teacher) // True 53 | ``` 54 | 55 | ## ADT, Algebraic Data Type 代數數據類型 56 | 57 | 由和類型與積類型組合構造出的類型就是代數數據類型,其中代數指的就是和與積的操作 58 | 59 | ### Bool 布爾 60 | 61 | 利用和類型的枚舉特性與積類型的組合特性,我們可以構造出 C# 中本來很基礎的基礎類型,比如枚舉布爾的兩個量來構造布爾類型: 62 | 63 | ```csharp 64 | public interface Bool { } 65 | public class True : Bool { } 66 | public class False : Bool { } 67 | ``` 68 | 69 | 然後用 `t is True` 就可以用來判定 `t` 作為 `Bool` 的值是不是 `True` 70 | 71 | `enum` 也有相同的效果: 72 | 73 | ```csharp 74 | public enum Bool { True, False } 75 | ``` 76 | 77 | ### Natural number 自然數 78 | 79 | 比如利用 S 的數量表示的自然數: 80 | 81 | ```csharp 82 | public interface Nat { } 83 | public class Z : Nat { } 84 | public class S : Nat 85 | { 86 | public Nat value; 87 | public S(Nat v) { value = v; } 88 | } 89 | ``` 90 | 91 | 這裡提一下自然數的皮亞諾構造,一個自然數要么是 0 (也就是上面的 `Z` ) 要么是比它小一的自然數 +1 (也就是上面的 `S` ) ,例如 3 可以用 `new S(new S(new S(new Z))` 來表示 92 | 93 | ```csharp 94 | public static int CountNat(Nat number) 95 | { 96 | int count = 0; 97 | while(!(number is Z)) 98 | { 99 | number = ((S)number).value; 100 | count++; 101 | } 102 | return count; 103 | } 104 | 105 | Nat number = new S(new S(new S(new Z()))); 106 | CountNat(number) // 3 107 | ``` 108 | 109 | ### Linked List 鏈表 / 連結串列 110 | 111 | 再比如鏈表: 112 | 113 | ```csharp 114 | public interface List { } 115 | public class Nil : List { } 116 | public class Cons : List { 117 | public T value; 118 | public List next; 119 | public Cons(T v, List n) { 120 | value = v; 121 | next = n; 122 | } 123 | } 124 | ``` 125 | 126 | `[1, 3, 4]` 就表示為 `new Cons(1, new Cons(3, new Cons(4, new Nil())))` 127 | 128 | ```csharp 129 | public static void PrintList(List list) 130 | { 131 | Console.Write('['); 132 | while (!(list is Nil)) 133 | { 134 | Cons cons = (Cons)list; 135 | Console.Write(cons.value + ", "); 136 | list = cons.next; 137 | } 138 | Console.Write("Nil]"); 139 | } 140 | 141 | List list = 142 | new Cons(1, new Cons(3, new Cons(4, new Nil()))); 143 | PrintList(list); // [1, 3, 4, Nil] 144 | ``` 145 | 146 | 更奇妙的是代數數據類型對應著數據類型可能的實例數量 147 | 148 | 很顯然積類型的實例數量來自各個字段可能情況的組合也就是各字段實例數量相乘,而和類型的實例數量就是各種可能類型的實例數量之和 149 | 150 | 比如 `Bool` 的類型是 `1 + 1 ` 而其實例只有 `True` 和 `False` ,而 `Nat` 的類型是 `1 + 1 + 1 + ...` 其中每一個1 都代表一個自然數,至於 `List` 的類型就是 `1 + x(1 + x(...))` 也就是 `1 + x^2 + x^3 ...` 其中 `x` 就是 `List` 所存對象的實例數量 151 | 152 | ## 實際運用 153 | 154 | ADT 最適合構造樹狀的結構,比如解析 JSON 出的結果需要一個聚合數據結構。 155 | 156 | ```csharp 157 | public interface JsonValue { } 158 | public class JsonBool : JsonValue { bool value; } 159 | public class JsonInt : JsonValue { int value; } 160 | public class JsonString : JsonValue { string value; } 161 | public class JsonArray : JsonValue { List value; } 162 | public class JsonMap : JsonValue { Dictionary value; } 163 | ``` -------------------------------------------------------------------------------- /doc/Algeff.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:代数作用 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:Java基础,续延 6 | 7 | ## 可恢复异常 8 | 9 | 有时候我们希望在异常抛出后经过保存异常信息再跳回原来的地方继续执行。 10 | 11 | 显然Java默认异常处理无法直接实现这样的需求,因为在异常抛出时整个调用栈的信息全部被清除了。 12 | 13 | 但如果我们有了异常抛出时的续延那么可以同时抛出,在 `catch` 块中调用这个续延就能恢复之前的执行状态。 14 | 15 | 下面是实现可恢复异常的 `try-catch` : 16 | 17 | ```java 18 | Stack> 19 | cs = new Stack<>(); 20 | 21 | void Try( 22 | Consumer body, 23 | TriConsumer 26 | handler, 27 | Runnable cont) { 28 | 29 | cs.push((e, c) -> 30 | handler.accept(e, cont, c)); 31 | body.accept(cont); 32 | cs.pop(); 33 | } 34 | 35 | void Throw(Exception e, Runnable cont) { 36 | cs.peek().accept(e, cont); 37 | } 38 | ``` 39 | 40 | 然后就可以像下面这样使用: 41 | 42 | ```java 43 | void test(int t) { 44 | Try( 45 | cont -> { 46 | System.out.println("try"); 47 | if (t == 0) 48 | Throw( 49 | new ArithmeticException(), 50 | () -> { 51 | System.out.println( 52 | "resumed"); 53 | cont.run(); 54 | }); 55 | else { 56 | System.out.println(100 / t); 57 | cont.run(); 58 | } 59 | }, 60 | (e, cont, resume) -> { 61 | System.out.println("catch"); 62 | resume.run(); 63 | }, 64 | () -> System.out.println("final")); 65 | } 66 | ``` 67 | 68 | 而调用 `test(0)` 就会得到: 69 | 70 | ``` 71 | try 72 | catch 73 | resumed 74 | final 75 | ``` 76 | 77 | ## 代数作用 78 | 79 | 如果说在刚刚异常恢复的基础上希望在恢复时修补之前的异常错误就需要把之前的 `resume` 函数加上参数,这样修改以后它就成了代数作用(Algebaic Effect)的基础工具: 80 | 81 | ```java 82 | Stack>> 84 | cs = new Stack<>(); 85 | 86 | void Try( 87 | Consumer body, 88 | TriConsumer> 91 | handler, 92 | Runnable cont) { 93 | 94 | cs.push((e, c) -> 95 | handler.accept(e, cont, c)); 96 | body.accept(cont); 97 | cs.pop(); 98 | } 99 | 100 | void Perform(Object e, 101 | Consumer cont) { 102 | cs.peek().accept(e, cont); 103 | } 104 | ``` 105 | 106 | 使用方式如下: 107 | 108 | ```java 109 | void test(int t) { 110 | Try( 111 | cont -> { 112 | System.out.println("try"); 113 | if (t == 0) 114 | Perform( 115 | new ArithmeticException(), 116 | v -> { 117 | System.out.println( 118 | "resumed"); 119 | System.out.println( 120 | 100 / (Integer) v); 121 | cont.run(); 122 | }); 123 | else { 124 | System.out.println(100 / t); 125 | cont.run(); 126 | } 127 | }, 128 | (e, cont, resume) -> { 129 | System.out.println("catch"); 130 | resume.accept(1); 131 | }, 132 | () -> System.out.println("final")); 133 | } 134 | ``` 135 | 136 | 而这个东西能实现不只是异常的功能,从某种程度上来说它能跨越函数发生作用(Perform Effect)。 137 | 138 | 比如说现在有个函数要记录日志,但是它并不关心如何记录日志,输出到标准流还是写入到文件或是上传到数据库。这时候它就可以调用 139 | 140 | ```java 141 | Perform(new LogIt(INFO, "test"), ...); 142 | ``` 143 | 144 | 来发生(Perform)一个记录日志的作用(Effect)然后再回到之前调用的位置继续执行,而具体这个作用产生了什么效果就由调用这个函数的人实现的 `try` 中的 `handler` 决定。这样发生作用和执行作用(Handle Effect)就解耦了。 145 | 146 | 进一步讲,发生作用和执行作用是可组合的。对于需要发生记录日志的作用,可以预先写一个输出到标准流的的执行器(Handler)一个输出到文件的执行器然后在调用函数的时候按需组合。这也就是它是代数的(Algebiac)的原因。 147 | 148 | 细心的读者还会发现这个东西还能跨函数传递数据,在需要某个量的时候调用 149 | 150 | ```java 151 | Perform(new Ask("config"), ...); 152 | ``` 153 | 154 | 就可以获得这个量而不用关心这个量是怎么来的,内存中来还是读取文件或者 HTTP 拉取。从而实现获取和使用的解耦。 155 | 156 | 而且这样的操作和状态单子非常非常像,实际上它就是和相比状态单子来说没有修改操作的读取器单子(Reader Monad)同构。 157 | 158 | 也就是说把执行器函数作为读取器单子的状态并在发生作用的时候执行对应函数就可以达到和用续延实现的代数作用相同的效果,反过来也同样可以模拟。 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /doc/ChurchE.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:丘奇编码 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:λ演算 6 | 7 | ## Intro 8 | 9 | 众所周知, λ 演算是一个图灵完备的计算模型,它能计算任何图灵机能算的东西。那么很显然它也能表示任何我们平时所用的 C 、 Java 能表示的数据结构。虽然这听起来挺不可思议的,毕竟 λ 演算中本身只有变量、函数定义、函数应用三种结构。 10 | 11 | 信息的编码大概是计算机科学中最为接近魔法的内容,凝结了最强的人类的智慧结晶。同一个量的不同表现形式,同构、抽象与组合都让人感到惊叹不已。 12 | 13 | 为了方便起见,这里引入一个语法糖 let 绑定(let-binding)来**命名**表达式: 14 | 15 | ``` 16 | x = E 17 | ...后续代码 18 | ``` 19 | 20 | 它解糖(Desugar)后等价于: 21 | 22 | ``` 23 | (λ x. ...后续代码) E 24 | ``` 25 | 26 | ## 布尔 27 | 28 | 通常来说丘奇编码(Church Encoding)的布尔表达为: 29 | 30 | ``` 31 | true = λ x. λ y. x 32 | false = λ x. λ y. y 33 | ``` 34 | 35 | 理论上这两个量的定义互相替换后和这种表达也是同构的,不过通常来说大家约定这种表示因为它更符合直觉。 36 | 37 | 实际上定义了布尔以后并不需要定义 if ,布尔量本身就可以接替 if 的作用,只需要将 if 的两个分支应用上去: 38 | 39 | ``` 40 | (boolValue thenTodo elseTodo) 41 | ``` 42 | 43 | 如果`boolValue`是`true`那么求值就会得到`thenTodo`否则会得到`elseTodo`。 44 | 45 | 我们不需要 if ,这很神奇。不过为了语义考虑也可以定义一个没有实际意义的 if : 46 | 47 | ``` 48 | if = λ x. λ a. λ b. (x a b) 49 | ``` 50 | 51 | 这样 `if true a b` 就可以得到 `a` , `if false a b` 就可以得到 `b` 。 52 | 53 | ## 自然数 54 | 55 | 皮亚诺构造(Peano Construct)是目前普遍使用的自然数定义。简单来说, 0 用 Z 表示, n 用 n 个 S 和一个 Z 表示。比如 3 就是 SSSZ 。而皮亚诺构造的加法就相当于把一个数的 Z 换成另一个数,就比如 3+3 就是 SSS(SSSZ) 。乘法就相当于把一个数的每个 S 换成另一个数的 S 部分,比如 3*3 就是 (SSS)(SSS)(SSS)Z 。 56 | 57 | 而这在 λ 演算中可以表示为: 58 | 59 | ``` 60 | 0 = λ f. λ x. x 61 | 3 = λ f. λ x. f (f (f x)) 62 | ``` 63 | 64 | 这样的表示方法叫丘奇数(Church number),非常类似于皮亚诺构造。实际上,它是和皮亚诺构造同构的。 65 | 66 | 丘奇数的加法和乘法很简单,加法只需要把 x 替换成另一个数就好了,乘法只需要把f替换成另一个数就好了: 67 | 68 | ``` 69 | + = λ m. λ n. (λ f. λ x. m f (n f x)) 70 | * = λ m. λ n. (λ f. λ x. m (n f) x) 71 | ``` 72 | 73 | 而某种程度上来说,一个自然数就是固定次数的循环,以 x 为初始值,把 f 循环执行 n 遍。比如 m*n 就相当于把 m 循环累加加 n 次。 74 | 75 | 我们不需要 for ,这很神奇。 76 | 77 | ## 元组 78 | 79 | 终于到了数据结构部分, λ 表达式保存数据的原理是把参数全部放在一个接受一个提取器的函数里面: 80 | 81 | ``` 82 | pair = λ a. λ b. λ f. f a b 83 | first = λ p. p (λ x. λ y. x) 84 | second = λ p. p (λ x. λ y. y) 85 | ``` 86 | 87 | 这样就可以保证 `first (pair x y)` 始终等于 `x` 而 `second (pair x y)` 始终等于 `y` 。其中 `λ x. λ y. x` 和 `λ x. λ y. y` 就是提取器函数。 88 | 89 | 进一步讲,把元组串起来就可以变成列表,比如: 90 | 91 | ``` 92 | list' = pair a1 (pair a2 (pair a3 ...)) 93 | ``` 94 | 95 | 而如果列表分叉就成了树: 96 | 97 | ``` 98 | tree' = pair (pair a1 a2) (pair a3 a4) 99 | ``` 100 | 101 | 我们用函数构造出了数据结构,这很神奇。 -------------------------------------------------------------------------------- /doc/CoC.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:构造演算 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:Java 基础,ADT,系统 F ω 6 | 7 | ## λ 立方 8 | 9 | 表达式非类型部分叫做项(Term),类型部分叫做类型(Type),其中类型的类型为种类(Kind)也写作 `*` 而种类的类型写作 `□`。表达式中的函数结构为 `λ x: A. (M: B)` ,如果记 `A` 的类型为 `S1` , `B` 的类型为 `S2` ,那么可以得到一个对子 `(S1, S2)` 。 10 | 11 | 简单类型 λ 演算中项和类型是分离的,其中只有针对项的函数,它接收一个项返回另一个项,其得到的对子是 `(*, *)` 。系统 F 对类型系统进行了扩充,在项中增加了一种函数,接收一个类型返回一个项,其得到的对子是 `(□, *)` 。而系统 F ω 进一步增加了接收类型返回类型的函数,也就是 `(□, □)` 。那么可以想象应该还存在一类函数接收一个值产生一个类型,其得到的对子应该是 `(*, □)` 。 12 | 13 | 对于所有 λ 演算都存在 `(*, *)` 的函数,而另外三种不同函数是三种额外的特性,可以自由组合来构造新的类型系统,一共能组合出六种不同的类型系统: 14 | 15 | ``` 16 | ω ------ C 17 | / | / | 18 | 2 ------ P2 | 19 | | _ω --|- _Pω 20 | | / | / 21 | → ------ P 22 | ``` 23 | 24 | 左下角的 λ→ 就是简单类型 λ 演算,和它相连的三条边对应在其基础上分别添加了三种不同函数的 λ 演算。 λ2 就是系统 F ,包含 `(□, *)` 函数。 λω 就是去除了系统 F 对应特性的系统 F ω ,也叫系统 F ω 。右下的 λP 就是在简单类型 λ 中加入了 `(*, □)` 的 λ 演算,而这样的类型系统中类型依赖值所以也叫依赖类型系统(Dependent Type System),在 C++ 中模板可以有值参数所以实际上 C++ 的类型系统中包括依赖类型(Dependent Type)。 25 | 26 | 这个立方体就被称为 λ 立方(Lambda Cube)。 27 | 28 | ## 构造演算 29 | 30 | 在 λ 立方的顶端放着 λC ,也叫构造演算(Calculus of Construction, CoC)。在构造演算中类型可以作为函数的输入,也可以作为函数的输出,那么实际上我们可以把项和函数作为相同的东西,不再加以区分。这样四种不同的函数也可以不加以区分放在一起,同时加入类别(Sort)来表达类型和类型的类型。而且因为 `A → B` 等价于 `∀ _: A. B` ,那么系统 F ω 中的 `TForall` 和 `TArr` 也可以合并。这样 CoC 的语法树表示如下: 31 | 32 | ```java 33 | interface Expr { 34 | Expr genUUID(); 35 | Expr applyUUID(Val v); 36 | 37 | Expr reduce(); 38 | Expr fullReduce(); 39 | Expr apply(Val v, Expr e); 40 | 41 | Expr checkType(Env env) throws BadTypeException; 42 | } 43 | 44 | class Sort implements Expr { 45 | int x; // 1 为 * , 2 为 □ 46 | } 47 | 48 | class Val implements Expr { 49 | String x; 50 | UUID id; 51 | Expr t; // 类型 52 | } 53 | 54 | class Fun implements Expr { 55 | Val x; 56 | Expr e; 57 | } 58 | 59 | class App implements Expr { 60 | Expr f, x; 61 | } 62 | // Forall, Arrow 63 | class Pi implements Expr { 64 | Val x; 65 | Expr e; 66 | } 67 | ``` 68 | 69 | 其中 `Expr` 的接口的函数被分成了三组,第一组是预生成 `id` 只需要简单递归生成就可以,之前也展示过;第二组是对表达式的化简,只需注意 `App` 在化简时只应用 `Fun` 应该忽略 `Pi` ,并且递归化简时别忘了变量部分的类型也是一个表达式;第三组就是类型检查部分了, `Fun` 的类型是 `Pi` , `Pi` 的类型是 `e` 的类型, `App` 把表达式应用到 `Pi` 上: 70 | 71 | ```java 72 | class Sort implements Expr { 73 | public Expr checkType(Env env) { 74 | return new Sort(x + 1); 75 | } 76 | } 77 | 78 | class Val implements Expr { 79 | public Expr checkType(Env env) throws BadTypeException { 80 | if (t == null) return env.lookup(id); 81 | return t; 82 | } 83 | } 84 | 85 | class Fun implements Expr { 86 | public Expr checkType(Env env) throws BadTypeException { 87 | Expr pi = new Pi(x, e.checkType(new ConsEnv(x, env))); 88 | if (pi.checkType(env) instanceof Sort) 89 | return pi; 90 | throw new BadTypeException(); 91 | } 92 | } 93 | 94 | class App implements Expr { 95 | public Expr checkType(Env env) throws BadTypeException { 96 | Expr tf = f.checkType(env); 97 | if (tf instanceof Pi) { 98 | Pi pi = (Pi) tf; 99 | if (x.checkType(env).fullReduce().equals( 100 | pi.x.checkType(env).fullReduce())) 101 | return pi.e.apply(pi.x, x); 102 | } 103 | throw new BadTypeException(); 104 | } 105 | } 106 | 107 | class Pi implements Expr { 108 | public Expr checkType(Env env) throws BadTypeException { 109 | Expr ta = x.t.checkType(env); // x.t 的类型 110 | Expr tb = e.checkType(new ConsEnv(x, env)); 111 | if (ta instanceof Sort && tb instanceof Sort) { 112 | return tb; 113 | } 114 | throw new BadTypeException(); 115 | } 116 | } 117 | ``` 118 | 119 | 所以实际上 `Pi` 就是一个类型检查期的标识,并不参与最终值的演算。因为不区分值和类型,其中 `Env` 保存的内容改为 `Val` ,并且 `lookup` 改为用 `UUID` 检索。 120 | 121 | 这样就构造出了一个相当强大的类型系统,它的表现力已经超越了几乎所有常见语言的类型系统。之后将会介绍如何利用这个强大的类型系统表达复杂的类型,做一些常见类型系统做不到的事情。 -------------------------------------------------------------------------------- /doc/CoData.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:余代数数据类型 2 | 3 | ### By 「玩火」,改写「CWKSC」 4 | 5 | > 前置技能:C# (Func, delegate),[ADT](ADT.md) 6 | 7 | ## ADT 的局限性 8 | 9 | 很显然, ADT 可以构造任何树形的数据结构:树的节点内分支用和类型连接,层级间节点用积类型连接。 10 | 11 | 但是同样很显然 ADT 并不能搞出环形的数据结构或者说是无穷大小的数据结构。比如下面的代码: 12 | 13 | ```csharp 14 | using IntList = ADT.List; 15 | using IntNil = ADT.Nil; 16 | using IntCons = ADT.Cons; 17 | 18 | IntList list = new IntCons(4, new IntCons(2, new IntNil())); 19 | ADT.PrintList(list); // [4, 2, Nil] 20 | 21 | // CS0165 使用未指派的区域变数 'list' 22 | IntList list = new IntCons(1, list); 23 | ^^^^ 24 | ``` 25 | 26 | 编译器会表示 `list` 在使用时未初始化。 27 | 28 | 为什么会这样呢? ADT 是归纳构造的,也就是说它必须从非递归的基本元素开始组合构造成更大的元素。 29 | 30 | 如果我们去掉这些基本元素那就没法凭空构造大的元素,也就是说如果去掉归纳的第一步那整个归纳过程毫无意义。 31 | 32 | ## 余代数数据类型 33 | 34 | 余代数数据类型(Coalgebraic Data Type)也就是余归纳数据类型(Coinductive Data Type),代表了自顶向下的数据类型构造思路,思考一个类型可以如何被分解从而构造数据类型。 35 | 36 | 这样在分解过程中再次使用自己这个数据类型本身就是一件非常自然的事情了。 37 | 38 | 不过在编程实现过程中使用自己需要加个惰性数据结构包裹,防止积极求值的语言无限递归生成数据。 39 | 40 | 比如一个列表可以被分解为第一项和剩余的列表: 41 | 42 | ```csharp 43 | public class InfIntList 44 | { 45 | public int head; 46 | public Func next; 47 | public InfIntList(int head, Func next) 48 | { 49 | this.head = head; 50 | this.next = next; 51 | } 52 | } 53 | ``` 54 | 55 | 这里的 `Func` 可以做到仅在需要 `next` 的时候才求值。使用的例子如下: 56 | 57 | ```csharp 58 | public static InfIntList InfAlt() 59 | { 60 | return new InfIntList(1, 61 | () => new InfIntList(2, 62 | InfAlt)); 63 | } 64 | 65 | Console.WriteLine(InfAlt().head); // 1 66 | Console.WriteLine(InfAlt().next().head); // 2 67 | Console.WriteLine(InfAlt().next().next().head); // 1 68 | Console.WriteLine(InfAlt().next().next().next().head); // 2 69 | Console.WriteLine(InfAlt().next().next().next().next().head); // 1 70 | ``` 71 | 72 | 这里的 `infAlt` 从某种角度来看实际上就是个长度为 2 的环形结构。 73 | 74 | 用这样的思路可以构造出无限大的树、带环的图等数据结构。 75 | 76 | 不过以上都是对余代数数据类型的一种模拟,实际上在对其支持良好的语言都会自动加上 `Func` 来辅助构造,同时还能处理好对无限大(其实是环)的数据结构的无限递归变换(`map`, `fold` ...)的操作。 77 | 78 | ## 无限数据结构的其他实现: 79 | 80 | 在 C# 中,可以定义递归的函数类型 81 | 82 | 对于环形结构,可以这样定义: 83 | 84 | ```csharp 85 | public delegate (T value, InfRing next) InfRing(); 86 | public static (int value, InfRing next) threeLengthRing() => 87 | (1, () => (2, () => (3, threeLengthRing))); 88 | ``` 89 | 90 | ```csharp 91 | Console.WriteLine(threeLengthRing().value); // 1 92 | Console.WriteLine(threeLengthRing().next().value); // 2 93 | Console.WriteLine(threeLengthRing().next().next().value); // 3 94 | Console.WriteLine(threeLengthRing().next().next().next().value); // 1 95 | Console.WriteLine(threeLengthRing().next().next().next().next().value); // 2 96 | Console.WriteLine(threeLengthRing().next().next().next().next().next().value); // 3 97 | 98 | 1 --> 2 --> 3 99 | ^ | 100 | | | 101 | +-----------+ 102 | ``` 103 | 104 | 有向树/图: 105 | 106 | ```csharp 107 | public delegate (T value, List> nexts) InfTree(); 108 | public static (int value, List> nexts) tree(int x) => 109 | (x, new List>{ 110 | () => tree(x += 1), 111 | () => tree(x += 2) }); 112 | ``` 113 | 114 | ```csharp 115 | Console.WriteLine(tree(1).value); // 1 116 | Console.WriteLine(tree(1).nexts[0]().value); // 2 117 | Console.WriteLine(tree(1).nexts[1]().value); // 3 118 | Console.WriteLine(tree(1).nexts[0]().nexts[0]().value); // 3 119 | Console.WriteLine(tree(1).nexts[0]().nexts[1]().value); // 4 120 | Console.WriteLine(tree(1).nexts[1]().nexts[0]().value); // 4 121 | Console.WriteLine(tree(1).nexts[1]().nexts[1]().value); // 5 122 | 123 | 1 124 | / \ 125 | / \ 126 | 2 3 127 | / \ / \ 128 | 3 4 4 5 129 | / \ / \ / \ / \ 130 | ... ... ... ... ... ... ... 131 | 132 | x 133 | / \ 134 | / \ 135 | x + 1 x + 2 136 | / \ / \ 137 | / \ / \ 138 | ... ...... ... 139 | ``` -------------------------------------------------------------------------------- /doc/CoData_zh-Hant.md: -------------------------------------------------------------------------------- 1 | # 十分鐘魔法練習:餘代數數據類型 2 | 3 | ### By 「玩火」,改寫「CWKSC」 4 | 5 | > 前置技能:C# 基礎,[ADT](ADT_zh_Hant.md) 6 | 7 | ## ADT 的局限性 8 | 9 | 很顯然, ADT 可以構造任何樹形的數據結構:樹的節點內分支用和類型連接,層級間節點用積類型連接。 10 | 11 | 但是同樣很顯然 ADT 並不能搞出環形的數據結構或者說是無窮大小的數據結構。比如下面的代碼: 12 | 13 | ```csharp 14 | using IntList = ADT.List; 15 | using IntNil = ADT.Nil; 16 | using IntCons = ADT.Cons; 17 | 18 | IntList list = new IntCons(4, new IntCons(2, new IntNil())); 19 | ADT.PrintList(list); // [4, 2, Nil] 20 | 21 | // CS0165 使用未指派的區域變數 'list' 22 | IntList list = new IntCons(1, list); 23 | ^^^^ 24 | ``` 25 | 26 | 編譯器會表示 `list` 在使用時未初始化。 27 | 28 | 為什麼會這樣呢? ADT 是歸納構造的,也就是說它必須從非遞歸的基本元素開始組合構造成更大的元素。 29 | 30 | 如果我們去掉這些基本元素那就沒法憑空構造大的元素,也就是說如果去掉歸納的第一步那整個歸納過程毫無意義。 31 | 32 | ## 餘代數數據類型 33 | 34 | 餘代數數據類型(Coalgebraic Data Type)也就是餘歸納數據類型(Coinductive Data Type),代表了自頂向下的數據類型構造思路,思考一個類型可以如何被分解從而構造數據類型。 35 | 36 | 這樣在分解過程中再次使用自己這個數據類型本身就是一件非常自然的事情了。 37 | 38 | 不過在編程實現過程中使用自己需要加個惰性數據結構包裹,防止積極求值的語言無限遞歸生成數據。 39 | 40 | 比如一個列表可以被分解為第一項和剩餘的列表: 41 | 42 | ```csharp 43 | public class InfIntList 44 | { 45 | public int head; 46 | public Func next; 47 | public InfIntList(int head, Func next) 48 | { 49 | this.head = head; 50 | this.next = next; 51 | } 52 | } 53 | ``` 54 | 55 | 這裡的 `Func` 可以做到僅在需要 `next` 的時候才求值。使用的例子如下: 56 | 57 | ```csharp 58 | public static InfIntList InfAlt() 59 | { 60 | return new InfIntList(1, 61 | () => new InfIntList(2, 62 | InfAlt)); 63 | } 64 | 65 | Console.WriteLine(InfAlt().head); // 1 66 | Console.WriteLine(InfAlt().next().head); // 2 67 | Console.WriteLine(InfAlt().next().next().head); // 1 68 | Console.WriteLine(InfAlt().next().next().next().head); // 2 69 | Console.WriteLine(InfAlt().next().next().next().next().head); // 1 70 | ``` 71 | 72 | 這裡的 `infAlt` 從某種角度來看實際上就是個長度為 2 的環形結構。 73 | 74 | 用這樣的思路可以構造出無限大的樹、帶環的圖等數據結構。 75 | 76 | 不過以上都是對余代數數據類型的一種模擬,實際上在對其支持良好的語言都會自動加上`Func` 來輔助構造,同時還能處理好對無限大(其實是環)的數據結構的無限遞歸變換(`map`, `fold` ...)的操作。 77 | 78 | ## 無限數據結構的其他實現: 79 | 80 | 在 C# 中,可以定義遞歸的函數類型 81 | 82 | 對於環形結構,可以這樣定義: 83 | 84 | ```csharp 85 | public delegate (T value, InfRing next) InfRing(); 86 | public static (int value, InfRing next) threeLengthRing() => 87 | (1, () => (2, () => (3, threeLengthRing))); 88 | ``` 89 | 90 | ```csharp 91 | Console.WriteLine(threeLengthRing().value); // 1 92 | Console.WriteLine(threeLengthRing().next().value); // 2 93 | Console.WriteLine(threeLengthRing().next().next().value); // 3 94 | Console.WriteLine(threeLengthRing().next().next().next().value); // 1 95 | Console.WriteLine(threeLengthRing().next().next().next().next().value); // 2 96 | Console.WriteLine(threeLengthRing().next().next().next().next().next().value); // 3 97 | 98 | 1 --> 2 --> 3 99 | ^ | 100 | | | 101 | +-----------+ 102 | ``` 103 | 104 | 有向樹/圖: 105 | 106 | ```csharp 107 | public delegate (T value, List> nexts) InfTree(); 108 | public static (int value, List> nexts) tree(int x) => 109 | (x, new List>{ 110 | () => tree(x += 1), 111 | () => tree(x += 2) }); 112 | ``` 113 | 114 | ```csharp 115 | Console.WriteLine(tree(1).value); // 1 116 | Console.WriteLine(tree(1).nexts[0]().value); // 2 117 | Console.WriteLine(tree(1).nexts[1]().value); // 3 118 | Console.WriteLine(tree(1).nexts[0]().nexts[0]().value); // 3 119 | Console.WriteLine(tree(1).nexts[0]().nexts[1]().value); // 4 120 | Console.WriteLine(tree(1).nexts[1]().nexts[0]().value); // 4 121 | Console.WriteLine(tree(1).nexts[1]().nexts[1]().value); // 5 122 | 123 | 1 124 | / \ 125 | / \ 126 | 2 3 127 | / \ / \ 128 | 3 4 4 5 129 | / \ / \ / \ / \ 130 | ... ... ... ... ... ... ... 131 | 132 | x 133 | / \ 134 | / \ 135 | x + 1 x + 2 136 | / \ / \ 137 | / \ / \ 138 | ... ...... ... 139 | ``` -------------------------------------------------------------------------------- /doc/Continuation.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:续延 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:简单Java基础 6 | 7 | ## 续延 8 | 9 | 续延(Continuation)是指代表一个程序未来的函数,其参数是一个程序过去计算的结果。 10 | 11 | 比如对于这个程序: 12 | 13 | ```java 14 | void test() { 15 | int i = 1; // 1 16 | i++; // 2 17 | System.out.println(i); // 3 18 | } 19 | ``` 20 | 21 | 它第二行以及之后的续延就是: 22 | 23 | ```java 24 | void cont(int i) { 25 | i++; // 2 26 | System.out.println(i); // 3 27 | } 28 | ``` 29 | 30 | 而第三行之后的续延是: 31 | 32 | ```java 33 | void cont(int i) { 34 | System.out.println(i); // 3 35 | } 36 | ``` 37 | 38 | 实际上可以把这整个程序的每一行改成一个续延然后用函数调用串起来变成和刚才的程序一样的东西: 39 | 40 | ```java 41 | void cont1() { 42 | int i = 1; // 1 43 | cont2(i); 44 | } 45 | void cont2(int i) { 46 | i++; // 2 47 | cont3(i); 48 | } 49 | void cont3(int i) { 50 | System.out.println(i); // 3 51 | } 52 | void test() { 53 | cont1(); 54 | } 55 | ``` 56 | 57 | ## 续延传递风格 58 | 59 | 续延传递风格(Continuation-Passing Style, CPS)是指把程序的续延作为函数的参数来获取函数返回值的编程思路。 60 | 61 | 听上去很难理解,把上面的三个 `cont` 函数改成CPS就很好理解了: 62 | 63 | ```java 64 | void logic1(Consumer f) { 65 | int i = 1; 66 | f.accept(i); // return i 67 | } 68 | void logic2(int i, Consumer f) { 69 | i++; 70 | f.accept(i); 71 | } 72 | void logic3(int i, Consumer f) { 73 | System.out.println(i); 74 | f.accept(i); 75 | } 76 | void test() { 77 | logic1( // 获取返回值 i 78 | i -> logic2(i, 79 | i -> logic3(i, 80 | i -> {}))); 81 | } 82 | ``` 83 | 84 | 每个 `logic` 函数的最后一个参数 `f` 就是整个程序的续延,而在每个函数的逻辑结束后整个程序的续延也就是未来会被调用。而 `test` 函数把整个程序组装起来。 85 | 86 | 小朋友,你有没有觉得最后的 `test` 函数写法超眼熟呢?实际上这个写法就是 Monad 的写法, Monad 的写法就是 CPS 。 87 | 88 | 另一个角度来说,这也是回调函数的写法,每个 `logic` 函数完成逻辑后调用了回调函数 `f` 来完成剩下的逻辑。实际上,异步回调思想很大程度上就是 CPS 。 89 | 90 | ## 有界续延 91 | 92 | 考虑有另一个函数 `callT` 调用了 `test` 函数,如: 93 | 94 | ```java 95 | void callT() { 96 | test(); 97 | System.out.println(3); 98 | } 99 | ``` 100 | 101 | 那么对于 `logic` 函数来说调用的 `f` 这个续延并不包括 `callT` 中的打印语句,那么实际上 `f` 这个续延并不是整个函数的未来而是 `test` 这个函数局部的未来。 102 | 103 | 这样代表局部程序的未来的函数就叫有界续延(Delimited Continuation)。 104 | 105 | 实际上在大多时候用的比较多的还是有界续延,因为在 Java 中获取整个程序的续延还是比较困难的,这需要全用 CPS 的写法。 106 | 107 | ## 异常 108 | 109 | 拿到了有界续延我们就能实现一大堆控制流魔法,这里拿异常处理举个例子,通过CPS写法自己实现一个 `try-throw` 。 110 | 111 | 首先最基本的想法是把每次调用 `try` 的 `catch` 函数保存起来,由于 `try` 可层层嵌套所以每次压入栈中,然后 `throw` 的时候将最近的 `catch` 函数取出来调用即可: 112 | 113 | ```java 114 | Stack> cs = 115 | new Stack<>(); 116 | 117 | void Try( 118 | Consumer body, 119 | BiConsumer 120 | handler, 121 | Runnable cont) { 122 | 123 | cs.push(e -> handler.accept(e, cont)); 124 | body.accept(cont); 125 | cs.pop(); 126 | } 127 | 128 | void Throw(Exception e) { 129 | cs.peek().accept(e); 130 | } 131 | ``` 132 | 133 | 这里 `body` 、 `Try` 、 `handler` 的最后一个参数都是这个程序的有界续延。 134 | 135 | 有了 `try-throw` 就可以按照CPS风格调用它们来达到处理异常的目的: 136 | 137 | ```java 138 | void test(int t) { 139 | Try( 140 | cont -> { 141 | System.out.println("try"); 142 | if (t == 0) Throw( 143 | new ArithmeticException()); 144 | else { 145 | System.out.println(100 / t); 146 | cont.run(); 147 | } 148 | }, 149 | (e, cont) -> { 150 | System.out.println("catch"); 151 | cont.run(); 152 | }, 153 | () -> System.out.println("final")); 154 | } 155 | ``` 156 | 157 | 调用 `test(0)` 会得到: 158 | 159 | ``` 160 | try 161 | catch 162 | final 163 | ``` 164 | 165 | 而调用 `test(1)` 会得到: 166 | 167 | ``` 168 | try 169 | 100 170 | final 171 | ``` 172 | 173 | 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /doc/DepsInj.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:依赖注入 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:Java基础,Monad,代数作用 6 | 7 | ## 模块依赖 8 | 9 | 有时候某些类需要在被调用方法的时候使用其他类: 10 | 11 | ```java 12 | class Human { 13 | void please() { 14 | new Hand().rush(); 15 | } 16 | void pick(T thing) { 17 | new Hand().hold(thing); 18 | } 19 | } 20 | ``` 21 | 22 | 不过像上面的 `hand` 在每次调用的时候都创建一个实例对 GC 就很不友好,实际上如果不是一次性的东西完全可以复用: 23 | 24 | ```java 25 | class Human { 26 | Hand hand = new Hand(); 27 | void please() { 28 | hand.rush(); 29 | } 30 | void pick(T thing) { 31 | hand.hold(thing); 32 | } 33 | } 34 | ``` 35 | 36 | 这样处理 `Human` 的依赖可以增强扩展性,比如换一个 `Hand` 实现只需要改一个地方。 37 | 38 | 而这个 `Hand` 在这里就是 `Human` 的一个**依赖**,也就是说 `Human` **依赖** `Hand` 。 39 | 40 | ## 依赖注入 41 | 42 | > 依赖注入就是我依赖你,你把你注入给我。 43 | > 44 | > By 千里冰封 45 | 46 | 上面的代码有个问题,如果两个人的手依赖不同的实现该怎么办?如果一个人的手是肉做的一个人是机械手该怎么办? 47 | 48 | 这时候就应该让构造 `Human` 的代码来选择构造什么样的 `Hand` 然后赋值给对应的属性或者直接传给构造函数: 49 | 50 | ```java 51 | class Human { 52 | Hand hand; 53 | Human(Hand hand) { 54 | this.hand = hand; 55 | } 56 | } 57 | ``` 58 | 59 | 这样使用 `Human` 的代码在构造 `Human` 的时候就需要传入它的依赖,也就是完成一次对 `Human` 的**依赖注入**(Dependency Injection),依赖从使用 `Human` 的代码转移到了 `Human` 中。 60 | 61 | 这样设计就可以在造人的时候选择人有什么样的手,甚至还能让两个人共用一个手。 62 | 63 | ## 自动注入 64 | 65 | 每次在使用 `Human` 的时候都要自己搓一个 `Hand` 塞进去实在是太麻烦,很多时候只需要默认的 `Hand` 就行了。这时候就需要工厂模式来自动注入依赖: 66 | 67 | ```java 68 | class HumanFactory { 69 | Hand hand; 70 | HumanFactory withHand(Hand hand) { 71 | this.hand = hand; 72 | return this; 73 | } 74 | Human build() { 75 | if (hand == null) 76 | hand = new HandDefault(); 77 | return new Human(hand); 78 | } 79 | } 80 | ``` 81 | 82 | 这种注入依赖的方法对一堆依赖的模块效果拔群,只需要配置部分依赖就可以正确使用。 83 | 84 | ## 依赖注入框架 85 | 86 | 工厂模式有个问题,当产出物非常非常复杂的时候代码量极大,但这实际上都是能自动生成的重复代码,于是人们就在这一层上进一步抽象,做出了依赖注入框架来自动生成装配工厂代码。 87 | 88 | 例如在Spring中可以通过这样添加注解来生成自动依赖注入代码: 89 | 90 | ```java 91 | @Component("Human") 92 | class Human { 93 | @Resource(name="handDefault") 94 | Hand hand; 95 | } 96 | ``` 97 | 98 | 而调用 `context.getBean("Human")` 就可以得到一个 `Human` 的实例。 99 | 100 | ## 读取器单子 101 | 102 | 如果你精通函数式编程就会想到为啥 `Hand` 要放在对象里面呢,保存状态多不好。于是第一个程序可以改成: 103 | 104 | ```java 105 | class Human { 106 | void please(Hand hand) { 107 | hand.rush(); 108 | } 109 | void pick(Hand hand, T thing) { 110 | hand.hold(thing); 111 | } 112 | } 113 | ``` 114 | 115 | 不过这样每个函数都要传一遍 `Hand` 就挺麻烦的,这时候就可以使用 `Reader Monad` 来改写这两个方法: 116 | 117 | ```java 118 | class Human { 119 | Reader please() { 120 | ReaderM m = new ReaderM<>(); 121 | return Reader.narrow( 122 | m.flatMap(m.ask, 123 | hand -> { 124 | hand.rush(); 125 | return m.pure(hand); 126 | })); 127 | } 128 | Reader pick(T thing) { 129 | ReaderM m = new ReaderM<>(); 130 | return Reader.narrow( 131 | m.flatMap(m.ask, 132 | hand -> { 133 | hand.hold(thing); 134 | return m.pure(hand); 135 | })); 136 | } 137 | } 138 | ``` 139 | 140 | 这样就可以让一个环境在函数之间隐式传递,来达到依赖注入的目的。 141 | 142 | 不过……这似乎看上去更加复杂了…… 143 | 144 | 我在这里只是提供一种思路,在某些对 `Monad` 支持良好的语言中这种思路是一种更简便的办法。 145 | 146 | ## 代数作用 147 | 148 | 讲到 `Reader Monad` ,读过代数作用那期的读者就会想到 `Reader Monad` 和代数作用是同构的。那既然 `Reader Monad` 能用来注入依赖,代数作用也可以: 149 | 150 | ```java 151 | class Human { 152 | void please(Runnable cont) { 153 | Eff.Perform("Hand", hand -> { 154 | ((Hand) hand).rush(); 155 | cont.run(); 156 | }); 157 | } 158 | void pick(T thing, Runnable cont) { 159 | Eff.Perform("Hand", hand -> { 160 | ((Hand) hand).hold(thing); 161 | cont.run(); 162 | }) 163 | } 164 | } 165 | ``` 166 | 167 | 这样看上去就比 `Reader Monad` 的写法清晰很多,虽然回调的写法也挺反人类的……不过在支持代数作用的语言里面这种写法将是强力的依赖注入工具。 168 | -------------------------------------------------------------------------------- /doc/EvalStrategy.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:求值策略 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:Java基础,λ演算 6 | 7 | ## 非严格求值 8 | 9 | 细心的读者应该已经注意到, λ 演算那期中讲到的 λ 演算解释器的 `reduce` 并不会先将参数 `reduce` ,而是先 `apply` 参数再在最后 `reduce` 结果: 10 | 11 | ```java 12 | // class App 13 | public Expr reduce() { 14 | Expr fr = f.reduce(); 15 | if (fr instanceof Fun) { 16 | Fun fun = (Fun) fr; 17 | return fun.e.apply(fun.x, x).reduce(); 18 | } 19 | return new App(fr, x); 20 | } 21 | ``` 22 | 23 | 并且函数的 `reduce` 并不会 `reduce` 函数内部表达式而是直接返回 `this` 。 24 | 25 | 这样处理和平时见过的常规语言似乎很不一样,C、Java在处理函数参数时都选择先对参数求值再传参。 26 | 27 | 像这样先传参再归约的求值思路就叫非严格求值(Non-strict Evaluation),也叫惰性求值(Lazy Evaluation)。其最大好处是在很多时候函数的参数没有被使用过的情况下节省了求值时归约次数。 28 | 29 | 比如 `(λ x. λ y. x) complex1 complex2` 这个 λ 表达式只会取 `complex1` 和 `complex2` 中的 `complex1` ,如果 `complex2` 非常复杂那就非常浪费算力了。 30 | 31 | ## 严格求值 32 | 33 | 所谓严格求值(Strict Evaluation)就是像 C 系语言一样先求参数的值再传参: 34 | 35 | ```java 36 | // class App 37 | public Expr strictReduce() { 38 | Expr fr = f.strictReduce(); 39 | Expr xr = x.strictReduce(); 40 | if (fr instanceof Fun) { 41 | Fun fun = (Fun) fr; 42 | return fun.e.apply(fun.x, xr).strictReduce(); 43 | } 44 | return new App(fr, xr); 45 | } 46 | ``` 47 | 48 | λ 演算中所有的表达式都是**纯**的,也就是说不同的求值策略和求值顺序并不会影响最终求值的结果。所以不同的求值策略求出的结果都是等价的,只是打印出来的效果不同。这与 C 系语言就很不一样。 49 | 50 | ## 完全 β 归约 51 | 52 | 如果想要让打印出的表达式被化到最简还需要在严格求值的基础上把函数定义中的表达式也进一步归约: 53 | 54 | ```java 55 | // class Fun 56 | public Expr fullBetaReduce() { 57 | return new Fun(x, e.fullBetaReduce()); 58 | } 59 | ``` 60 | 61 | 这就是完全 β 归约。它会将任何能归约的地方归约,即使这个函数并没有被应用函数内部也会被归约。 62 | 63 | ## 对比 64 | 65 | 对于λ表达式: 66 | 67 | ```` 68 | (λ n. λ f. λ x. (f (n f x)) (λ f. λ x. x)) 69 | ```` 70 | 71 | 进行非严格求值会得到: 72 | 73 | ``` 74 | λ f. λ x. f ((λ f. λ x. x) f x) 75 | ``` 76 | 77 | 进行完全β归约会得到: 78 | 79 | ``` 80 | λ f. λ x. f x 81 | ``` 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /doc/GADT.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:广义代数数据类型 2 | 3 | ### By 「玩火」,改写「CWKSC」 4 | 5 | > 前置技能:C# (interface, class),[ADT](ADT.md) 6 | 7 | 在 ADT 中可以构造出如下类型: 8 | 9 | ```csharp 10 | // 构造函数已省去 11 | public interface Expr { } 12 | public class IntVal : Expr { int value; } 13 | public class BoolVal : Expr { bool value; } 14 | public class Add : Expr { Expr e1, e2; } 15 | public class Eq : Expr { Expr e1, e2; } 16 | ``` 17 | 18 | 但是这样构造有个问题,很显然 `BoolVal` 是不能相加,而这样的构造并不能防止构造出这样的东西。实际上在这种情况下 ADT 的表达能力是不足的 19 | 20 | 一个比较显然的解决办法是给 `Expr` 添加一个类型参数用于标记表达式的类型: 21 | 22 | ```csharp 23 | // 构造函数已省去 24 | public interface Expr { } 25 | public interface Expr : Expr { } 26 | public class IntVal : Expr { int value; } 27 | public class BoolVal : Expr { bool value; } 28 | public class Add : Expr { Expr e1, e2; } 29 | public class Eq : Expr { Expr e1, e2; } 30 | ``` 31 | 32 | 这样就可以避免构造出两个类型为 `bool` 的表达式相加,能构造出的表达式都是类型安全的 33 | 34 | 注意到四个 `class` 的父类都不是 `Expr` 而是包含参数的 `Expr` ,这和 ADT 并不一样。而这就是广义代数数据类型(Generalized Algebraic Data Type, GADT) 35 | 36 | ### Example 完整例子: 37 | 38 | ```csharp 39 | public interface Expr { } 40 | public interface Expr : Expr { } 41 | public class IntVal : Expr 42 | { 43 | public int value; 44 | public IntVal(int v) => value = v; 45 | } 46 | public class BoolVal : Expr 47 | { 48 | public bool value; 49 | public BoolVal(bool b) => value = b; 50 | } 51 | public class Add : Expr 52 | { 53 | public Expr e1, e2; 54 | public Add(Expr e1, Expr e2) 55 | { 56 | this.e1 = e1; 57 | this.e2 = e2; 58 | } 59 | } 60 | public class Eq : Expr 61 | { 62 | public Expr e1, e2; 63 | public Eq(Expr e1, Expr e2) 64 | { 65 | this.e1 = e1; 66 | this.e2 = e2; 67 | } 68 | } 69 | 70 | // 1 + 2 71 | Expr OnePlusTwo = new Add(new IntVal(1), new IntVal(2)); 72 | 73 | // true == false 74 | Expr TrueEqFalse = new Eq(new BoolVal(true), new BoolVal(false)); 75 | 76 | // 1 + true 77 | // CS1503 引数 2: 无法从 'GADT.BoolVal' 转换成 'GADT.Expr' 78 | Expr OnePlusTrue = new Add(new IntVal(1), new BoolVal(true)); 79 | ^^^^^^^^^^^^^^^^^ 80 | 81 | // true == 42 82 | // CS1503 引数 2: 无法从 'GADT.IntVal' 转换成 'GADT.Expr' 83 | Expr TrueEq42 = new Eq(new BoolVal(true), new IntVal(42)); 84 | ^^^^^^^^^^^^^^ 85 | ``` 86 | 87 | ### See other 参看其他: 88 | 89 | [什么是 Haskell 中的 GADT(广义代数数据类型)? | 黃河青山](https://colliot.org/zh/2017/11/what-is-gadt-in-haskell/) 90 | 91 | [Generalized algebraic data type - Wikipedia](https://en.wikipedia.org/wiki/Generalized_algebraic_data_type) -------------------------------------------------------------------------------- /doc/GADT_zh-Hant.md: -------------------------------------------------------------------------------- 1 | # 十分鐘魔法練習:廣義代數數據類型 2 | 3 | ### By 「玩火」,改寫「CWKSC」 4 | 5 | > 前置技能:C# 基礎,[ADT](ADT_zh_Hant.md) 6 | 7 | 在 ADT 中可以構造出如下類型: 8 | 9 | ```csharp 10 | // 構造函數已省去 11 | public interface Expr { } 12 | public class IntVal : Expr { int value; } 13 | public class BoolVal : Expr { bool value; } 14 | public class Add : Expr { Expr e1, e2; } 15 | public class Eq : Expr { Expr e1, e2; } 16 | ``` 17 | 18 | 但是這樣構造有個問題,很顯然 `BoolVal` 是不能相加,而這樣的構造並不能防止構造出這樣的東西。實際上在這種情況下 ADT 的表達能力是不足的 19 | 20 | 一個比較顯然的解決辦法是給 `Expr` 添加一個類型參數用於標記表達式的類型: 21 | 22 | ```csharp 23 | // 構造函數已省去 24 | public interface Expr { } 25 | public interface Expr : Expr { } 26 | public class IntVal : Expr { int value; } 27 | public class BoolVal : Expr { bool value; } 28 | public class Add : Expr { Expr e1, e2; } 29 | public class Eq : Expr { Expr e1, e2; } 30 | ``` 31 | 32 | 這樣就可以避免構造出兩個類型為 `bool` 的表達式相加,能構造出的表達式都是類型安全的 33 | 34 | 注意到四個 `class` 的父類都不是 `Expr` 而是包含參數的 `Expr` ,這和 ADT 並不一樣。而這就是廣義代數數據類型(Generalized Algebraic Data Type, GADT) 35 | 36 | ### Example 完整例子: 37 | 38 | ```csharp 39 | public interface Expr { } 40 | public interface Expr : Expr { } 41 | public class IntVal : Expr 42 | { 43 | public int value; 44 | public IntVal(int v) => value = v; 45 | } 46 | public class BoolVal : Expr 47 | { 48 | public bool value; 49 | public BoolVal(bool b) => value = b; 50 | } 51 | public class Add : Expr 52 | { 53 | public Expr e1, e2; 54 | public Add(Expr e1, Expr e2) 55 | { 56 | this.e1 = e1; 57 | this.e2 = e2; 58 | } 59 | } 60 | public class Eq : Expr 61 | { 62 | public Expr e1, e2; 63 | public Eq(Expr e1, Expr e2) 64 | { 65 | this.e1 = e1; 66 | this.e2 = e2; 67 | } 68 | } 69 | 70 | // 1 + 2 71 | Expr OnePlusTwo = new Add(new IntVal(1), new IntVal(2)); 72 | 73 | // true == false 74 | Expr TrueEqFalse = new Eq(new BoolVal(true), new BoolVal(false)); 75 | 76 | // 1 + true 77 | // CS1503 引數 2: 無法從 'GADT.BoolVal' 轉換成 'GADT.Expr' 78 | Expr OnePlusTrue = new Add(new IntVal(1), new BoolVal(true)); 79 | ^^^^^^^^^^^^^^^^^ 80 | 81 | // true == 42 82 | // CS1503 引數 2: 無法從 'GADT.IntVal' 轉換成 'GADT.Expr' 83 | Expr TrueEq42 = new Eq(new BoolVal(true), new IntVal(42)); 84 | ^^^^^^^^^^^^^^ 85 | ``` 86 | 87 | ### See other 參看其他: 88 | 89 | [什麼是 Haskell 中的 GADT(廣義代數數據類型)? | 黃河青山](https://colliot.org/zh/2017/11/what-is-gadt-in-haskell/) 90 | 91 | [Generalized algebraic data type - Wikipedia](https://en.wikipedia.org/wiki/Generalized_algebraic_data_type) -------------------------------------------------------------------------------- /doc/HKT.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:高阶类型 2 | 3 | ### By 「玩火」,改写「CWKSC」 4 | 5 | > 前置技能:C# (interface, List) 6 | 7 | ## 常常碰到的困难 8 | 9 | 写代码的时候常常会碰到语言表达能力不足的问题,比如下面这段用来给 `F` 容器中的值进行映射的代码: 10 | 11 | ```csharp 12 | public interface Functor 13 | { 14 | public F Map(Func f, F a); 15 | ^^^^ ^^^^ 16 | } 17 | // CS0307 类型参数 'F' 不可搭配类型引数一起使用 18 | ``` 19 | 20 | 并不能通过编译,编译器会告诉你 F 不能有泛型参数。 21 | 22 | 最简单粗暴的解决方案就是放弃类型检查,全上 `object` ,如: 23 | 24 | ```csharp 25 | public interface Functor 26 | { 27 | public object Map(Func f, object a); 28 | } 29 | ``` 30 | 31 | ## 高阶类型 32 | 33 | 假设类型的类型是 `Type` ,比如 `int` 和 `String` 类型都是 `Type` 。 34 | 35 | 而对于 `List` 这样带有一个泛型参数的类型来说,它相当于一个把类型 `T` 映射到 `List` 的函数,其类型可以表示为 `Type -> Type` 。 36 | 37 | 同样的对于 `Map` 来说它有两个泛型参数,类型可以表示为 `(Type, Type) -> Type` 。 38 | 39 | 像这样把类型映射到类型的非平凡类型就叫高阶类型(HKT, Higher Kinded Type)。 40 | 41 | 虽然 C# 中存在这样的高阶类型但是我们并不能用一个泛型参数表示出来,也就不能写出如上 `F` 这样的代码了,因为 `F` 是个高阶类型。 42 | 43 | > 如果加一层解决不了问题,那就加两层。 44 | 45 | 虽然在 C# 中不能直接表示出高阶类型,但是我们可以通过加一个中间层来在保留完整信息的情况下强类型地模拟出高阶类型。 46 | 47 | 首先,我们需要一个中间层: 48 | 49 | ```csharp 50 | public interface HKT { } 51 | ``` 52 | 53 | 然后我们就可以用 `HKT` 来表示 `F` ,这样操作完 `HKT` 后我们仍然有完整的类型信息来还原 `F` 的类型。 54 | 55 | 这样,上面 `Functor` 就可以写成: 56 | 57 | ```csharp 58 | public interface Functor 59 | { 60 | public HKT Map(Func f, HKT a); 61 | } 62 | ``` 63 | 64 | 这样就可以编译通过了。而对于想实现 `Functor` 的类,需要先实现 `HKT` 这个中间层,这里拿 `List` 举例: 65 | 66 | ```csharp 67 | public interface ListHKT { } 68 | public class ListHKT : HKT, ListHKT 69 | { 70 | public List value; 71 | public ListHKT() => value = new List(); 72 | public ListHKT(List v) => value = v; 73 | public static ListHKT Narrow(HKT v) => (ListHKT)v; 74 | } 75 | ``` 76 | 77 | 注意 `ListHKT` 把自己作为了 `HKT` 的第一个参数来保存自己的类型信息,这样对于 `HKT` 这个接口来说就只有自己这一个子类,而在 `Narrow` 函数中可以安全地把这个唯一子类转换回来。 78 | 79 | 这样,实现 `Functor` 类就是一件简单的事情了: 80 | 81 | ```csharp 82 | public class ListF : Functor 83 | { 84 | public HKT Map(Func f, HKT a) => 85 | new ListHKT(new List( 86 | ListHKT.Narrow(a).value.Select(f))); 87 | } 88 | ``` 89 | 90 | ## 测试代码: 91 | 92 | ```csharp 93 | List list = new List(){ 1, 2, 3, 4 }; 94 | ListHKT listHKT = new ListHKT(list); 95 | ListF listFunctor = new ListF(); 96 | ListHKT mappedListHKT = (ListHKT)listFunctor.Map(x => x + 1.5, listHKT); 97 | List mappedList = mappedListHKT.value; 98 | 99 | foreach (var ele in mappedList) 100 | Console.Write(ele + " "); 101 | // 2.5 3.5 4.5 5.5 102 | ``` 103 | 104 | 压缩: 105 | 106 | ```csharp 107 | List list2 = 108 | ((ListHKT)new ListF().Map(x => x + 1.5, 109 | new ListHKT( 110 | new List() { 1, 2, 3, 4 }))).value; 111 | ``` 112 | 113 | 为了方便使用,可以在 `ListHKT` 中加入隐含转换 114 | 115 | ```csharp 116 | public static implicit operator ListHKT(List list) => new ListHKT(list); 117 | public static implicit operator List(ListHKT list) => list.value; 118 | ``` 119 | 120 | ```csharp 121 | ListHKT listHKT = new List() { 1, 2, 3, 4 }; 122 | List mappedList = (ListHKT)new ListF().Map(x => x + 1.5, listHKT); 123 | ``` 124 | 125 | -------------------------------------------------------------------------------- /doc/Lambda.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:λ 演算 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:C# (Guid),ADT 6 | 7 | ## Intro 8 | 9 | 程序员们总是为哪种语言更好而争论不休,而强悍的大佬也为自己造出语言而感到高兴。造语言也被称为程序员的三大浪漫之一。这样一项看上去高难度的活动总是让萌新望而生畏,接下来我要介绍一种世界上最简单的**图灵完备**语言并给出 100 行 C# 代码的解释器实现。让萌新也能体验造语言的乐趣。 10 | 11 | ## λ演算 12 | 13 | 1936 年,丘奇(Alonzo Church)提出了一种非常简单的计算模型,叫 λ 演算(Lambda Calculus)。 14 | 15 | > 一些不严谨的通俗理解: 16 | > 17 | > λ表达式中的函数定义 `(λ x. E)` 就是定义了数学上的函数 `f(x)=E` ,只不过没有名字, `λ` 代表一个函数定义的开始,而 `.` 左边的是函数的自变量,可以是任意符号,这里用了 `x` , `.` 的右边是函数的内容 `E` ,可以是任意 λ 表达式。 18 | > 19 | > 而函数应用 `F X` 就是对于一个数学上的函数 `F` 求值 `F(X)` , `F` 就是函数, `X` 就是参数。比如 `(λ x. x)` 就是 `f(x)=x` ,比如 `(λ x. (x x))` 可以表示为 `f(x) = x(x)` ,其中 `x` 应当是个函数,不过这在数学里面是不允许的,而 `((λ x. (x x)) y)` 就可以表示为数学上的 `f(x) = x(x), f(y)` 也就是 `y(y)` 。 20 | > 21 | > 和传统数学函数最不一样的是λ演算里面的函数可以在任何位置被定义并且没有名字,并且可以被当作变量传递也可以作为函数的计算结果。 22 | 23 | 一个λ表达式有三种组成可能:变量 `x` 、函数定义 `(λ x. E)` 、函数应用 `(F X)` 。其中 `x` 是一个抽象的符号, `E, F, X` 是 λ 表达式。注意这是递归的定义,我们可以通过组合三种形式来构造复杂的 λ 表达式。比如 `((λ x. (x x)) y)` 整体是一个函数应用,其 `F` 是函数定义 `(λ x. (x x))` , `X` 是 `y` ,而 `(λ x. (x x))` 函数定义的 `x` 是变量 `x` , `E` 是 `(x x)` 。 24 | 25 | λ表达式的计算也称为归约 (reduce) ,只需要将函数应用整体变换,变换结果为其作为函数定义的第一项 `F` (也就是 `(λ x. E)` ) 中 `E` 里出现的所有**自由**的 `x` 替换为其第二项 `X` ,也就是说 `((λ x. E) X)` 会被归约为 `E(x → X)` ,。听上去挺复杂,举个最简单的例子 `((λ x. (x x)) y)` 可以归约为 `(y y)` 。我这里提到了自由的 `x` ,意思是说它不是任何λ函数定义的自变量,比如 `(λ x. (x t))` 中的 `x` 就是不自由的, `t` 就是自由的。 26 | 27 | 函数定义有比函数应用更低的优先级,也就是说是 `(λ x. (x x))` 可以写成 `(λ x. x x)` 。函数应用是左结合的,所以 `((x x) x)` 可以写成 `(x x x)` 。 28 | 29 | ## 解释器 30 | 31 | 首先,我们要用 ADT 定义出 λ 表达式的数据结构: 32 | 33 | ```csharp 34 | public interface Expr { }; 35 | 36 | // Value 变量 // 37 | public class Val : Expr 38 | { 39 | public string value; 40 | public string id; 41 | public Val(string value) => this.value = value; 42 | public Val(string value, string id) 43 | { 44 | this.value = value; 45 | this.id = id; 46 | } 47 | public override string ToString() => value; 48 | public override bool Equals(object obj) => 49 | obj is Val val && id.Equals(val.id); 50 | public override int GetHashCode() => HashCode.Combine(value, id); 51 | } 52 | 53 | // Function 函数定义 // 54 | public class Fun : Expr 55 | { 56 | public Val value; 57 | public Expr expr; 58 | public Fun(string value, Expr expr) { 59 | this.value = new Val(value); 60 | this.expr = expr; 61 | } 62 | public Fun(Val value, Expr expr) { 63 | this.value = value; 64 | this.expr = expr; 65 | } 66 | public override string ToString() => 67 | "(λ " + value + ". " + expr + ")"; 68 | } 69 | 70 | // Apply 函数应用 // 71 | public class App : Expr 72 | { 73 | public Expr f, x; 74 | public App(Expr f, Expr x) { 75 | this.f = f; 76 | this.x = x; 77 | } 78 | public override string ToString() => 79 | "(" + f + " " + x + ")"; 80 | } 81 | ``` 82 | 83 | > 注意到上面代码中 `Val` 有一个类型为 `string` 的字段,同时 `Equals` 函数只比较 `id` 字段,这个字段是用来区分相同名字的不同变量的。如果不做区分那么对于下面的 λ 表达式: 84 | > 85 | > ``` 86 | > λ z. (λ x. (λ z. x)) z 87 | > ``` 88 | > 89 | > 会被规约成 90 | > 91 | > ``` 92 | > λ z. (λ z. z) 93 | > ``` 94 | > 95 | > 然而实际上最内层的 `z` 最开始是被最外层的函数定义定义的,而这里它被内层的函数定义错误地捕获(Capture)了,所以正确的规约结果应该是: 96 | > 97 | > ``` 98 | > λ z'. (λ z. z') 99 | > ``` 100 | 101 | 然后就可以构造 λ 表达式了,比如 `(λ x. x (λ x. x)) y` 就可以这样构造: 102 | 103 | ```csharp 104 | Expr expr = new App( 105 | new Fun("x", 106 | new App(new Val("x"), 107 | new Fun("x", new Val("x")))), 108 | new Val("y")); 109 | ``` 110 | 111 | 然后就可以定义归约函数 `Reduce` 和应用自由变量函数 `Apply` 还有用来生成 `UUID` 的 `GenUUID` 函数和 `ApplyUUID` 函数: 112 | 113 | ```csharp 114 | public interface Expr { 115 | Expr Reduce(); 116 | Expr Apply(Val value, Expr expr); 117 | Expr GenUUID(); 118 | Expr ApplyUUID(Val a); 119 | } 120 | 121 | public class Val : Expr 122 | public Expr Reduce() => this; 123 | 124 | public Expr Apply(Val value, Expr expr) => 125 | Equals(value) ? expr : this; 126 | 127 | public Expr GenUUID() => this; 128 | 129 | public Expr ApplyUUID(Val a) => 130 | value.Equals(a.value) ? new Val(value, a.id) : this; 131 | } 132 | 133 | public class Fun : Expr 134 | public Expr Reduce() => this; 135 | 136 | public Expr Apply(Val variable, Expr expr) => 137 | variable.Equals(this.variable) ? this : 138 | new Fun(this.variable, this.expr.Apply(this.variable, expr)); 139 | 140 | public Expr GenUUID() 141 | { 142 | if (variable.id == null) 143 | { 144 | Val v = new Val(variable.value, Guid.NewGuid().ToString()); 145 | return new Fun(v, expr.ApplyUUID(v).GenUUID()); 146 | } 147 | return new Fun(variable, expr.GenUUID()); 148 | } 149 | 150 | public Expr ApplyUUID(Val a) => 151 | variable.value.Equals(a.value) ? this : 152 | new Fun(variable, expr.ApplyUUID(a)); 153 | } 154 | 155 | public class App : Expr 156 | { 157 | public Expr Reduce() 158 | { 159 | Expr fr = f.Reduce(); 160 | return fr is Fun fun ? 161 | fun.expr.Apply(fun.variable, x).Reduce() : 162 | new App(fr, x); 163 | } 164 | 165 | public Expr Apply(Val value, Expr expr) => 166 | new App(f.Apply(value, expr), 167 | x.Apply(value, expr)); 168 | 169 | public Expr GenUUID() => 170 | new App(f.GenUUID(), x.GenUUID()); 171 | 172 | public Expr ApplyUUID(Val a) => 173 | new Fun((Val)f.ApplyUUID(a), x.ApplyUUID(a)); 174 | } 175 | ``` 176 | 177 | 注意在 `Reduce` 一个表达式之前应该先调用 `GenUUID` 来生成变量标签否则会抛出空指针异常。 178 | 179 | ```csharp 180 | Expr expr = new App( 181 | new Fun("x", 182 | new App(new Val("x"), 183 | new Fun("x", new Val("x")))), 184 | new Val("y")); 185 | 186 | Console.WriteLine(expr); // ((λ x. (x (λ x. x))) y) 187 | Console.WriteLine(expr.GenUUID().Reduce()); // (λ x. (λ x. x)) 188 | ``` 189 | 190 | 以上就是 100 行 C# 写成的解释器啦! 191 | 192 | -------------------------------------------------------------------------------- /doc/Lifting.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:提升 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:Java基础,HKT,Monad 6 | 7 | ## 概念 8 | 9 | 提升(Lifting)指的是把一个通用函数变成容器映射函数的操作。 10 | 11 | 比如把 `Function` 变成 `Function, M>` 就是一种提升操作。而由于被操作的函数有一个参数所以这个操作也叫 `lift1` 。 12 | 13 | 注意被提升的函数可以有不止一个参数,我们也可以把 `BiFunction` 提升为 `BiFunction, M, M>` 。这样两个参数的提升可以称为 `lift2` 。 14 | 15 | 同样,被提升的函数可以没有参数,这时候我们可以看成没有这个函数,也就是把 `A` 提升为 `M` 。这样的提升可以称为 `lift0` 。实际上它也和 `Monad` 中的 `pure` 是同构的。 16 | 17 | 也就是说: 18 | 19 | ```java 20 | M 21 | lift0(A f) {} 22 | Function, M> 23 | lift1(Function f) {} 24 | BiFunction, M, M> 25 | lift2(Function) {} 26 | ``` 27 | 28 | ## fmap 29 | 30 | 看到这个函数签名肯定有人会拍案而起:这不就是 fmap 么? 31 | 32 | fmap is a lifting surly. 因为它符合 lifting 的函数签名,但是 lifting 并不一定是 fmap 。只要符合这样的函数签名就可以说是一个 lifting 。 33 | 34 | 比如对于 list 来说 `f -> x -> x.tail().map(f)` 也符合 lifting 的函数签名但很显然它不是一个 `fmap` 函数。或者说很多改变结构的函数和 `fmap` 组合还是一个 lifting 函数。 35 | 36 | ## 除此之外呢 37 | 38 | 回到上面那个函数签名,里面有个非泛型的参数 `M` ,这个 `M` 可以是个泛型参数,可以是个包装器比如 `Maybe` ,也可以是个线性容器比如 `List` ,可以是个非线性的容器比如 `Set` ,甚至可以是抽象容器比如 `Function` 。 39 | 40 | 同时提升操作也可能对容器结构做出一些改变,尤其是对于多参函数的提升可能会对函数的参数做出一些组合。比如对于 `List` 来说 `lift2` 既可以是 `zipMap` 也可也是以 `f` 为操作的卷积。 41 | 42 | ## liftM 43 | 44 | 对于 Monad 来说,存在一种通用的提升操作叫 `liftM` ,比如对于 `List` 来说 `liftM2` 就是: 45 | 46 | ```java 47 | 48 | BiFunction, List, List> 49 | liftM2List(BiFunction f) { 50 | HKTListM m = new HKTListM(); 51 | return (ma, mb) -> HKTList.narrow( 52 | m.flatMap(new HKTList<>(ma), 53 | a -> m.flatMap(new HKTList<>(mb), 54 | b -> m.pure(f.apply(a, b)))) 55 | ).value; 56 | } 57 | ``` 58 | 59 | 而对 `Integer::sum` 进行提升以后的函数输入 `[1, 2, 3]` 和 `[2, 3, 4]` 就会得到 `[3, 4, 5, 4, 5, 6, 5, 6, 7]` 。实际上就是对于任意两个元素组合操作。 60 | 61 | 再比如 `liftM5` 在 `Haskell` 中的表述为: 62 | 63 | ```haskell 64 | liftM5 f ma mb mc md me = do 65 | a <- ma 66 | b <- mb 67 | c <- mc 68 | d <- md 69 | e <- me 70 | pure (f a b c d e) 71 | ``` 72 | 73 | 也就是 `liftM[n]` 就相当于嵌套 `n` 层 `flatMap` 提取 `Monad` 中的值然后应用给被提升的函数。 -------------------------------------------------------------------------------- /doc/Monad.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:单子 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:C# 基础,[HKT](HKT.md) 6 | 7 | ## Monad 单子 8 | 9 | 单子(Monad)是指一种有一个类型参数的数据结构,拥有 `pure` (也叫 `unit` 或者 `return` )和 `flatMap` (也叫 `bind` 或者 `>>=` )两种操作: 10 | 11 | ```csharp 12 | public interface Monad 13 | { 14 | public HKT Pure(V value); 15 | public HKT FlatMap(HKT a, Func> f); 16 | } 17 | ``` 18 | 19 | 其中 `Pure` 要求返回一个包含参数类型内容的数据结构, `FlatMap` 要求把 `a` 的值经过 `f` 以后再串起来。 20 | 21 | 举个最经典的例子: 22 | 23 | ## List Monad 24 | 25 | ```csharp 26 | public class ListM : Monad 27 | { 28 | public HKT Pure(V value) => 29 | new ListHKT(new List() { value }); 30 | 31 | // 方便使用 32 | public HKT Pure(params V[] values) => 33 | new ListHKT(new List(values)); 34 | 35 | public HKT FlatMap(HKT a, Func> f) => 36 | ListHKT.Narrow(a).value.Aggregate(new ListHKT(), (result, ele) => 37 | { 38 | result.value.AddRange(ListHKT.Narrow(f(ele)).value); 39 | return result; 40 | }); 41 | } 42 | ``` 43 | 44 | 简单来说 `Pure(v)` 将得到 `{v}` ,而 `FlatMap({1, 2, 3}, v -> {v + 1, v + 2})` 将得到 `{2, 3, 3, 4, 4, 5}` 。 45 | 46 | ```csharp 47 | ListM listM = new ListM(); 48 | 49 | ListHKT pureHKT = (ListHKT)listM.Pure(42, 10007); 50 | List pure = pureHKT.value; 51 | foreach (var ele in pure) 52 | Console.Write(ele + " "); 53 | // 42 10007 54 | Console.WriteLine(); 55 | 56 | List list = new List() { 1, 2, 3, 4 }; 57 | ListHKT listHKT = new ListHKT(list); 58 | 59 | ListHKT flatMappedListHKT = 60 | (ListHKT)listM.FlatMap(listHKT, v => 61 | listM.Pure(v + 0.5, v + 1.5)); 62 | 63 | List flatMappedList = flatMappedListHKT.value; 64 | foreach (var ele in flatMappedList) 65 | Console.Write(ele + " "); 66 | // 1.5 2.5 2.5 3.5 3.5 4.5 4.5 5.5 67 | ``` 68 | 69 | ## Maybe Monad 70 | 71 | C# 不是一个空安全的语言,也就是说任何对象类型的变量都有可能为 `null` 。对于一串可能出现空值的逻辑来说,判空常常是件麻烦事: 72 | 73 | ```csharp 74 | public static Maybe AddI(Maybe ma, Maybe mb) 75 | { 76 | if (ma is Nothing || mb is Nothing) 77 | return new Nothing(); 78 | return new Just( 79 | ((Just)ma).value + 80 | ((Just)mb).value); 81 | } 82 | ``` 83 | 84 | `Maybe` 的实现: 85 | 86 | ```csharp 87 | public interface Maybe { } 88 | public interface Maybe : HKT, Maybe 89 | { 90 | public static Maybe Narrow(HKT v) => (Maybe)v; 91 | } 92 | 93 | public class Nothing : Maybe { } 94 | public class Just : Maybe 95 | { 96 | public T value; 97 | public Just() { } 98 | public Just(T value) => this.value = value; 99 | } 100 | ``` 101 | 102 | 可以像这样定义 `Maybe Monad` : 103 | 104 | ```csharp 105 | public class MaybeM : Monad 106 | { 107 | public HKT Pure(V value) => new Just(value); 108 | public HKT FlatMap(HKT a, Func> f) 109 | { 110 | A value = ((Just)Maybe.Narrow(a)).value; 111 | return value == null ? new Nothing() : f(value); 112 | } 113 | } 114 | ``` 115 | 116 | 上面 `AddI` 的代码就可以改成: 117 | 118 | ```csharp 119 | public static Maybe AddI(Maybe ma, Maybe mb) 120 | { 121 | MaybeM m = new MaybeM(); 122 | return Maybe.Narrow( // do 123 | m.FlatMap(ma, a => // a <- ma 124 | m.FlatMap(mb, b => // b <- mb 125 | m.Pure(a + b))) // pure (a + b) 126 | ); 127 | } 128 | ``` 129 | 130 | 这样看上去就比上面的连续 `if-return` 优雅很多。在一些有语法糖的语言 (`Haskell`) 里面 Monad 的逻辑甚至可以像上面右边的注释一样简单明了。 131 | 132 | > 我知道会有人说,啊,我有更简单的写法: 133 | > 134 | > ```csharp 135 | > public static Maybe AddE(Maybe ma, Maybe mb) 136 | > { 137 | > try 138 | > { 139 | > return new Just(((Just)ma).value + ((Just)mb).value); 140 | > } 141 | > catch (Exception) 142 | > { 143 | > return new Nothing(); 144 | > } 145 | > } 146 | > ``` 147 | > 148 | > 确实,这样写也挺简洁直观的, `Maybe Monad` 在有异常的 C# 里面确实不是一个很好的例子,不过 `Maybe Monad` 确实是在其他没有异常的函数式语言里面最为常见的 Monad 用法之一。而之后我也会介绍一些异常也无能为力的 Monad 用法。 149 | 150 | -------------------------------------------------------------------------------- /doc/Monoid.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:单位半群 2 | 3 | ### By 「玩火」,改写「CWKSC」 4 | 5 | > 前置技能:C# (IEnumerable, Aggregate(), Extension Methods, IComparable) 6 | 7 | ## Semigroup 半群 8 | 9 | 半群是一种代数结构,在集合 `A` 上包含一个将两个 `A` 的元素映射到 `A` 上的运算即 `<> : (A, A) -> A​` ,同时该运算满足**结合律**即 `(a <> b) <> c == a <> (b <> c)` ,那么代数结构 `{<>, A}` 就是一个半群。 10 | 11 | 比如在自然数集上的加法或者乘法可以构成一个半群,再比如字符串集上字符串的连接构成一个半群。 12 | 13 | ```csharp 14 | // 二元运算 15 | public interface BinaryOperation { public T BinaryOperation(T a, T b); } 16 | // 半群 17 | public interface Semigroup : BinaryOperation { } 18 | ``` 19 | 20 | ## Monoid 单位半群 21 | 22 | 单位半群是一种带单位元的半群,对于集合 `A` 上的半群 `{<>, A}` , `A` 中的元素 `a` 使 `A` 中的所有元素 `x` 满足 `x <> a` 和 `a <> x` 都等于 `x`,则 `a` 就是 `{<>, A}` 上的单位元。 23 | 24 | 举个例子, `{+, 自然数集}` 的单位元就是 0 , `{*, 自然数集}` 的单位元就是 1 , `{+, 字符串集}` 的单位元就是空串 `""` 。 25 | 26 | ```csharp 27 | // 单位元 28 | public interface Unital { public T Identity(); } 29 | // 单位半群 30 | public interface Monoid : Unital, Semigroup { } 31 | ``` 32 | 33 | 对于 `Monoid` 这种代数结构,可以折叠 `fold` / `reduce`: 34 | 35 | ```csharp 36 | public static T Appends(this Monoid monoid, IEnumerable x) => 37 | x.Aggregate(monoid.Identity(), monoid.BinaryOperation); 38 | 39 | // 下面这个是为了方便使用 40 | public static T Appends(this Monoid monoid, params T[] x) => 41 | monoid.Appends((IEnumerable)x); 42 | ``` 43 | 44 | 对于其他在 `Monoid` 上使用的运算,可以用 C# 中的 Extension Methods 添加。 45 | 46 | ## 应用:Optional 47 | 48 | 在 C# 中可以用 `T?` 或者 `Nullable` 可以用来表示可能有值的类型,我们可以对它定义个 `Monoid` : 49 | 50 | ```csharp 51 | public class OptionalM : Monoid where T : struct 52 | { 53 | public T? Identity() => null; 54 | public T? BinaryOperation(T? a, T? b) => a ?? b; 55 | } 56 | ``` 57 | 58 | 这样 `appends` 将获得一串 `T?` 中第一个不为空的值,对于需要进行一连串尝试操作可以这样写: 59 | 60 | ```csharp 61 | new OptionalM().Appends(null, 2, 3) // 2 62 | ``` 63 | 64 | ## 应用:Ordering 65 | 66 | 存在一個 `Student` 类 67 | 68 | ```csharp 69 | public class Student : IComparable 70 | { 71 | public string name; 72 | public string sex; 73 | public DateTime birthday; 74 | public string from; 75 | } 76 | ``` 77 | 78 | 如果想对其实现 `IComparable` 接口,正常情况下: 79 | 80 | ```csharp 81 | public int CompareTo(Student s) => 82 | name.CompareTo(s.name) != 0 ? name.CompareTo(s.name) : 83 | sex.CompareTo(s.sex) != 0 ? sex.CompareTo(s.sex) : 84 | birthday.CompareTo(s.birthday) != 0 ? birthday.CompareTo(s.birthday) : 85 | from.CompareTo(s.from) != 0 ? from.CompareTo(s.from) : 0; 86 | ``` 87 | 88 | 对于 `IComparable` ,可以构造出一個 `Monoid`: 89 | 90 | ```csharp 91 | public class OrderingM : Monoid 92 | { 93 | public int Identity() => 0; 94 | public int BinaryOperation(int a, int b) => a == 0 ? b : a; 95 | } 96 | ``` 97 | 98 | 同样如果有一串带有优先级的比较操作就可以用 `appends` 串起来,比如: 99 | 100 | ```csharp 101 | public int CompareTo(Student student) => 102 | OrderingM().Appends( 103 | name.CompareTo(student.name), 104 | sex.CompareTo(student.sex), 105 | birthday.CompareTo(student.birthday), 106 | from.CompareTo(student.from) 107 | ); 108 | ``` 109 | 110 | 这样的写法比一连串 `if-else` 或者 `?:` 优雅太多。 111 | 112 | ## 扩展 113 | 114 | 用 Extension Methods 對 `Monoid` 添加方法,支持更多方便的操作: 115 | 116 | ```csharp 117 | public static T When(this Monoid monoid, bool c, T then) => 118 | c ? then : monoid.Identity(); 119 | public static T Cond(this Monoid monoid, bool c, T then, T els) => 120 | c ? then : els; 121 | ``` 122 | 123 | 存在一個 `Todo` 类, 124 | 125 | ```csharp 126 | public class Todo : Monoid 127 | { 128 | public Action Identity() => () => { }; 129 | public Action BinaryOperation(Action a, Action b) => 130 | () => { a(); b(); }; 131 | } 132 | ``` 133 | 134 | 然后就可以像下面这样使用上面的定义: 135 | 136 | ```csharp 137 | Monoid todo = new Todo(); 138 | todo.Appends( 139 | () => Console.WriteLine("logic1"), 140 | todo.When(true, () => Console.WriteLine("logic2 When true")), 141 | todo.Cond(false, 142 | () => Console.WriteLine("logic3 Cond true"), 143 | () => Console.WriteLine("logic4 Cond false")) 144 | )(); 145 | // logic1 146 | // logic2 When true 147 | // logic4 Cond false 148 | ``` 149 | 150 | > 注:上面的 Optional 并不是 lazy 的,实际运用中加上非空短路能提高效率。 -------------------------------------------------------------------------------- /doc/Monoid_zh-Hant.md: -------------------------------------------------------------------------------- 1 | # 十分鐘魔法練習:單位半群 2 | 3 | ### By 「玩火」,改寫「CWKSC」 4 | 5 | > 前置技能:C# (IEnumerable, Aggregate(), Extension Methods, IComparable) 6 | 7 | ## Semigroup 半群 8 | 9 | 半群是一種代數結構,在集合 `A` 上包含一個將兩個 `A` 的元素映射到 `A` 上的運算即 `<> : (A, A) -> A` ,同時該運算滿足**結合律**即 `(a <> b) <> c == a <> (b <> c)` ,那麼代數結構 `{<>, A}` 就是一個半群。 10 | 11 | 比如在自然數集上的加法或者乘法可以構成一個半群,再比如字符串集上字符串的連接構成一個半群。 12 | 13 | ```csharp 14 | // 二元運算 15 | public interface BinaryOperation { public T BinaryOperation(T a, T b); } 16 | // 半群 17 | public interface Semigroup : BinaryOperation { } 18 | ``` 19 | 20 | ## Monoid 單位半群 21 | 22 | 單位半群是一種帶單位元的半群,對於集合`A` 上的半群`{<>, A}` , `A` 中的元素 `a` 使 `A` 中的所有元素 `x ` 滿足 `x <> a` 和 `a <> x` 都等於 `x`,則 `a` 就是 `{<>, A}` 上的單位元。 23 | 24 | 舉個例子, `{+, 自然數集}` 的單位元就是 0 , `{*, 自然數集}` 的單位元就是 1 , `{+, 字符串集}` 的單位元就是空串 `"" ` 。 25 | 26 | ```csharp 27 | // 單位元 28 | public interface Unital { public T Identity(); } 29 | // 單位半群 30 | public interface Monoid : Unital, Semigroup { } 31 | ``` 32 | 33 | 對於 `Monoid` 這種代數結構,可以折疊 `fold` / `reduce`: 34 | 35 | ```csharp 36 | public static T Appends(this Monoid monoid, IEnumerable x) => 37 | x.Aggregate(monoid.Identity(), monoid.BinaryOperation); 38 | 39 | // 下面這個是為了方便使用 40 | public static T Appends(this Monoid monoid, params T[] x) => 41 | monoid.Appends((IEnumerable)x); 42 | ``` 43 | 44 | 對於其他在 `Monoid` 上使用的運算,可以用 C# 中的 Extension Methods 添加。 45 | 46 | ## 應用:Optional 47 | 48 | 在 C# 中可以用 `T?` 或者 `Nullable` 可以用來表示可能有值的類型,我們可以對它定義個 `Monoid` : 49 | 50 | ```csharp 51 | public class OptionalM : Monoid where T : struct 52 | { 53 | public T? Identity() => null; 54 | public T? BinaryOperation(T? a, T? b) => a ?? b; 55 | } 56 | ``` 57 | 58 | 這樣 `appends` 將獲得一串 `T?` 中第一個不為空的值,對於需要進行一連串嘗試操作可以這樣寫: 59 | 60 | ```csharp 61 | new OptionalM().Appends(null, 2, 3) // 2 62 | ``` 63 | 64 | ## 應用:Ordering 65 | 66 | 存在一個 `Student` 類 67 | 68 | ```csharp 69 | public class Student : IComparable 70 | { 71 | public string name; 72 | public string sex; 73 | public DateTime birthday; 74 | public string from; 75 | } 76 | ``` 77 | 78 | 如果想對其實現 `IComparable` 接口,正常情況下: 79 | 80 | ```csharp 81 | public int CompareTo(Student s) => 82 | name.CompareTo(s.name) != 0 ? name.CompareTo(s.name) : 83 | sex.CompareTo(s.sex) != 0 ? sex.CompareTo(s.sex) : 84 | birthday.CompareTo(s.birthday) != 0 ? birthday.CompareTo(s.birthday) : 85 | from.CompareTo(s.from) != 0 ? from.CompareTo(s.from) : 0; 86 | ``` 87 | 88 | 對於 `IComparable` ,可以構造出一個 `Monoid`: 89 | 90 | ```csharp 91 | public class OrderingM : Monoid 92 | { 93 | public int Identity() => 0; 94 | public int BinaryOperation(int a, int b) => a == 0 ? b : a; 95 | } 96 | ``` 97 | 98 | 同樣如果有一串帶有優先級的比較操作就可以用 `appends` 串起來,比如: 99 | 100 | ```csharp 101 | public int CompareTo(Student student) => 102 | OrderingM().Appends( 103 | name.CompareTo(student.name), 104 | sex.CompareTo(student.sex), 105 | birthday.CompareTo(student.birthday), 106 | from.CompareTo(student.from) 107 | ); 108 | ``` 109 | 110 | 這樣的寫法比一連串 `if-else` 或者 `?:` 優雅太多。 111 | 112 | ## 擴展 113 | 114 | 用 Extension Methods 對 `Monoid` 添加方法,支持更多方便的操作: 115 | 116 | ```csharp 117 | public static T When(this Monoid monoid, bool c, T then) => 118 | c ? then : monoid.Identity(); 119 | public static T Cond(this Monoid monoid, bool c, T then, T els) => 120 | c ? then : els; 121 | ``` 122 | 123 | 存在一個 `Todo` 類, 124 | 125 | ```csharp 126 | public class Todo : Monoid 127 | { 128 | public Action Identity() => () => { }; 129 | public Action BinaryOperation(Action a, Action b) => 130 | () => { a(); b(); }; 131 | } 132 | ``` 133 | 134 | 然後就可以像下面這樣使用上面的定義: 135 | 136 | ```csharp 137 | Monoid todo = new Todo(); 138 | todo.Appends( 139 | () => Console.WriteLine("logic1"), 140 | todo.When(true, () => Console.WriteLine("logic2 When true")), 141 | todo.Cond(false, 142 | () => Console.WriteLine("logic3 Cond true"), 143 | () => Console.WriteLine("logic4 Cond false")) 144 | )(); 145 | // logic1 146 | // logic2 When true 147 | // logic4 Cond false 148 | ``` 149 | 150 | > 注:上面的 Optional 並不是 lazy 的,實際運用中加上非空短路能提高效率。 -------------------------------------------------------------------------------- /doc/Mu.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习: μ 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:Java 基础,构造演算, Y 组合子 6 | 7 | ## Y 组合子的类型 8 | 9 | Y 组合子在无类型 λ 演算中是这样定义的 10 | 11 | ``` 12 | Y = λ f. (λ x. f (x x)) (λ x. f (x x)) 13 | ``` 14 | 15 | 但是它的类型是什么呢?实际上我们可以构造如下式子来分析: 16 | 17 | ``` 18 | Y f = f (Y f) 19 | ``` 20 | 21 | 假设 `f` 的类型为 `A` , `Y` 的类型为 `A → B` ,那么 `Y f` 的类型就是 `B` ,所以等式右边的类型也应该为 `B` ,那么 `f` 的类型就应当是 `A = B → B` ,解得 `Y` 的类型为 `(B → B) → B` 。这就解得了 Y 组合子的宏观上的类型 `π T: *. (T → T) → T` 。 22 | 23 | 不过如果想完整地写出 Y 组合子中每个变量的类型就会遇到困难, `x` 的类型是什么呢?如果假设 `x` 的类型是 `A → B` 那么考虑到存在 `x x` 这样的结构, `A` 应该就是 `A → B` ,但这样又回到了最开始的问题,假设 `x` 的类型是 `(A → B) → B` 。显然这样最后并不收敛, `x` 的类型没有解。 24 | 25 | 既然 Y 组合子中有些变量的类型无法解出那么实际上在 Lambda Cube 中的各个类型系统都是无法构造 Y 组合子的,这就意味着这些类型系统没有办法递归,而通过这些类型系统检查的程序是一定会停机的。换句话说其实这些演算是不图灵完备的。 26 | 27 | ## μ 28 | 29 | 所以实际上虽然看上去类似 Y 的不动点组合子是存在的但是无法给出具体实现,这时候就需要在构造演算中开个洞来获得不动点组合子。在表达式中增加 `μ x: A. e` 的结构,等价于 `Y (λ x: A. e)` ,注意 `Y` 的类型是 `(A → A) → A` ,而对应的 `λ x: A. e` 的类型是 `A → A` ,也就是说 `e` 的类型是 `A` 。调用 `μ` 的 `reduce` 得到 `e[x → μ x: A. e]` 。 30 | 31 | ```java 32 | class Mu implements Expr { 33 | Val x; 34 | Expr e; 35 | 36 | Expr open() { // unfold 37 | return e.apply(x, this); 38 | } 39 | 40 | public Expr reduce() { 41 | return this; 42 | } 43 | 44 | public Expr fullReduce() { 45 | return this; 46 | } 47 | 48 | public Expr checkType(Env env) throws BadTypeException { 49 | Pi pi = new Pi(x, e.checkType(new ConsEnv(x, env))); 50 | if (pi.checkType(env) instanceof Sort && 51 | pi.e.fullReduce().equals(pi.x.t.fullReduce())) 52 | return pi.e; 53 | throw new BadTypeException(); 54 | } 55 | 56 | public boolean equals(Object o) { 57 | if (this == o) return true; 58 | if (o == null) return false; 59 | if (getClass() != e.getClass()) 60 | return Objects.equals(open(), e); 61 | // ^^^^^^^^^^^^^^^^^^^^^^^^^ 62 | Mu mu = (Mu) o; 63 | return Objects.equals(e.apply(x, mu.x), mu.e); 64 | // ^^^^^^^^^^^^^^^^ 65 | } 66 | } 67 | ``` 68 | 69 | 注意在比较的时候如果类型不一致需要尝试展开再继续比较,而且对于其他所有的表达式元素在比较时都需要对 μ 特殊处理,这里拿 `Fun` 举个例子: 70 | 71 | ```java 72 | class Fun implements Expr { 73 | public boolean equals(Object o) { 74 | if (this == o) return true; 75 | if (o == null) return false; 76 | // Or: equals(((Mu) o).open()) 77 | if (o instanceof Mu) return o.equals(this); 78 | // ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ 79 | if (getClass() != o.getClass()) return false; 80 | Fun fun = (Fun) o; 81 | return Objects.equals(e, fun.e.apply(fun.x, x)); 82 | } 83 | } 84 | ``` 85 | 86 | 在 `App` 中碰到 μ 也要展开一下: 87 | 88 | ```java 89 | class App implements Expr { 90 | public Expr reduce() { 91 | Expr fr = f.reduce(); 92 | if (fr instanceof Mu) fr = ((Mu) fr).open(); 93 | // ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ 94 | if (fr instanceof Fun) { 95 | Fun fun = (Fun) fr; 96 | return fun.e.apply(fun.x, x).reduce(); 97 | } 98 | return new App(fr.reduce(), x.reduce()); 99 | } 100 | 101 | public Expr fullReduce() { 102 | Expr fr = f.reduce(); 103 | if (fr instanceof Mu) fr = ((Mu) fr).open(); 104 | // ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ 105 | if (fr instanceof Fun) { 106 | Fun fun = (Fun) fr; 107 | return fun.e.apply(fun.x, x).fullReduce(); 108 | } 109 | return new App(fr.fullReduce(), x.fullReduce()); 110 | } 111 | 112 | public Expr checkType(Env env) throws BadTypeException { 113 | Expr tf = f.checkType(env).fullReduce(); 114 | if (tf instanceof Mu) tf = ((Mu) tf).open(); 115 | // ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^ 116 | if (tf instanceof Pi) { 117 | Pi pi = (Pi) tf; 118 | if (x.checkType(env).fullReduce().equals(pi.x.checkType(env).fullReduce())) 119 | return pi.e.apply(pi.x, x); 120 | } 121 | throw new BadTypeException(); 122 | } 123 | } 124 | ``` 125 | 126 | μ 只在使用的时候才会展开一次,这是为了防止出现求值时无限递归的情况。 127 | 128 | > 实际上 μ 这个东西只是用来处理类型的,但是 CoC 里面不区分类型和项所以这里也用于项的递归了。 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /doc/Parsec.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:解析器组合子 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:Java基础,HKT,Monad 6 | 7 | ## 常见组合子 8 | 9 | 解析器单子那期的最后给出了 `map` 和 `combine` 的定义,而 `combine` 可以进一步组合出只取自己结果和只取参数结果的组合子: 10 | 11 | ```java 12 | // class Parser 13 | 14 | // 忽略参数解析器的解析结果 15 | Parser 16 | skip(Parser p) { 17 | return combine(p, (a, b) -> a); 18 | } 19 | // 使用参数解析器的解析结果 20 | Parser 21 | use(Parser p) { 22 | return combine(p, (a, b) -> b); 23 | } 24 | ``` 25 | 26 | `or` 组合子可以在解析失败的时候用参数解析器来重新解析: 27 | 28 | ```java 29 | // class Parser 30 | Parser 31 | or(Parser p) { 32 | return new Parser<>(s -> { 33 | Maybe> 34 | r = runParser(s); 35 | if (r.value == null) 36 | return p.runParser(s); 37 | return r; 38 | }); 39 | } 40 | ``` 41 | 42 | `many` 组合子用来构造匹配任意个相同的解析器的解析器,用了 List 来返回结果,所以代码比较复杂: 43 | 44 | ```java 45 | // class Parser 46 | Parser> 47 | many() { 48 | return new Parser<>(s -> { 49 | Maybe> 50 | r = runParser(s); 51 | if (r.value == null) 52 | return new Maybe<>( 53 | new Pair<>( 54 | new ArrayList<>(), s)); 55 | Maybe, ParseState>> 56 | rs = many().runParser( 57 | r.value.second); 58 | rs.value.first.add(0, 59 | r.value.first); 60 | return rs; 61 | }); 62 | } 63 | ``` 64 | 65 | `some` 组合子利用 `many` 定义,用来构造匹配一个及以上相同的解析器的解析器: 66 | 67 | ```java 68 | // class Parser 69 | Parser> 70 | some() { 71 | return combine(many(), (x, xs) -> { 72 | xs.add(0, x); 73 | return xs; 74 | }); 75 | } 76 | ``` 77 | 78 | ## 常见解析器 79 | 80 | 最基本的是 `id` 解析器,解析任意一个字符并作为解析结果返回: 81 | 82 | ```java 83 | static Parser id = 84 | new Parser<>(s -> { 85 | 86 | if (s.p == s.s.length()) 87 | return new Maybe<>(); 88 | 89 | return new Maybe<>( 90 | new Pair<>(s.s.charAt(s.p), 91 | s.next())); 92 | 93 | }); 94 | ``` 95 | 96 | 有了 `id` 解析器之后构造的解析器就只需要把 `id` 和组合子组合而不需要再关心解析一个字符的细节。 97 | 98 | 最常用的解析器就是 `pred` ,解析一个符合要求的字符: 99 | 100 | ```java 101 | static Parser 102 | pred(Predicate f) { 103 | ParserM m = new ParserM(); 104 | return narrow( 105 | m.flatMap(id, 106 | c -> f.test(c) ? 107 | m.pure(c) : 108 | m.fail())); 109 | } 110 | ``` 111 | 112 | 另一个常用的解析器是 `character` ,解析特定字符: 113 | 114 | ```java 115 | static Parser 116 | character(char x) { 117 | return pred(c -> c == x); 118 | } 119 | ``` 120 | 121 | ## 组合 122 | 123 | 既然叫解析器组合子,那么它们是用来组合的,这里给出如何用它们组合出一个解析浮点数的解析器例子: 124 | 125 | ```java 126 | // 解析一个数字字符 127 | static Parser digit = 128 | pred(c -> '0' <= c && c <= '9') 129 | p(c -> c - '0'); 130 | // 解析一个自然数 131 | static Parser nat = 132 | digit.some().map(xs -> { 133 | int x = 0; 134 | for (int i : xs) x = x * 10 + i; 135 | return x; 136 | }); 137 | // 解析一个整数 138 | static Parser integer = 139 | (character('-').use(nat).map(x -> -x)) 140 | .or(nat); 141 | // 解析一个浮点数 142 | static Parser real = 143 | (integer.combine(character('.') 144 | .use(digit.some()).map(xs -> { 145 | double ans = 0, base = 0.1; 146 | for (int i : xs) { 147 | ans += base * i; 148 | base *= 0.1; 149 | } 150 | return ans; 151 | }), 152 | Double::sum)).or(integer 153 | .map(Integer::doubleValue)); 154 | ``` 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /doc/ParserM.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:解析器单子 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:Java基础,HKT,Monad 6 | 7 | 解析器(Parser)是编译器的一部分,它读取源代码(Source Code),输出一个抽象语法树(Abstract Syntax Tree, AST)。某种程度上来说,解析器是一种可组合的东西,字符解析器组成了整数解析器,整数解析器组成了浮点数解析器。 8 | 9 | 这样可组合的解析器有一个抽象的函数表达: 10 | 11 | ```java 12 | Function>> 14 | ``` 15 | 16 | 其中 `ParseState` 是包含当前解析位置的源文本的类型。返回值用 `Maybe` 包起来因为解析可能失败。 `A` 就是解析出来的具体数据类型。返回值中包括解析后的状态 `ParseState` ,这样就可以传递给下一个解析器函数,从而组合多个解析器。 17 | 18 | 而且很显然这个函数是一个 Monad ,为了为它实现 Monad 需要用 HKT 包装一下: 19 | 20 | ```java 21 | class Parser 22 | implements HKT, A> { 23 | 24 | static Parser 25 | narrow(HKT, A> v) { 26 | return (Parser) v; 27 | } 28 | 29 | Function>> 31 | parser; 32 | 33 | Parser(Function>> f) { 35 | parser = f; 36 | } 37 | 38 | Maybe> 39 | runParser(ParseState s) { 40 | return parser.apply(s); 41 | } 42 | 43 | Maybe parse(String s) { 44 | MaybeM m = new MaybeM(); 45 | return Maybe.narrow( 46 | m.flatMap(runParser( 47 | new ParseState(s)), 48 | r -> m.pure(r.first))); 49 | } 50 | } 51 | ``` 52 | 53 | 然后就可以实现 Parser Monad 了: 54 | 55 | ```java 56 | class ParserM 57 | implements Monad> { 58 | 59 | public HKT, A> 60 | pure(A v) { 61 | return new Parser<>(s -> 62 | new Maybe<>( 63 | new Pair<>(v, s))); 64 | } 65 | 66 | public HKT, A> 67 | fail() { 68 | return new Parser<>(s -> 69 | new Maybe<>()); 70 | } 71 | 72 | public HKT, B> 73 | flatMap(HKT, A> ma, 74 | Function, B>> f) { 76 | 77 | return new Parser<>(s -> { 78 | MaybeM m = new MaybeM(); 79 | // 一点伪代码(not Haskell) 80 | // do 81 | // r <- ma.runParser(s) 82 | // f(r.first).runParser( 83 | // r.second) 84 | return Maybe.narrow( 85 | m.flatMap(Parser 86 | .narrow(ma) 87 | .runParser(s), 88 | r -> Parser 89 | .narrow(f.apply( 90 | r.first)) 91 | .runParser( 92 | r.second)) 93 | ); 94 | }); 95 | } 96 | } 97 | 98 | ``` 99 | 100 | 实现了 Monad 以后写 Parser 就可以不用管理错误回溯也不用手动传递解析状态,只需要把解析器看成一个抽象的容器,取出解析结果,组合,再放回容器。 101 | 102 | 这里举两个用 Parser Monad 写的组合函数: 103 | 104 | ```java 105 | // class Parser 106 | Parser 107 | map(Function f) { 108 | ParserM m = new ParserM(); 109 | // do 110 | // x <- this 111 | // pure (f(x)) 112 | return narrow( 113 | m.flatMap(this, 114 | x -> m.pure(f.apply(x)))); 115 | } 116 | 117 | Parser 118 | combine(Parser p, 119 | BiFunction f) { 120 | ParserM m = new ParserM(); 121 | // do 122 | // a <- this 123 | // b <- p 124 | // pure (f(a, b)) 125 | return narrow( 126 | m.flatMap(this, 127 | a -> m.flatMap(p, 128 | b -> m.pure(f.apply(a, b))))); 129 | } 130 | ``` 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /doc/PiSigma.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:π 类型和 Σ 类型 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:ADT,构造演算 6 | 7 | ## π 类型 8 | 9 | 构造演算中包含一种 `* → □` 的函数,这种函数的类型被称为 π 类型。名字很奇怪,这个类型和 π 毫无关系的啊。实际上这个名字是这么来的: 10 | 11 | 首先构造一个 π 类型的函数 `F: T → U` ,在其定义域 `T` 中任取一个值 `x` 那么可以构造类型 `F(x)` ,那么实际上可以对定义域中每个值都这么操作并且把构造出的类型全部统统放入一个有名元组中 12 | 13 | ````` 14 | (x_0: F(x_0), x_1: F(x_1), ... , x_n: F(x_n)) 15 | ````` 16 | 17 | 这样的定义实际上是和原来的函数等价的,而且这个元组的类型实际上就是一个 ADT 中的积类型 18 | 19 | ``` 20 | F(x_0) * F(x_1) * ... * F(x_n) 21 | ``` 22 | 23 | 用数学的连乘符号可以表示为 24 | 25 | ``` 26 | π_{x: T} F(x) 27 | ``` 28 | 29 | 这里有个 π 所以它就被称为 π 类型。 30 | 31 | ## Σ 类型 32 | 33 | 设值 `x: T` ,和 π 类型函数 `F: T → U` 那么元组 `(x, F(x))` 的类型就是 Σ 类型。实际上有了上面的例子,这里也挺好理解的,对于类型 `T` 的所有 `x` 可以构造出 34 | 35 | ``` 36 | x_0: F(x_0) | x_1: F(x_1) | ... | x_n: F(x_n) 37 | ``` 38 | 39 | 这个类型和那个元组的类型是等价的,而它就是 ADT 中的和类型 40 | 41 | ``` 42 | F(x_0) + F(x_1) + ... + F(x_n) 43 | ``` 44 | 45 | 用数学的求和符号可以表示为 46 | 47 | ``` 48 | Σ_{x: T} F(x) 49 | ``` 50 | 51 | 所以它就被称为 Σ 类型。 52 | 53 | > 题外话:个人感觉这样命名类型挺离谱的,拿个符号就来命名,本来符号意思就多,放在这里一眼看上去真猜不出来是啥意思,我觉得叫总积/求和类型都看上去好理解些。 -------------------------------------------------------------------------------- /doc/STLC.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:简单类型 λ 演算 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:Java 基础,ADT,λ 演算 6 | 7 | ## 简单类型 λ 演算 8 | 9 | 简单类型 λ 演算(Simply-Typed Lambda Calculus)是在无类型 λ 演算(Untyped Lambda Calculus)的基础上加了个非常简单的类型系统。 10 | 11 | 这个类型系统包含两种类型结构,一种是内建的基础类型 `T` ,一种是函数类型 `A → B` ,其中函数类型由源类型 `A` 和目标类型 `B` 组成: 12 | 13 | ``` 14 | Type = BaseType + FunctionType 15 | FunctionType = Type * Type 16 | ``` 17 | 18 | 注意函数类型的符号是右结合的,也就是说 `A → A → A` 等价于 `A → (A → A)` 。 19 | 20 | 用 Java 代码可以表示为: 21 | 22 | ```java 23 | // 构造函数, equals 已省去 24 | interface Type {} 25 | // BaseType 26 | class TVal implements Type { 27 | String name; 28 | public String toString() { 29 | return name; 30 | } 31 | } 32 | // FunctionType 33 | class TArr implements Type { 34 | Type src, tar; 35 | public String toString() { 36 | return "(" + src + " → " + tar + ")"; 37 | } 38 | } 39 | ``` 40 | 41 | ## 年轻人的第一个 TypeChecker 42 | 43 | 然后需把类型嵌入到 λ 演算的语法树中: 44 | 45 | ```java 46 | // 构造函数, toString 已省去 47 | class Val implements Expr { 48 | String x; 49 | Type type; 50 | } 51 | class Fun implements Expr { 52 | Val x; 53 | Expr e; 54 | } 55 | class App implements Expr { 56 | Expr f, x; 57 | } 58 | ``` 59 | 60 | 注意只有函数定义的变量需要标记类型,表达式的类型是可以被简单推导出的。同时还需要一个环境来保存定义变量的类型(其实是一个不可变链表): 61 | 62 | ```java 63 | interface Env { 64 | Type lookup(String s) throws BadTypeException; 65 | } 66 | class NilEnv implements Env { 67 | public Type lookup(String s) throws BadTypeException { 68 | throw new BadTypeException(); 69 | } 70 | } 71 | class ConsEnv implements Env { 72 | Val v; 73 | Env next; 74 | public Type lookup(String s) throws BadTypeException { 75 | if (s.equals(v.x)) return v.type; 76 | return next.lookup(s); 77 | } 78 | } 79 | ``` 80 | 81 | 而对于这样简单的模型,类型检查只需要判断 `F X` 中的 `F` 需要是函数类型,并且 `(λ x. F) E` 中 `x` 的类型和 `E` 的类型一致。 82 | 83 | 而类型推导也很简单:变量的类型就是它被标记的类型;函数定义的类型就是以它变量的标记类型为源,它函数体的类型为目标的函数类型;而函数应用的类型就是函数的目标类型,在能通过类型检查的情况下。 84 | 85 | 以上用 Java 代码描述就是: 86 | 87 | ```java 88 | // 构造函数, toString 已省去 89 | interface Expr { 90 | Type checkType(Env env) throws BadTypeException; 91 | } 92 | class Val implements Expr { 93 | public Type checkType(Env env) throws BadTypeException { 94 | if (type != null) return type; 95 | return env.lookup(x); 96 | } 97 | } 98 | class Fun implements Expr { 99 | public Type checkType(Env env) throws BadTypeException { 100 | return new TArr(x.type, e.checkType(new ConsEnv(x, env))); 101 | } 102 | } 103 | class App implements Expr { 104 | public Type checkType(Env env) throws BadTypeException { 105 | Type tf = f.checkType(env); 106 | if (tf instanceof TArr && 107 | ((TArr) tf).src.equals(x.checkType(env))) 108 | return ((TArr) tf).tar; 109 | else throw new BadTypeException(); 110 | } 111 | } 112 | ``` 113 | 114 | 下面的测试代码对 115 | 116 | ```` 117 | (λ (x: int). (λ (y: int → bool). (y x))) 118 | ```` 119 | 120 | 进行了类型检查,会打印输出 `(int → ((int → bool) → bool))` : 121 | 122 | ```java 123 | public interface STLambda { 124 | static void main(String[] args) throws BadTypeException { 125 | System.out.println( 126 | new Fun(new Val("x", new TVal("int")), 127 | new Fun(new Val("y", new TArr(new TVal("int"), new TVal("bool"))), 128 | new App(new Val("y"), new Val("x")))).checkType(new NilEnv())); 129 | } 130 | } 131 | ``` 132 | 133 | 而如果对 134 | 135 | ``` 136 | (λ (x: bool). (λ (y: int → bool). (y x))) 137 | ``` 138 | 139 | 进行类型检查就会抛出 `BadTypeException` 。 -------------------------------------------------------------------------------- /doc/ScottE.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:斯科特编码 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:构造演算, ADT ,μ 6 | 7 | 斯科特编码(Scott Encoding)可以在 λ 演算上编码 ADT 。其核心思想就是利用解构函数来处理和类型不同的分支,比如对于如下类型: 8 | 9 | ``` 10 | Either = Left + Right 11 | ``` 12 | 13 | 在构造演算中拥有类型: 14 | 15 | ``` 16 | Either = λ A: *. λ B: *. (π C: *. (A → C) → (B → C) → C) 17 | ``` 18 | 19 | 它接受两个解构函数,分别用来处理 Left 分支和 Right 分支然后返回其中一个分支的处理结果。可以按照这个类型签名构造出以下两个类型构造器: 20 | 21 | ``` 22 | Left = λ A: *. λ B: *. λ val: A. (λ C: *. λ l: A → C. λ r: B → C. l val) 23 | Right = λ A: *. λ B: *. λ val: B. (λ C: *. λ l: A → C. λ r: B → C. r val) 24 | ``` 25 | 26 | 乍一看挺复杂的,不过两个构造器具有非常相似的结构,区别仅仅是 `val` 的类型和最内侧调用的函数。实际上构造一个 `Left` 的值时先填入对应 `Either` 的类型参数然后再填入储存的值就可以得到一个符合 `Either` 类型签名的实例,解构时填入不同分支的解构函数就一定会得到 `Left` 分支解构函数处理的结果。 27 | 28 | 再举个 `List` 的例子: 29 | 30 | ``` 31 | List = λ T: *. (μ L: *. (π R: *. R → (T → L → R) → R)) 32 | 33 | Nil = λ T: *. (λ R: *. λ nil: R. λ cons: T → List T → R. nil) 34 | Cons = λ T: *. λ val: T. λ next: List T. 35 | (λ R: *. λ nil: R. λ cons: T → List T → T. cons val next) 36 | 37 | map = λ A: *. λ B: *. λ f: A → B. μ m: List A → List B. 38 | λ list: List A. 39 | list (List B) 40 | (Nil B) 41 | (λ x: A. λ xs: List A. Cons B (f x) (m xs)) 42 | ``` 43 | 44 | 也就是说,积类型 `A * B * ... * Z` 会被翻译为 45 | 46 | ``` 47 | π A: *. π B: *. ... π Z: *. 48 | (π Res: *. (A → B → ... → Z → Res) → Res) 49 | ``` 50 | 51 | 和类型 `A + B + ... + Z` 会被翻译为 52 | 53 | ``` 54 | π A: *. π B: *. ... π Z: *. 55 | (π Res: *. (A → Res) → (B → Res) → ... → (Z → Res) → Res) 56 | ``` 57 | 58 | 并且两者可以互相嵌套从而构成复杂的类型。 59 | 60 | 如果给和类型的每个分支取个名字,并且允许在解构调用的时候按照名字索引,随意改变分支顺序,在解糖阶段把解构函数调整成正确的顺序那么就可以得到很多函数式语言里面的模式匹配(Pattern match)。然后就可以像这样表示 `List` : 61 | 62 | ``` 63 | List = λ T: *. (μ L: *. Nil | Cons T L) 64 | ``` 65 | 66 | 像这样使用 `List` : 67 | 68 | ``` 69 | map = λ A: *. λ B: *. λ f: A → B. μ m: List A → List B. 70 | λ list: List A. 71 | match list (List B) 72 | | Cons → λ x: A. λ xs: List A. Cons B (f x) (m xs) 73 | | Nil → Nil B 74 | ``` 75 | 76 | -------------------------------------------------------------------------------- /doc/StateMonad.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:状态单子 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:C# (Func, Tuple),[HKT](HKT.md),[Monad](Monad.md) 6 | 7 | ## 函数容器 8 | 9 | 很显然 C# 标准库中的各类容器都是可以看成是单子的, `Linq` 也给出了这些类的 `flatMap` 实现。不过在函数式的理论中单子不仅仅可以是实例意义上的容器,也可以是其他抽象意义上的容器,比如函数。 10 | 11 | 对于一个形如 ` Func>` 形式的函数来说,我们可以把它看成包含了一个 `A` 的惰性容器,只有在给出 `S` 的时候才能知道 `A` 的值。对于这样形式的函数我们同样能写出对应的 `flatMap` ,这里就拿状态单子举例子。 12 | 13 | ## State Monad 状态单子 14 | 15 | 状态单子(State Monad)是一种可以包含一个“可变”状态的单子,我这里用了引号是因为尽管状态随着逻辑流在变化但是在内存里面实际上都是不变量。 16 | 17 | 其本质就是在每次状态变化的时候将新状态作为代表接下来逻辑的函数的输入。比如对于: 18 | 19 | ```csharp 20 | i = i + 1; 21 | Console.WriteLine(i); 22 | ``` 23 | 24 | 可以用状态单子的思路改写成: 25 | 26 | ```csharp 27 | ((Action)(v => Console.WriteLine(v)))(i + 1); 28 | ``` 29 | 30 | 最简单的理解就是这样的一个包含函数的对象: 31 | 32 | ```csharp 33 | public interface State { } 34 | public class State : HKT, V>, State 35 | { 36 | public Func runState; 37 | public State(Func f) => runState = f; 38 | public static State Narrow(HKT, V> v) => (State)v; 39 | } 40 | ``` 41 | 42 | 最核心是 `runState` 函数对象,通过组合这个函数对象来使变化的状态在逻辑间传递。 43 | 44 | `State` 是一个 Monad (注释中是简化的伪代码): 45 | 46 | ```csharp 47 | public class StateM : Monad> 48 | { 49 | public HKT, V> Pure(V value) => 50 | new State(state => (state, value)); 51 | 52 | public HKT, B> FlatMap(HKT, A> a, Func, B>> f) => 53 | new State(s => 54 | { 55 | (S state, A value) = State.Narrow(a).runState(s); 56 | return State.Narrow(f(value)).runState(state); 57 | }); 58 | } 59 | ``` 60 | 61 | `pure` 操作直接返回当前状态和给定的值, `flatMap` 操作只需要把 `ma` 中的 `A` 取出来然后传给 `f` ,并处理好 `state` 。 62 | 63 | 仅仅这样的话 `State` 使用起来并不方便,还需要定义一些常用的操作来读取写入状态: 64 | 65 | ```csharp 66 | // 读取 // 67 | public static HKT, S> Get = 68 | new State(s => (s, s)); 69 | 70 | // 写入 // 71 | public static HKT, S> Put(S s) => 72 | new State(any => (s, any)); 73 | 74 | // 修改 // 75 | public static HKT, S> Modify(Func f) => 76 | new State(s => (f(s), s)); 77 | ``` 78 | 79 | 使用的话这里举个求斐波那契数列的例子: 80 | 81 | ```csharp 82 | public static StateM<(int, int)> m = new StateM<(int, int)>(); 83 | 84 | public static State<(int, int), int> Fib(int n) => 85 | n == 0 ? 86 | State<(int, int), int>.Narrow( 87 | m.FlatMap(StateM<(int, int)>.Get, 88 | x => m.Pure(x.Item1))) : 89 | State<(int, int), int>.Narrow( 90 | m.FlatMap(StateM<(int, int)>.Modify(x => (x.Item2, x.Item1 + x.Item2)), 91 | v => Fib(n - 1))); 92 | ``` 93 | 94 | ```csharp 95 | Console.WriteLine(Fib(0).runState((0, 1)).value); // 0 96 | Console.WriteLine(Fib(1).runState((0, 1)).value); // 1 97 | Console.WriteLine(Fib(2).runState((0, 1)).value); // 1 98 | Console.WriteLine(Fib(3).runState((0, 1)).value); // 2 99 | Console.WriteLine(Fib(4).runState((0, 1)).value); // 3 100 | Console.WriteLine(Fib(5).runState((0, 1)).value); // 5 101 | Console.WriteLine(Fib(6).runState((0, 1)).value); // 8 102 | Console.WriteLine(Fib(7).runState((0, 1)).value); // 13 103 | Console.WriteLine(Fib(8).runState((0, 1)).value); // 21 104 | Console.WriteLine(Fib(9).runState((0, 1)).value); // 34 105 | Console.WriteLine(Fib(10).runState((0, 1)).value); // 55 106 | ``` 107 | 108 | `Fib` 函数对应的 Haskell 代码是: 109 | 110 | ```haskell 111 | fib :: Int -> State (Int, Int) Int 112 | fib 0 = do 113 | (_, x) <- get 114 | pure x 115 | fib n = do 116 | modify (\(a, b) -> (b, a + b)) 117 | fib (n - 1) 118 | ``` 119 | 120 | ~~看上去比 C# 版简单很多~~ 121 | 122 | ## 有啥用 123 | 124 | 看到这里肯定有人会拍桌而起:求斐波那契数列我有更简单的写法! 125 | 126 | ```csharp 127 | public static int Fib(int n) { 128 | int[] a = {0, 1, 1}; 129 | for (int i = 0; i < n; i++) 130 | a[(i + 2) % 3] = a[(i + 1) % 3] + 131 | a[i % 3]; 132 | return a[(n + 1) % 3]; 133 | } 134 | ``` 135 | 136 | 但问题是你用变量了啊, `State Monad` 最妙的一点就是全程都是常量而模拟出了变量的感觉。 137 | 138 | 更何况你这里用了数组而不是在递归,如果你递归就会需要在 `fib` 上加一个状态参数, `State Monad` 可以做到在不添加任何函数参数的情况下在函数之间传递参数。 139 | 140 | 同时它还是纯的,也就是说是**可组合**的,把任意两个状态类型相同的 `State Monad` 组合起来并不会有任何问题,比全局变量的解决方案不知道高到哪里去。 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /doc/SysFO.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:系统 F ω 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:Java 基础,ADT,系统 F 6 | 7 | 在 Java 和 C# 中有泛型,在 C++ 中有模板,他们都可以让一个类型接受一些类型产生一个类型,比如: 8 | 9 | ```java 10 | class Just { 11 | T value; 12 | } 13 | ``` 14 | 15 | `Just` 就是个能接受类型参数 `T` 的类型,它被称为类型构造器(Type Constructor)。在系统 F 中加入类型构造器后它被称为系统 F ω (System F ω)。加入了类型构造器后就可以在 λ 演算中构造泛型容器了,比如构造泛型的 List 。 16 | 17 | 实际上这相当于在类型系统中嵌入了一个完整的 λ 演算解释器,所以我们需要在系统 F 的类型系统中加入 `TFun` 来定义一个类型函数 `TApp` 来应用一个类型函数: 18 | 19 | ```java 20 | interface Type { 21 | Type reduce(); 22 | Type fullReduce(); 23 | Type apply(TVal v, Type t); 24 | Type genUUID(); 25 | } 26 | class TVal implements Type { 27 | String x; 28 | UUID id; 29 | } 30 | class TFun implements Type { 31 | TVal x; 32 | Type t; 33 | } 34 | class TApp implements Type { 35 | Type f, x; 36 | } 37 | class TForall implements Type { 38 | TVal x; 39 | Type t; 40 | } 41 | class TArr implements Type { 42 | Type a, b; 43 | public Type reduce() { 44 | return new TArr(a.reduce(), b.reduce()); 45 | } 46 | public Type fullReduce() { 47 | return new TArr(a.fullReduce(), b.fullReduce()); 48 | } 49 | public Type apply(TVal v, Type t) { 50 | return new TArr(a.apply(v, t), b.apply(v, t)); 51 | } 52 | } 53 | ``` 54 | 55 | 其中 `TVal` 、 `TFun` 和 `TApp` 的函数实现和无类型 λ 演算中的表达式基本一致,不过注意要加上 `equals` 函数的实现,并且 `TFun` 在比较前需要把变量替换成一样的, `fullReduce` 函数在 Y 组合子那期中给出了实现,这里就不贴出展示了。而 `TForall` 的实现可以参考 System F , `TArr` 的实现也只是简单进行递归调用,非常简单。 56 | 57 | 而表达式相比系统 F 需要的改动是 `TVal` 在检查类型时需要先调用 `fullReduce` 来化简类型: 58 | 59 | ```java 60 | interface Expr { 61 | Type checkType(Env env) throws BadTypeException; 62 | Expr genUUID(); 63 | Expr applyUUID(TVal v); 64 | } 65 | class Val implements Expr { 66 | String x; 67 | Type t; 68 | public Type checkType(Env env) { 69 | if (t == null) return env.lookup(x); 70 | return t; 71 | } 72 | } 73 | class Fun implements Expr { 74 | Val x; 75 | Expr e; 76 | public Type checkType(Env env) throws BadTypeException { 77 | return new TArr(x.t, e.checkType(new ConsEnv(x, env))); 78 | } 79 | } 80 | class App implements Expr { 81 | Expr f, x; 82 | } 83 | class Forall implements Expr { 84 | TVal x; 85 | Expr e; 86 | } 87 | class AppT implements Expr { 88 | Expr e; 89 | Type t; 90 | } 91 | ``` 92 | 93 | 有了类型构造器,我们就可以表达带有泛型的容器,比如列表(建议只看注释): 94 | 95 | ```java 96 | public interface TypeCons { 97 | // List = λ X. ∀ R. (X → (R → R)) → (R → R) 98 | Type List = new TFun("X", new TForall("R", new TArr( 99 | new TArr(new TVal("X"), new TArr(new TVal("R"), new TVal("R"))), 100 | new TArr(new TVal("R"), new TVal("R"))))).genUUID(); 101 | // nil = Λ X. (Λ R. λ c: X → (R → R). λ n: R. n) 102 | Expr nil = new Forall("X", new Forall("R", new Fun( 103 | new Val("c", new TArr(new TVal("X"), new TArr(new TVal("R"), new TVal("R")))), 104 | new Fun(new Val("n" , new TVal("R")), new Val("n"))))).genUUID(); 105 | // cons = Λ X. λ h: X. λ t: List X. (Λ R. λ c: X → R → R. λ n: R. c h (t R c n)) 106 | Expr cons = new Forall("X", new Fun(new Val("h", new TVal("X")), new Fun( 107 | new Val("t", new TApp(List, new TVal("X"))), 108 | new Forall("R", new Fun( 109 | new Val("c", new TArr(new TVal("X"), new TArr(new TVal("R"), new TVal("R")))), 110 | new Fun(new Val("n", new TVal("R")), new App( 111 | new App(new Val("c"), new Val("h")), 112 | new App(new App(new AppT(new Val("t"), new TVal("R")), 113 | new Val("c")), new Val("n"))))))))).genUUID(); 114 | static void main(String[] args) throws BadTypeException { 115 | // (∀ X. (∀ R. ((X → (R → R)) → (R → R)))) 116 | System.out.println(nil.checkType()); 117 | // (∀ X. (X → ((∀ R. ((X → (R → R)) → (R → R))) → (∀ R. ((X → (R → R)) → (R → R)))))) 118 | System.out.println(cons.checkType()); 119 | } 120 | } 121 | ``` 122 | 123 | 这个列表的构造类似于自然数,每次在原列表的外面套一层来增加一项。 124 | 125 | 注意上面的类型系统中是个无类型的 λ 演算,实际上类型也是可以拥有类型的,被称为种类(Kind)。基础类型和函数类型的种类是 `*` ,而类型构造器的种类是 `* → *` 。而为了增强类型检查器的能力我们也可以先进行种类检查,不过这里并没实现。 126 | 127 | -------------------------------------------------------------------------------- /doc/SystemF.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:系统 F 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:Java 基础,ADT,简单类型 λ 演算 6 | 7 | 简单类型 λ 演算的类型系统非常简单,比常见的 C++, Java 语言的类型系统表现力差远了。而如果往简单类型 λ 演算的表达式中加入类型函数定义和类型函数应用来联系类型和表达式就可以大大增强其表现力,这样的类型系统被称为系统 F (System F)。 8 | 9 | 类型函数定义 `Λ t. E` 定义了一个类型变量 `t` ,可以在表达式 `E` 中使用,其类型是 `∀ t. [Typeof(E)]` 。 10 | 11 | 类型函数应用 `F T` 类似于函数应用,当 `F` 的类型为 `∀ t. E` 时 `F T` 的类型是 `E(t → T)` ,也就是 `E` 中所有自由的 `t` 被替换为 `T` 。 12 | 13 | 比如 `true` 的定义可以写成: 14 | 15 | ``` 16 | true = Λ a. λ (x: a). λ (y: a). (x: a) 17 | ``` 18 | 19 | 其类型是: 20 | 21 | ``` 22 | ∀ a. a → a → a 23 | ``` 24 | 25 | 这就有点类似 Java 中的泛型函数: 26 | 27 | ```java 28 | A True(A x, A y) { 29 | return x; 30 | } 31 | ``` 32 | 33 | 而类型函数应用就像是给函数填入泛型参数的类型,像这样: 34 | 35 | ``` 36 | Λ x. true x 37 | ``` 38 | 39 | 会得到 `true` 本身。 40 | 41 | 表达式中加入了新东西那么显然类型系统也需要有一些改变,系统 F 的类型系统由类型变量 `x` ,类型函数 `∀ t. E` ,函数类型 `a → b` 构成: 42 | 43 | ```java 44 | interface Type {} 45 | class TVal implements Type { 46 | String x; 47 | UUID id; 48 | public String toString() { 49 | return x; 50 | } 51 | } 52 | class TForall implements Type { 53 | TVal x; 54 | Type e; 55 | public String toString() { 56 | return "(∀ " + x + ". " + e + ")"; 57 | } 58 | } 59 | class TArr implements Type { 60 | Type a, b; 61 | public String toString() { 62 | return "(" + a + " → " + b + ")"; 63 | } 64 | } 65 | ``` 66 | 67 | 注意 `TVal` 的 `id` 字段是像无类型 λ 演算中一样 `equals` 函数只需要比较 `id` 字段。 68 | 69 | 既然有类型函数那就需要有类似函数应用的操作来填入类型参数,同时还需要函数来生成 `UUID` : 70 | 71 | ```java 72 | interface Type { 73 | Type apply(TVal x, Type t); 74 | Type genUUID(); 75 | Type applyUUID(TVal v); 76 | } 77 | class TVal implements Type { 78 | public Type apply(TVal x, Type t) { 79 | if (equals(x)) return t; 80 | else return this; 81 | } 82 | public Type genUUID() { return this; } 83 | public Type applyUUID(TVal v) { 84 | if (x.equals(v.x)) return new TVal(x, v.id); 85 | return this; 86 | } 87 | } 88 | class TForall implements Type { 89 | public Type apply(TVal x, Type t) { 90 | if (this.x.equals(x)) return this; 91 | else return e.apply(x, t); 92 | } 93 | public Type genUUID() { 94 | if (x.id == null) { 95 | TVal v = new TVal(x.x, UUID.randomUUID()); 96 | return new TForall(v, e.applyUUID(v).genUUID()); 97 | } 98 | return new TForall(x, e.genUUID()); 99 | } 100 | public Type applyUUID(TVal v) { 101 | if (x.x.equals(v.x)) return this; 102 | return new TForall(x, e.applyUUID(v)); 103 | } 104 | public boolean equals(Object o) { 105 | if (this == o) return true; 106 | if (o == null || getClass() != o.getClass()) return false; 107 | TForall tForall = (TForall) o; 108 | return Objects.equals(e, tForall.e.apply(tForall.x, x)); 109 | // Notice: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 110 | } 111 | } 112 | class TArr implements Type { 113 | public Type apply(TVal x, Type t) { 114 | return new TArr(a.apply(x, t), 115 | b.apply(x, t)); 116 | } 117 | public Type genUUID() { 118 | return new TArr(a.genUUID(), b.genUUID()); 119 | } 120 | public Type applyUUID(TVal v) { 121 | return new TArr(a.applyUUID(v), b.applyUUID(v)); 122 | } 123 | } 124 | ``` 125 | 126 | 这里的实现和无类型 λ 演算很像。不过要注意 `TForall` 的 `equals` 函数实现,在比较前需要先把变量统一一下。 127 | 128 | 再在简单类型 λ 演算的基础上给表达式加上类型函数定义和类型函数应用,同时还需要协助类型系统生成类型的 `UUID` : 129 | 130 | ```java 131 | interface Expr { 132 | Type checkType(Env env) throws BadTypeException; 133 | Expr genUUID(); 134 | Expr applyUUID(TVal v); 135 | } 136 | class Val implements Expr { /* ... */ } 137 | class Fun implements Expr { /* ... */ } 138 | class App implements Expr { /* ... */ } 139 | // 类型函数定义 140 | class Forall implements Expr { 141 | TVal x; 142 | Expr e; 143 | public Type checkType() throws BadTypeException { 144 | return new TForall(x, e.checkType(env)); 145 | } 146 | public Expr genUUID() { 147 | if (x.id == null) { 148 | TVal v = new TVal(x.x, UUID.randomUUID()); 149 | return new Forall(v, e.applyUUID(v).genUUID()); 150 | } 151 | return new Forall(x, e.genUUID()); 152 | } 153 | public Expr applyUUID(TVal v) { 154 | if (x.x.equals(v.x)) return this; 155 | return new Forall(x, e.applyUUID(v)); 156 | } 157 | } 158 | // 类型函数应用 159 | class AppT implements Expr { 160 | Expr e; 161 | Type t; 162 | public Type checkType(Env env) throws BadTypeException { 163 | Type te = e.checkType(env); 164 | if (te instanceof TForall) // 填入类型参数 165 | return ((TForall) te).e.apply(((TForall) te).x, t); 166 | throw new BadTypeException(); 167 | } 168 | public Expr genUUID() { 169 | return new AppT(e.genUUID(), t.genUUID()); 170 | } 171 | public Expr applyUUID(TVal v) { 172 | return new AppT(e.applyUUID(v), t.applyUUID(v)); 173 | } 174 | } 175 | ``` 176 | 177 | 其中 `Val`, `Fun`, `App` 的定义和简单类型 λ 演算中基本一致,这里不作展示。他们的 `UUID` 生成只需要想 `AppT` 那样递归就可以,无需特殊操作。 178 | 179 | 而测试代码 180 | 181 | ```java 182 | public interface SystemF { 183 | Expr T = new Forall("a", new Fun( 184 | new Val("x", new TVal("a")), 185 | new Fun(new Val("y", new TVal("a")), 186 | new Val("x")))).genUUID(); 187 | Expr F = new Forall("a", new Fun( 188 | new Val("x", new TVal("a")), 189 | new Fun(new Val("y", new TVal("a")), 190 | new Val("y")))).genUUID(); 191 | Type Bool = new TForall("x", new TArr( 192 | new TVal("x"), 193 | new TArr(new TVal("x"), new TVal("x")))).genUUID(); 194 | Expr IF = new Forall("a", new Fun( 195 | new Val("b", Bool), 196 | new Fun(new Val("x", new TVal("a")), new Fun( 197 | new Val("y", new TVal("a")), 198 | new App(new App( 199 | new AppT(new Val("b"), new TVal("a")), 200 | new Val("x")), new Val("y")))))).genUUID(); 201 | static void main(String[] args) throws BadTypeException { 202 | System.out.println(T.checkType(new NilEnv())); 203 | System.out.println(IF.checkType(new NilEnv())); 204 | } 205 | } 206 | ``` 207 | 208 | 运行会输出: 209 | 210 | ``` 211 | (∀ a. (a → (a → a))) 212 | (∀ a. ((∀ x. (x → (x → x))) → (a → (a → a)))) 213 | ``` 214 | 215 | -------------------------------------------------------------------------------- /doc/TableDriven.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:表驱动编程 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能: 简单Java基础 6 | 7 | ## Intro 8 | 9 | 表驱动编程被称为是普通程序员和高级程序员的分水岭,而它本身并没有那么难,甚至很多时候不知道的人也能常常重新发明它。而它本身在我看来是锻炼抽象思维的最佳途径,几乎所有复杂的系统都能利用表驱动法来进行进一步抽象优化,而这也非常考验程序员的水平。 10 | 11 | ## 数据表 12 | 13 | 学编程最开始总会遇到这样的经典习题: 14 | 15 | > 输入成绩,返回等第, 90 以上 A , 80 以上 B , 70 以上 C , 60 以上 D ,否则为 E 16 | 17 | 作为一道考察 `if` 语句的习题初学者总是会写出这样的代码: 18 | 19 | ```java 20 | static char getLevel(int s) { 21 | if (s >= 90) return 'A'; 22 | if (s >= 80) return 'B'; 23 | if (s >= 70) return 'C'; 24 | if (s >= 60) return 'D'; 25 | return 'E'; 26 | } 27 | ``` 28 | 29 | 等学了 `switch` 语句以后有些聪明的人会把它改成 `switch(s/10)` 的写法。 30 | 31 | 但是这两种写法都有个同样的问题:如果需要不断添加等第个数那最终 `getLevel` 函数就会变得很长很长,最终变得不可维护。 32 | 33 | 学会循环和数组后会有聪明人回头再看这个程序,会发现这个程序由反复的 34 | 35 | ```java 36 | if (s >= _) return _; 37 | ``` 38 | 39 | 构成,可以改成循环结构,把对应的数据塞进数组: 40 | 41 | ```java 42 | static int[] score = 43 | {90, 80, 70, 60}; 44 | static char[] level = 45 | {'A', 'B', 'C', 'D', 'E'}; 46 | static char getLeve(int s) { 47 | int pos = 0; 48 | for (; pos < score.length 49 | && s < score[pos]; 50 | pos++); 51 | return level[pos]; 52 | } 53 | ``` 54 | 55 | 这样的好处是只需要在两个数组中添加一个值就能加一组等第而不需要碰 `getLevel` 的逻辑代码。 56 | 57 | 而且进一步讲, `score` 和 `level` 数组可以被存在外部文件中作为配置文件,与源代码分离,这样不用重新编译就能轻松添加一组等第。 58 | 59 | 这就是表驱动编程最初阶的形式,通过抽取相似的逻辑并把不同的数据放入表中来避免逻辑重复,提高可读性和可维护性。 60 | 61 | 再举个带修改的例子,写一个有特定商品的购物车: 62 | 63 | ```java 64 | class ShopList{ 65 | class Item { 66 | String name; 67 | int price; 68 | int count = 0; 69 | Item(String name, int price) { 70 | this.name = name; 71 | this.price = price; 72 | } 73 | } 74 | Item[] items = { 75 | new Item("water", 1), 76 | new Item("cola" , 2), 77 | new Item("choco", 5) 78 | }; 79 | ShopList buy(String name) { 80 | for (Item x : items) 81 | if (x.name.equals(name)) 82 | x.count++; 83 | return this; 84 | } 85 | public String toString() { 86 | return Arrays.stream(items) 87 | .map(x -> 88 | x.name + "($" + 89 | x.price + "/per): " + 90 | x.count) 91 | .collect(Collectors 92 | .joining("\n")); 93 | } 94 | } 95 | ``` 96 | 97 | ## 逻辑表 98 | 99 | 初学者在写习题的时候还会碰到另一种没啥规律的东西,比如: 100 | 101 | > 用户输入 0 时购买 water ,输入 1 时购买 cola ,输入 2 时打印购买的情况,输入 3 退出系统。 102 | 103 | 看似没有可以抽取数据的相似逻辑。但是细想一下,真的没有公共逻辑吗?实际上公共的逻辑在于这些都是在同一个用户输入情况下触发的事件,区别就在于不同输入触发的逻辑不一样,那么其实可以就把逻辑制成表: 104 | 105 | ```java 106 | class SimpleUI { 107 | ShopList list = new ShopList(); 108 | Runnable[] event = { 109 | () -> list.buy("water"), 110 | () -> list.buy("cola"), 111 | () -> System.out.println(list), 112 | () -> System.exit(0) 113 | }; 114 | int[] index = {0, 1, 2, 3}; 115 | void runEvent(int e) { 116 | for (int i = 0; 117 | i < index.length; 118 | i++) 119 | if (index[i] == e) 120 | event[i].run(); 121 | } 122 | } 123 | ``` 124 | 125 | 这样如果需要添加一个用户输入指令只需要在 `event` 表和 `index` 表中添加对应逻辑和索引,修改用户的指令对应的逻辑也变得非常方便,甚至可以把用户指令存在配置文件里提供自定义修改。这样用户输入和时间触发两个逻辑就不会串在一起,维护起来更加方便。 126 | 127 | ## 自动机 128 | 129 | 如果再加个逻辑表能修改的跳转状态就构成了自动机(Automaton)。这里举个例子,利用自动机实现了一个复杂的 UI ,在 `menu` 界面可以选择开始玩或者退出,在 `move` 界面可以选择移动或者打印位置或者返回 `menu` 界面: 130 | 131 | ```java 132 | class ComplexUI { 133 | interface Jumper { 134 | void jump(char c); 135 | } 136 | 137 | // 界面绘制 138 | Runnable[] draw = { 139 | () -> { /* draw menu */ }, 140 | () -> { /* draw play */ } 141 | }; 142 | 143 | Jumper[] jumpers = { 144 | this::menu, 145 | this::move 146 | }; 147 | 148 | int state; 149 | int x = 0, y = 0; 150 | 151 | static class CharEvent { 152 | char c; 153 | Runnable e; 154 | CharEvent(char c, Runnable e) { 155 | this.c = c; 156 | this.e = e; 157 | } 158 | } 159 | 160 | void menu(char c) { 161 | CharEvent[] events = { 162 | new CharEvent('p', () -> 163 | jumpTo(1)), 164 | new CharEvent('q', () -> 165 | System.exit(0)) 166 | }; 167 | for (CharEvent i : events) 168 | if (i.c == c) i.e.run(); 169 | } 170 | 171 | void move(char c) { 172 | CharEvent[] events = { 173 | new CharEvent('w', () -> y++), 174 | new CharEvent('s', () -> y--), 175 | new CharEvent('d', () -> x++), 176 | new CharEvent('a', () -> x--), 177 | new CharEvent('e', () -> 178 | System.out.println( 179 | "{x=" + x + 180 | ";y=" + y + "}")), 181 | new CharEvent('q', () -> 182 | jumpTo(0)) 183 | }; 184 | for (CharEvent i : events) 185 | if (i.c == c) i.e.run(); 186 | } 187 | 188 | private void jumpTo(int s) { 189 | state = s; 190 | draw[state].run(); 191 | } 192 | 193 | void runEvent(char c) { 194 | jumpers[state].jump(c); 195 | } 196 | 197 | { 198 | jumpTo(0); 199 | } 200 | } 201 | ``` 202 | 203 | 实际上更标准的写法应该把状态设定成枚举,这里为了演示的简单期间并没有那样写。 204 | 205 | 同时推荐不用下标作为表的索引标签,并总是把所有相关状态打包起来放在同一个类里面而不是用不同数组的相同下标来访问,这样可以有更加紧凑的语义和更好的缓存命中率。 206 | 207 | -------------------------------------------------------------------------------- /doc/YCombinator.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习:Y 组合子 2 | 3 | ### By 「玩火」 4 | 5 | > 前置技能:Java 基础,λ 演算,λ 演算编码 6 | 7 | ## 递归 8 | 9 | 在 Java 里面实现递归非常简单,只需要在函数内调用函数本身就好了,比如下面的求和程序: 10 | 11 | ```java 12 | int sum(int n) { 13 | if (n == 0) return 0; 14 | else return n + sum(n - 1); 15 | } 16 | ``` 17 | 18 | 这时候就会注意到看起来递归必须要函数有名字,不然怎么调用时表示自己呢?实际上有个很显然的例子: 19 | 20 | ``` 21 | (λ x. x x) (λ x. x x) 22 | ``` 23 | 24 | 这个表达式无论怎么求值都会得到它自身,实际上这就是个无限递归的例子。而在它的基础上稍加修改就可以得到 Y 组合子(Y Combinator): 25 | 26 | ``` 27 | Y = λ f. (λ x. f (x x)) (λ x. f (x x)) 28 | ``` 29 | 30 | 而往 Y 组合子上应用一个函数就会得到: 31 | 32 | ``` 33 | Y g = (λ x. g (x x)) (λ x. g (x x)) 34 | = g ((λ x. g (x x)) (λ x. g (x x))) 35 | = g (Y g) 36 | ``` 37 | 38 | 这样 `g` 就拿到了 `Y g` 也就是它自己的函数作为参数,那么就可以递归了,比如上面的 `sum` 就可以写成: 39 | 40 | ``` 41 | sum' = λ self. λ n. 42 | isZero n 43 | n 44 | (+ n (self (prev n))) 45 | sum = Y sum' 46 | ``` 47 | 48 | `n` 是个丘奇数, `isZero` 判断数字是不是 0 并得到一个 λ 演算编码的布尔值, `+` 函数把两个丘奇数相加, `prev` 函数得到比 `n` 小一的数。 `sum` 在递归到 `n` 为 0 时停止递归。 49 | 50 | ## 求值策略 51 | 52 | 很显然如果直接使用严格求值会无限展开 Y 算子而得不到结果,如果使用惰性求值会得不到易于阅读的结果。这时候就要用一种介于两者之间的求值策略: 53 | 54 | ```java 55 | // class Fun 56 | public Expr fullReduce() { 57 | return new Fun(x, e.fullReduce()); 58 | } 59 | // class App 60 | public Expr fullReduce() { 61 | Expr fr = f.reduce(); 62 | if (fr instanceof Fun) { 63 | Fun fun = (Fun) fr; 64 | return fun.e.apply(fun.x, x).fullReduce(); 65 | } 66 | return new App(fr.fullReduce(), x.fullReduce()); 67 | } 68 | ``` 69 | 70 | 它只有在尝试应用函数失败的时候才会进行完全展开,这样每次只展开一点就可以避免陷入无限递归。 71 | 72 | ## 循环 73 | 74 | 在编码那期中介绍了如何在 λ 演算中构造分支结构,而循环循环可以用递归来表示,每个循环都可以写成循环变量作为参数的尾递归函数,实际上如下的循环: 75 | 76 | ```java 77 | State state; 78 | while (needLoop(state)) { 79 | doSomething(); 80 | state = update(state); 81 | } 82 | ``` 83 | 84 | 都可以写成如下的递归函数: 85 | 86 | ```java 87 | State While(State state) { 88 | if (needLoop(state)) 89 | return While(update(state)); 90 | else return state; 91 | } 92 | ``` 93 | 94 | 这样就可以把任意的循环改写成 λ 演算的形式了。 95 | 96 | -------------------------------------------------------------------------------- /gen.js: -------------------------------------------------------------------------------- 1 | const marked = require('marked') 2 | const fs = require('fs') 3 | const hljs = require('highlight.js') 4 | 5 | if (fs.existsSync("html")) fs.rmdirSync("html", { 6 | recursive: true 7 | }) 8 | 9 | fs.mkdirSync("html") 10 | 11 | const $$ = label => s => '<' + label + '>\n' + s + '\n' 12 | 13 | const charset = '\n' 14 | const viewpoint = '\n' 15 | const title = $$('title')('十分钟魔法练习') 16 | 17 | const hljscss = '' 18 | const materialize = '' 19 | 20 | const sbody = 'body { max-width: 650px; margin: auto; width: 90%; margin-top: 10%; margin-bottom: 10%; color: #0B0E26; background: #FAFAFC; }\n' 21 | const sfont = 'body { font-family: -apple-system,BlinkMacSystemFont,Helvetica Neue,PingFang SC,Microsoft YaHei,Source Han Sans SC,Noto Sans CJK SC,WenQuanYi Micro Hei,sans-serif; }\n' 22 | const sh1 = 'h1 { font-size: 2.5em; color: #EF96AB; }\n' 23 | const sh2 = 'h2 { margin-top: 2em; }\n' 24 | const scenter = 'h1, h2, h3 { text-align: center; }\n' 25 | const squote = 'blockquote { color: gray; margin: 0; padding: 1 1 1 20; border-left: 5px solid #EF96AB; }\n' 26 | const scode = 'code, pre { font-family: Consolas,Menlo,Monaco,source-code-pro,Courier New,monospace; background: #FCF6FC; }\n' 27 | const spre = 'pre { overflow-x: auto; padding: 10px; }\n' 28 | const sscorll = '::-webkit-scrollbar, .element::-webkit-scrollbar, .element { opacity: 0.5; }\n' 29 | const sa = 'a { color: #02AEF1; text-decoration: none; }\n' 30 | 31 | const sdarkbody = '@media (prefers-color-scheme: dark) { body { color: #D8D8D6; background: #0E0E10; } }\n' 32 | const sdarkpre = '@media (prefers-color-scheme: dark) { pre, code { color: #D8D8D6; background: #0E0F1F; } }\n' 33 | 34 | const hlkeyword = '.hljs-keyword { color: #F288AF; }\n' 35 | const hlconmment = '.hljs-comment { color: #929CA6; }\n' 36 | const hlstring = '.hljs-string { color: #0594A6; }\n' 37 | const hltitle = '.hljs-title { color: #4581D9 }\n' 38 | 39 | const hlcss = hlkeyword + hlconmment + hlstring + hltitle 40 | const style = $$('style')(sbody + sfont + sh1 + sh2 + squote + scode + spre + sa + sdarkbody + sdarkpre + hlcss) 41 | 42 | const head = $$('head')(charset + viewpoint + title + style) 43 | 44 | const star = $$('p')('⭐Star me on GitHub⭐') 45 | 46 | const gen = s => { 47 | return $$('html')(head + $$('body')(star + marked(s, { highlight: s => hljs.highlightAuto(s, ['java']).value }))) 48 | } 49 | 50 | fs.readdirSync("doc").forEach(f => { 51 | if (f.endsWith(".md")){ 52 | const content = fs.readFileSync("doc/" + f).toString() 53 | fs.writeFileSync("html/" + f.substr(0, f.length - 3) + ".html", gen(content)) 54 | } 55 | }) 56 | 57 | const index = fs.readFileSync('readme.md').toString() 58 | fs.writeFileSync('index.html', gen(index)) 59 | -------------------------------------------------------------------------------- /html/ADT.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:代数数据类型

28 |

By 「玩火」

29 |
30 |

前置技能:Java基础

31 |
32 |

积类型(Product type)

33 |

积类型是指同时包括多个值的类型,比如 Java 中的 class 就会包括多个字段:

34 |
final class Student {
 35 |     String name;
 36 |     int id;
 37 | }
38 |

而上面这段代码中 Student 的类型中既有 String 类型的值也有 int 类型的值,可以表示为 String 和 int 的「积」,即 String * int

39 |

和类型(Sum type)

40 |

和类型是指可以是某一些类型之一的类型,在 Java 中可以用继承来表示:

41 |
interface SchoolPerson {}
 42 | final class Student implements SchoolPerson {
 43 |     String name;
 44 |     int id;
 45 | }
 46 | final class Teacher implements SchoolPerson {
 47 |     String name;
 48 |     String  office;
 49 | }
50 |

SchoolPerson 可能是 Student 也可能是 Teacher ,可以表示为 Student 和 Teacher 的「和」,即 String * int + String * String 。而使用时只需要用 instanceof 就能知道当前的 StudentPerson 具体是 Student 还是 Teacher 。

51 |

代数数据类型(ADT, Algebraic Data Type)

52 |

由和类型与积类型组合构造出的类型就是代数数据类型,其中代数指的就是和与积的操作。

53 |

利用和类型的枚举特性与积类型的组合特性,我们可以构造出 Java 中本来很基础的基础类型,比如枚举布尔的两个量来构造布尔类型:

54 |
interface Bool {}
 55 | final class True implements Bool {}
 56 | final class False implements Bool {}
57 |

然后用 t instanceof True 就可以用来判定 t 作为 Bool 的值是不是 True 。

58 |

比如利用S的数量表示的自然数:

59 |
interface Nat {}
 60 | final class Z implements Nat {}
 61 | final class S implements Nat {
 62 |     Nat value;
 63 | 
 64 |     S(Nat v) { value = v; }
 65 | }
66 |

这里提一下自然数的皮亚诺构造,一个自然数要么是 0 (也就是上面的 Z ) 要么是比它小一的自然数 +1 (也就是上面的 S ) ,例如3可以用 new S(new S(new S(new Z)) 来表示。

67 |

再比如链表:

68 |
interface List<T> {}
 69 | final class Nil<T> implements List<T> {}
 70 | final class Cons<T> implements List<T> {
 71 |     T value;
 72 |     List<T> next;
 73 | 
 74 |     Cons(T v, List<T> n) {
 75 |         value = v;
 76 |         next = n;
 77 |     }
 78 | }
79 |

[1, 3, 4] 就表示为 new Cons(1, new Cons(3, new Cons(4, new Nil)))

80 |

更奇妙的是代数数据类型对应着数据类型可能的实例数量。

81 |

很显然积类型的实例数量来自各个字段可能情况的组合也就是各字段实例数量相乘,而和类型的实例数量就是各种可能类型的实例数量之和。

82 |

比如 Bool 的类型是 1+1 而其实例只有 True 和 False ,而 Nat 的类型是 1+1+1+... 其中每一个1都代表一个自然数,至于 List 的类型就是1+x(1+x(...)) 也就是 1+x^2+x^3... 其中 x 就是 List 所存对象的实例数量。

83 |

实际运用

84 |

ADT 最适合构造树状的结构,比如解析 JSON 出的结果需要一个聚合数据结构。

85 |
interface JsonValue {}
 86 | final class JsonBool implements JsonValue {
 87 |     boolean value;
 88 | }
 89 | final class JsonInt implements JsonValue {
 90 |     int value;
 91 | }
 92 | final class JsonString implements JsonValue {
 93 |     String value;
 94 | }
 95 | final class JsonArray implements JsonValue {
 96 |     List<JsonValue> value;
 97 | }
 98 | final class JsonMap implements JsonValue {
 99 |     Map<String, JsonValue> value;
100 | }
101 |
102 |

注1:上面的和类型代码都存在用户可能自己写一个子类的问题,更好的写法应该用 Java 14 中的 sealed interface 代替基类。

103 |

注2:上面的写法是基于变量非空假设的,也就是代码中不会出现 null ,所有变量也不为 null 。

104 |
105 | 106 | 107 | -------------------------------------------------------------------------------- /html/Algeff.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:代数作用

28 |

By 「玩火」

29 |
30 |

前置技能:Java基础,续延

31 |
32 |

可恢复异常

33 |

有时候我们希望在异常抛出后经过保存异常信息再跳回原来的地方继续执行。

34 |

显然Java默认异常处理无法直接实现这样的需求,因为在异常抛出时整个调用栈的信息全部被清除了。

35 |

但如果我们有了异常抛出时的续延那么可以同时抛出,在 catch 块中调用这个续延就能恢复之前的执行状态。

36 |

下面是实现可恢复异常的 try-catch

37 |
Stack<BiConsumer<Exception, Runnable>> 
 38 |     cs = new Stack<>();
 39 | 
 40 | void Try(
 41 |         Consumer<Runnable> body,
 42 |         TriConsumer<Exception, 
 43 |                     Runnable, 
 44 |                     Runnable>
 45 |             handler,
 46 |         Runnable cont) {
 47 | 
 48 |     cs.push((e, c) -> 
 49 |             handler.accept(e, cont, c));
 50 |     body.accept(cont);
 51 |     cs.pop();
 52 | }
 53 | 
 54 | void Throw(Exception e, Runnable cont) {
 55 |     cs.peek().accept(e, cont);
 56 | }
57 |

然后就可以像下面这样使用:

58 |
void test(int t) {
 59 |     Try(
 60 |     cont -> {
 61 |         System.out.println("try");
 62 |         if (t == 0)
 63 |             Throw(
 64 |             new ArithmeticException(),
 65 |             () -> {
 66 |                 System.out.println(
 67 |                     "resumed");
 68 |                 cont.run();
 69 |             });
 70 |         else {
 71 |             System.out.println(100 / t);
 72 |             cont.run();
 73 |         }
 74 |     },
 75 |     (e, cont, resume) -> {
 76 |         System.out.println("catch");
 77 |         resume.run();
 78 |     },
 79 |     () -> System.out.println("final"));
 80 | }
81 |

而调用 test(0) 就会得到:

82 |
try
 83 | catch
 84 | resumed
 85 | final
86 |

代数作用

87 |

如果说在刚刚异常恢复的基础上希望在恢复时修补之前的异常错误就需要把之前的 resume 函数加上参数,这样修改以后它就成了代数作用(Algebaic Effect)的基础工具:

88 |
Stack<BiConsumer<Object, 
 89 |                  Consumer<Object>>> 
 90 |     cs = new Stack<>();
 91 | 
 92 | void Try(
 93 |         Consumer<Runnable> body,
 94 |         TriConsumer<Object, 
 95 |                     Runnable, 
 96 |                     Consumer<Object>>
 97 |             handler,
 98 |         Runnable cont) {
 99 | 
100 |     cs.push((e, c) -> 
101 |             handler.accept(e, cont, c));
102 |     body.accept(cont);
103 |     cs.pop();
104 | }
105 | 
106 | void Perform(Object e, 
107 |              Consumer<Object> cont) {
108 |     cs.peek().accept(e, cont);
109 | }
110 |

使用方式如下:

111 |
void test(int t) {
112 |     Try(
113 |     cont -> {
114 |         System.out.println("try");
115 |         if (t == 0)
116 |             Perform(
117 |             new ArithmeticException(),
118 |             v -> {
119 |                 System.out.println(
120 |                     "resumed");
121 |                 System.out.println(
122 |                     100 / (Integer) v);
123 |                 cont.run();
124 |             });
125 |         else {
126 |             System.out.println(100 / t);
127 |             cont.run();
128 |         }
129 |     },
130 |     (e, cont, resume) -> {
131 |         System.out.println("catch");
132 |         resume.accept(1);
133 |     },
134 |     () -> System.out.println("final"));
135 | }
136 |

而这个东西能实现不只是异常的功能,从某种程度上来说它能跨越函数发生作用(Perform Effect)。

137 |

比如说现在有个函数要记录日志,但是它并不关心如何记录日志,输出到标准流还是写入到文件或是上传到数据库。这时候它就可以调用

138 |
Perform(new LogIt(INFO, "test"), ...);
139 |

来发生(Perform)一个记录日志的作用(Effect)然后再回到之前调用的位置继续执行,而具体这个作用产生了什么效果就由调用这个函数的人实现的 try 中的 handler 决定。这样发生作用和执行作用(Handle Effect)就解耦了。

140 |

进一步讲,发生作用和执行作用是可组合的。对于需要发生记录日志的作用,可以预先写一个输出到标准流的的执行器(Handler)一个输出到文件的执行器然后在调用函数的时候按需组合。这也就是它是代数的(Algebiac)的原因。

141 |

细心的读者还会发现这个东西还能跨函数传递数据,在需要某个量的时候调用

142 |
Perform(new Ask("config"), ...);
143 |

就可以获得这个量而不用关心这个量是怎么来的,内存中来还是读取文件或者 HTTP 拉取。从而实现获取和使用的解耦。

144 |

而且这样的操作和状态单子非常非常像,实际上它就是和相比状态单子来说没有修改操作的读取器单子(Reader Monad)同构。

145 |

也就是说把执行器函数作为读取器单子的状态并在发生作用的时候执行对应函数就可以达到和用续延实现的代数作用相同的效果,反过来也同样可以模拟。

146 | 147 | 148 | -------------------------------------------------------------------------------- /html/ChurchE.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:丘奇编码

28 |

By 「玩火」

29 |
30 |

前置技能:λ演算

31 |
32 |

Intro

33 |

众所周知, λ 演算是一个图灵完备的计算模型,它能计算任何图灵机能算的东西。那么很显然它也能表示任何我们平时所用的 C 、 Java 能表示的数据结构。虽然这听起来挺不可思议的,毕竟 λ 演算中本身只有变量、函数定义、函数应用三种结构。

34 |

信息的编码大概是计算机科学中最为接近魔法的内容,凝结了最强的人类的智慧结晶。同一个量的不同表现形式,同构、抽象与组合都让人感到惊叹不已。

35 |

为了方便起见,这里引入一个语法糖 let 绑定(let-binding)来命名表达式:

36 |
x = E
37 | ...后续代码
38 |

它解糖(Desugar)后等价于:

39 |
(λ x. ...后续代码) E
40 |

布尔

41 |

通常来说丘奇编码(Church Encoding)的布尔表达为:

42 |
true  = λ x. λ y. x
43 | false = λ x. λ y. y
44 |

理论上这两个量的定义互相替换后和这种表达也是同构的,不过通常来说大家约定这种表示因为它更符合直觉。

45 |

实际上定义了布尔以后并不需要定义 if ,布尔量本身就可以接替 if 的作用,只需要将 if 的两个分支应用上去:

46 |
(boolValue thenTodo elseTodo)
47 |

如果boolValuetrue那么求值就会得到thenTodo否则会得到elseTodo

48 |

我们不需要 if ,这很神奇。不过为了语义考虑也可以定义一个没有实际意义的 if :

49 |
if = λ x. λ a. λ b. (x a b)
50 |

这样 if true a b 就可以得到 aif false a b 就可以得到 b

51 |

自然数

52 |

皮亚诺构造(Peano Construct)是目前普遍使用的自然数定义。简单来说, 0 用 Z 表示, n 用 n 个 S 和一个 Z 表示。比如 3 就是 SSSZ 。而皮亚诺构造的加法就相当于把一个数的 Z 换成另一个数,就比如 3+3 就是 SSS(SSSZ) 。乘法就相当于把一个数的每个 S 换成另一个数的 S 部分,比如 3*3 就是 (SSS)(SSS)(SSS)Z 。

53 |

而这在 λ 演算中可以表示为:

54 |
0 = λ f. λ x. x
55 | 3 = λ f. λ x. f (f (f x))
56 |

这样的表示方法叫丘奇数(Church number),非常类似于皮亚诺构造。实际上,它是和皮亚诺构造同构的。

57 |

丘奇数的加法和乘法很简单,加法只需要把 x 替换成另一个数就好了,乘法只需要把f替换成另一个数就好了:

58 |
+ = λ m. λ n. (λ f. λ x. m f (n f x))
59 | * = λ m. λ n. (λ f. λ x. m (n f) x)
60 |

而某种程度上来说,一个自然数就是固定次数的循环,以 x 为初始值,把 f 循环执行 n 遍。比如 m*n 就相当于把 m 循环累加加 n 次。

61 |

我们不需要 for ,这很神奇。

62 |

元组

63 |

终于到了数据结构部分, λ 表达式保存数据的原理是把参数全部放在一个接受一个提取器的函数里面:

64 |
pair   = λ a. λ b. λ f. f a b
65 | first  = λ p. p (λ x. λ y. x)
66 | second = λ p. p (λ x. λ y. y)
67 |

这样就可以保证 first (pair x y) 始终等于 xsecond (pair x y) 始终等于 y 。其中 λ x. λ y. xλ x. λ y. y 就是提取器函数。

68 |

进一步讲,把元组串起来就可以变成列表,比如:

69 |
list' = pair a1 (pair a2 (pair a3 ...))
70 |

而如果列表分叉就成了树:

71 |
tree' = pair (pair a1 a2) (pair a3 a4)
72 |

我们用函数构造出了数据结构,这很神奇。

73 | 74 | 75 | -------------------------------------------------------------------------------- /html/CoData.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:余代数数据类型

28 |

By 「玩火」

29 |
30 |

前置技能:Java基础,ADT

31 |
32 |

ADT 的局限性

33 |

很显然, ADT 可以构造任何树形的数据结构:树的节点内分支用和类型连接,层级间节点用积类型连接。

34 |

但是同样很显然 ADT 并不能搞出环形的数据结构或者说是无穷大小的数据结构。比如下面的代码:

35 |
IntList list = new IntCons(1, list);
36 |

编译器会表示 list 在使用时未初始化。

37 |

为什么会这样呢? ADT 是归纳构造的,也就是说它必须从非递归的基本元素开始组合构造成更大的元素。

38 |

如果我们去掉这些基本元素那就没法凭空构造大的元素,也就是说如果去掉归纳的第一步那整个归纳过程毫无意义。

39 |

余代数数据类型

40 |

余代数数据类型(Coalgebraic Data Type)也就是余归纳数据类型(Coinductive Data Type),代表了自顶向下的数据类型构造思路,思考一个类型可以如何被分解从而构造数据类型。

41 |

这样在分解过程中再次使用自己这个数据类型本身就是一件非常自然的事情了。

42 |

不过在编程实现过程中使用自己需要加个惰性数据结构包裹,防止积极求值的语言无限递归生成数据。

43 |

比如一个列表可以被分解为第一项和剩余的列表:

44 |
class InfIntList {
45 |     int head;
46 |     Supplier<InfIntList> next;
47 | 
48 |     InfIntList(
49 |         int head, 
50 |         Supplier<InfIntList> next
51 |     ) {
52 |         this.head = head;
53 |         this.next = next;
54 |     }
55 | }
56 |

这里的 Supplier 可以做到仅在需要 next 的时候才求值。使用的例子如下:

57 |
public class Codata {
58 |     static InfIntList
59 |     infAlt() {
60 |         return new InfIntList(1, 
61 |          () -> new InfIntList(2, 
62 |          Codata::infAlt));
63 |     }
64 | 
65 |     public static void 
66 |     main(String[] args) {
67 |         System.out.println(
68 |             infAlt().next.get().head);
69 |     }
70 | }
71 |

运行会输出 2 。注意,这里的 infAlt 从某种角度来看实际上就是个长度为 2 的环形结构。

72 |

用这样的思路可以构造出无限大的树、带环的图等数据结构。

73 |

不过以上都是对余代数数据类型的一种模拟,实际上在对其支持良好的语言都会自动加上 Supplier 来辅助构造,同时还能处理好对无限大(其实是环)的数据结构的无限递归变换(map, fold ...)的操作。

74 | 75 | 76 | -------------------------------------------------------------------------------- /html/Continuation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:续延

28 |

By 「玩火」

29 |
30 |

前置技能:简单Java基础

31 |
32 |

续延

33 |

续延(Continuation)是指代表一个程序未来的函数,其参数是一个程序过去计算的结果。

34 |

比如对于这个程序:

35 |
void test() {
 36 |     int i = 1;                // 1
 37 |     i++;                      // 2
 38 |     System.out.println(i);    // 3
 39 | }
40 |

它第二行以及之后的续延就是:

41 |
void cont(int i) {
 42 |     i++;                      // 2
 43 |     System.out.println(i);    // 3
 44 | }
45 |

而第三行之后的续延是:

46 |
void cont(int i) {
 47 |     System.out.println(i);    // 3
 48 | }
49 |

实际上可以把这整个程序的每一行改成一个续延然后用函数调用串起来变成和刚才的程序一样的东西:

50 |
void cont1() {
 51 |     int i = 1;                // 1
 52 |     cont2(i);
 53 | }
 54 | void cont2(int i) {
 55 |     i++;                      // 2
 56 |     cont3(i);
 57 | }
 58 | void cont3(int i) {
 59 |     System.out.println(i);    // 3
 60 | }
 61 | void test() {
 62 |     cont1();
 63 | }
64 |

续延传递风格

65 |

续延传递风格(Continuation-Passing Style, CPS)是指把程序的续延作为函数的参数来获取函数返回值的编程思路。

66 |

听上去很难理解,把上面的三个 cont 函数改成CPS就很好理解了:

67 |
void logic1(Consumer<Integer> f) {
 68 |     int i = 1;
 69 |     f.accept(i); // return i
 70 | }
 71 | void logic2(int i, Consumer<Integer> f) {
 72 |     i++;
 73 |     f.accept(i);
 74 | }
 75 | void logic3(int i, Consumer<Integer> f) {
 76 |     System.out.println(i);
 77 |     f.accept(i);
 78 | }
 79 | void test() {
 80 |          logic1( // 获取返回值 i
 81 |     i -> logic2(i, 
 82 |     i -> logic3(i, 
 83 |     i -> {})));
 84 | }
85 |

每个 logic 函数的最后一个参数 f 就是整个程序的续延,而在每个函数的逻辑结束后整个程序的续延也就是未来会被调用。而 test 函数把整个程序组装起来。

86 |

小朋友,你有没有觉得最后的 test 函数写法超眼熟呢?实际上这个写法就是 Monad 的写法, Monad 的写法就是 CPS 。

87 |

另一个角度来说,这也是回调函数的写法,每个 logic 函数完成逻辑后调用了回调函数 f 来完成剩下的逻辑。实际上,异步回调思想很大程度上就是 CPS 。

88 |

有界续延

89 |

考虑有另一个函数 callT 调用了 test 函数,如:

90 |
void callT() {
 91 |     test();
 92 |     System.out.println(3);
 93 | }
94 |

那么对于 logic 函数来说调用的 f 这个续延并不包括 callT 中的打印语句,那么实际上 f 这个续延并不是整个函数的未来而是 test 这个函数局部的未来。

95 |

这样代表局部程序的未来的函数就叫有界续延(Delimited Continuation)。

96 |

实际上在大多时候用的比较多的还是有界续延,因为在 Java 中获取整个程序的续延还是比较困难的,这需要全用 CPS 的写法。

97 |

异常

98 |

拿到了有界续延我们就能实现一大堆控制流魔法,这里拿异常处理举个例子,通过CPS写法自己实现一个 try-throw

99 |

首先最基本的想法是把每次调用 trycatch 函数保存起来,由于 try 可层层嵌套所以每次压入栈中,然后 throw 的时候将最近的 catch 函数取出来调用即可:

100 |
Stack<Consumer<Exception>> cs = 
101 |     new Stack<>();
102 | 
103 | void Try(
104 |         Consumer<Runnable> body,
105 |         BiConsumer<Exception, Runnable> 
106 |                   handler,
107 |         Runnable cont) {
108 | 
109 |     cs.push(e -> handler.accept(e, cont));
110 |     body.accept(cont);
111 |     cs.pop();
112 | }
113 | 
114 | void Throw(Exception e) {
115 |     cs.peek().accept(e);
116 | }
117 |

这里 bodyTryhandler 的最后一个参数都是这个程序的有界续延。

118 |

有了 try-throw 就可以按照CPS风格调用它们来达到处理异常的目的:

119 |
void test(int t) {
120 |     Try(
121 |     cont -> {
122 |         System.out.println("try");
123 |         if (t == 0) Throw(
124 |             new ArithmeticException());
125 |         else {
126 |             System.out.println(100 / t);
127 |             cont.run();
128 |         }
129 |     },
130 |     (e, cont) -> {
131 |         System.out.println("catch");
132 |         cont.run();
133 |     },
134 |     () -> System.out.println("final"));
135 | }
136 |

调用 test(0) 会得到:

137 |
try
138 | catch
139 | final
140 |

而调用 test(1) 会得到:

141 |
try
142 | 100
143 | final
144 | 145 | 146 | -------------------------------------------------------------------------------- /html/DepsInj.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:依赖注入

28 |

By 「玩火」

29 |
30 |

前置技能:Java基础,Monad,代数作用

31 |
32 |

模块依赖

33 |

有时候某些类需要在被调用方法的时候使用其他类:

34 |
class Human {
 35 |     void please() {
 36 |         new Hand().rush();
 37 |     }
 38 |     <T> void pick(T thing) {
 39 |         new Hand().hold(thing);
 40 |     }
 41 | }
42 |

不过像上面的 hand 在每次调用的时候都创建一个实例对 GC 就很不友好,实际上如果不是一次性的东西完全可以复用:

43 |
class Human {
 44 |     Hand hand = new Hand();
 45 |     void please() {
 46 |         hand.rush();
 47 |     }
 48 |     <T> void pick(T thing) {
 49 |         hand.hold(thing);
 50 |     }
 51 | }
52 |

这样处理 Human 的依赖可以增强扩展性,比如换一个 Hand 实现只需要改一个地方。

53 |

而这个 Hand 在这里就是 Human 的一个依赖,也就是说 Human 依赖 Hand

54 |

依赖注入

55 |
56 |

依赖注入就是我依赖你,你把你注入给我。

57 |

By 千里冰封

58 |
59 |

上面的代码有个问题,如果两个人的手依赖不同的实现该怎么办?如果一个人的手是肉做的一个人是机械手该怎么办?

60 |

这时候就应该让构造 Human 的代码来选择构造什么样的 Hand 然后赋值给对应的属性或者直接传给构造函数:

61 |
class Human {
 62 |     Hand hand;
 63 |     Human(Hand hand) {
 64 |         this.hand = hand;
 65 |     }
 66 | }
67 |

这样使用 Human 的代码在构造 Human 的时候就需要传入它的依赖,也就是完成一次对 Human依赖注入(Dependency Injection),依赖从使用 Human 的代码转移到了 Human 中。

68 |

这样设计就可以在造人的时候选择人有什么样的手,甚至还能让两个人共用一个手。

69 |

自动注入

70 |

每次在使用 Human 的时候都要自己搓一个 Hand 塞进去实在是太麻烦,很多时候只需要默认的 Hand 就行了。这时候就需要工厂模式来自动注入依赖:

71 |
class HumanFactory {
 72 |     Hand hand;
 73 |     HumanFactory withHand(Hand hand) {
 74 |         this.hand = hand;
 75 |         return this;
 76 |     }
 77 |     Human build() {
 78 |         if (hand == null) 
 79 |             hand = new HandDefault();
 80 |         return new Human(hand);
 81 |     }
 82 | }
83 |

这种注入依赖的方法对一堆依赖的模块效果拔群,只需要配置部分依赖就可以正确使用。

84 |

依赖注入框架

85 |

工厂模式有个问题,当产出物非常非常复杂的时候代码量极大,但这实际上都是能自动生成的重复代码,于是人们就在这一层上进一步抽象,做出了依赖注入框架来自动生成装配工厂代码。

86 |

例如在Spring中可以通过这样添加注解来生成自动依赖注入代码:

87 |
@Component("Human")
 88 | class Human {
 89 |     @Resource(name="handDefault")
 90 |     Hand hand;
 91 | }
92 |

而调用 context.getBean("Human") 就可以得到一个 Human 的实例。

93 |

读取器单子

94 |

如果你精通函数式编程就会想到为啥 Hand 要放在对象里面呢,保存状态多不好。于是第一个程序可以改成:

95 |
class Human {
 96 |     void please(Hand hand) {
 97 |         hand.rush();
 98 |     }
 99 |     <T> void pick(Hand hand, T thing) {
100 |         hand.hold(thing);
101 |     }
102 | }
103 |

不过这样每个函数都要传一遍 Hand 就挺麻烦的,这时候就可以使用 Reader Monad 来改写这两个方法:

104 |
class Human {
105 |     Reader<Hand, Hand> please() {
106 |         ReaderM<Hand> m = new ReaderM<>();
107 |         return Reader.narrow(
108 |             m.flatMap(m.ask,
109 |             hand -> {
110 |                 hand.rush();
111 |                 return m.pure(hand);
112 |             }));
113 |     }
114 |     <T> Reader<Hand, Hand> pick(T thing) {
115 |         ReaderM<Hand> m = new ReaderM<>();
116 |         return Reader.narrow(
117 |             m.flatMap(m.ask,
118 |             hand -> {
119 |                 hand.hold(thing);
120 |                 return m.pure(hand);
121 |             }));
122 |     }
123 | }
124 |

这样就可以让一个环境在函数之间隐式传递,来达到依赖注入的目的。

125 |

不过……这似乎看上去更加复杂了……

126 |

我在这里只是提供一种思路,在某些对 Monad 支持良好的语言中这种思路是一种更简便的办法。

127 |

代数作用

128 |

讲到 Reader Monad ,读过代数作用那期的读者就会想到 Reader Monad 和代数作用是同构的。那既然 Reader Monad 能用来注入依赖,代数作用也可以:

129 |
class Human {
130 |     void please(Runnable cont) {
131 |         Eff.Perform("Hand", hand -> {
132 |             ((Hand) hand).rush();
133 |             cont.run();
134 |         });
135 |     }
136 |     <T> void pick(T thing, Runnable cont) {
137 |         Eff.Perform("Hand", hand -> {
138 |             ((Hand) hand).hold(thing);
139 |             cont.run();
140 |         })
141 |     }
142 | }
143 |

这样看上去就比 Reader Monad 的写法清晰很多,虽然回调的写法也挺反人类的……不过在支持代数作用的语言里面这种写法将是强力的依赖注入工具。

144 | 145 | 146 | -------------------------------------------------------------------------------- /html/EvalStrategy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:求值策略

28 |

By 「玩火」

29 |
30 |

前置技能:Java基础,λ演算

31 |
32 |

非严格求值

33 |

细心的读者应该已经注意到, λ 演算那期中讲到的 λ 演算解释器的 reduce 并不会先将参数 reduce ,而是先 apply 参数再在最后 reduce 结果:

34 |
// class App
35 | public Expr reduce() {
36 |     Expr fr = f.reduce();
37 |     if (fr instanceof Fun) {
38 |         Fun fun = (Fun) fr;
39 |         return fun.e.apply(fun.x, x).reduce();
40 |     }
41 |     return new App(fr, x);
42 | }
43 |

并且函数的 reduce 并不会 reduce 函数内部表达式而是直接返回 this

44 |

这样处理和平时见过的常规语言似乎很不一样,C、Java在处理函数参数时都选择先对参数求值再传参。

45 |

像这样先传参再归约的求值思路就叫非严格求值(Non-strict Evaluation),也叫惰性求值(Lazy Evaluation)。其最大好处是在很多时候函数的参数没有被使用过的情况下节省了求值时归约次数。

46 |

比如 (λ x. λ y. x) complex1 complex2 这个 λ 表达式只会取 complex1complex2 中的 complex1 ,如果 complex2 非常复杂那就非常浪费算力了。

47 |

严格求值

48 |

所谓严格求值(Strict Evaluation)就是像 C 系语言一样先求参数的值再传参:

49 |
// class App
50 | public Expr strictReduce() {
51 |     Expr fr = f.strictReduce();
52 |     Expr xr = x.strictReduce();
53 |     if (fr instanceof Fun) {
54 |         Fun fun = (Fun) fr;
55 |         return fun.e.apply(fun.x, xr).strictReduce();
56 |     }
57 |     return new App(fr, xr);
58 | }
59 |

λ 演算中所有的表达式都是的,也就是说不同的求值策略和求值顺序并不会影响最终求值的结果。所以不同的求值策略求出的结果都是等价的,只是打印出来的效果不同。这与 C 系语言就很不一样。

60 |

完全 β 归约

61 |

如果想要让打印出的表达式被化到最简还需要在严格求值的基础上把函数定义中的表达式也进一步归约:

62 |
// class Fun
63 | public Expr fullBetaReduce() {
64 |     return new Fun(x, e.fullBetaReduce());
65 | }
66 |

这就是完全 β 归约。它会将任何能归约的地方归约,即使这个函数并没有被应用函数内部也会被归约。

67 |

对比

68 |

对于λ表达式:

69 |
(λ n. λ f. λ x. (f (n f x)) (λ f. λ x. x))
70 |

进行非严格求值会得到:

71 |
λ f. λ x. f ((λ f. λ x. x) f x)
72 |

进行完全β归约会得到:

73 |
λ f. λ x. f x
74 | 75 | 76 | -------------------------------------------------------------------------------- /html/GADT.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:广义代数数据类型

28 |

By 「玩火」

29 |
30 |

前置技能:Java基础,ADT

31 |
32 |

在 ADT 中可以构造出如下类型:

33 |
// 构造函数已省去
34 | interface Expr {}
35 | class IVal implements Expr {
36 |     Integer value;
37 | }
38 | class BVal implements Expr {
39 |     Boolean value;
40 | }
41 | class Add implements Expr {
42 |     Expr e1, e2;
43 | }
44 | class Eq implements Expr {
45 |     Expr e1, e2;
46 | }
47 |

但是这样构造有个问题,很显然 BVal 是不能相加的而这样的构造并不能防止构造出这样的东西。实际上在这种情况下ADT的表达能力是不足的。

48 |

一个比较显然的解决办法是给 Expr 添加一个类型参数用于标记表达式的类型:

49 |
// 构造函数已省去
50 | interface Expr<T> {}
51 | class IVal implements Expr<Integer> {
52 |     Integer value;
53 | }
54 | class BVal implements Expr<Boolean> {
55 |     Boolean value;
56 | }
57 | class Add implements Expr<Integer> {
58 |     Expr<Integer> e1, e2;
59 | }
60 | class Eq<T> implements Expr<Boolean> {
61 |     Expr<T> e1, e2;
62 | }
63 |

这样就可以避免构造出两个类型为 Boolean 的表达式相加,能构造出的表达式都是类型安全的。

64 |

注意到四个 class 的父类都不是 Expr<T> 而是包含参数的 Expr ,这和 ADT 并不一样。而这就是广义代数数据类型(Generalized Algebraic Data Type, GADT)。

65 | 66 | 67 | -------------------------------------------------------------------------------- /html/HKT.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:高阶类型

28 |

By 「玩火」

29 |
30 |

前置技能:Java基础

31 |
32 |

常常碰到的困难

33 |

写代码的时候常常会碰到语言表达能力不足的问题,比如下面这段用来给 F 容器中的值进行映射的代码:

34 |
interface Functor<F> {
 35 |     <A, B>
 36 |     F<B> map(F<A> a, Function<A, B> f);
 37 | }
38 |

并不能通过 javac 的编译,编译器会告诉你F不能有泛型参数。

39 |

最简单粗暴的解决方案就是放弃类型检查,全上 Object ,如:

40 |
interface Functor<F> {
 41 |     Object map(Object a, 
 42 |                Function<Object, Object> f);
 43 | }
44 |

实际上Java经常这么干,标准库中到处是 Object 的身影,重载的各种接口也常常要手工转换类型, equals 要和 Object 比较, compareTo 要和 Object 比较……似乎习惯了以后也挺好,又不是不能用!

45 |

高阶类型

46 |

假设类型的类型是 Type ,比如 intString 类型都是 Type

47 |

而对于 List 这样带有一个泛型参数的类型来说,它相当于一个把类型 T 映射到 List<T> 的函数,其类型可以表示为 Type -> Type

48 |

同样的对于 Map 来说它有两个泛型参数,类型可以表示为 (Type, Type) -> Type

49 |

像这样把类型映射到类型的非平凡类型就叫高阶类型(HKT, Higher Kinded Type)。

50 |

虽然Java中存在这样的高阶类型但是我们并不能用一个泛型参数表示出来,也就不能写出如上 F<A> 这样的代码了,因为 F 是个高阶类型。

51 |
52 |

如果加一层解决不了问题,那就加两层。

53 |
54 |

虽然在Java中不能直接表示出高阶类型,但是我们可以通过加一个中间层来在保留完整信息的情况下强类型地模拟出高阶类型。

55 |

首先,我们需要一个中间层:

56 |
interface HKT<F, A> {}
57 |

然后我们就可以用 HKT<F, A> 来表示 F<A> ,这样操作完 HKT<F, A> 后我们仍然有完整的类型信息来还原 F<A> 的类型。

58 |

这样,上面 Functor 就可以写成:

59 |
interface Functor<F> {
 60 |     <A, B> 
 61 |     HKT<F, B> map(HKT<F, A> ma, 
 62 |                   Function<A, B> f);
 63 | }
64 |

这样就可以编译通过了。而对于想实现 Functor 的类,需要先实现 HKT 这个中间层,这里拿 List 举例:

65 |
class HKTList<A> 
 66 |     implements HKT<HKTList<?>, A> {
 67 | 
 68 |     List<A> value;
 69 | 
 70 |     HKTList() {
 71 |         value = new ArrayList<>();
 72 |     }
 73 | 
 74 |     HKTList(List<A> v) {
 75 |         value = v;
 76 |     }
 77 | 
 78 |     static <T> HKTList<T>
 79 |     narrow(HKT<HKTList<?>, T> v) {
 80 |         return (HKTList<T>) v;
 81 |     }
 82 | 
 83 |     static <T> Collector<T, ?, HKTList<T>>
 84 |     collector() { /* ... */ }
 85 | }
86 |

注意 HKTList 把自己作为了 HKT 的第一个参数来保存自己的类型信息,这样对于 HKT<HKTList<?>, T> 这个接口来说就只有自己这一个子类,而在 narrow 函数中可以安全地把这个唯一子类转换回来。

87 |

这样,实现 Functor 类就是一件简单的事情了:

88 |
class ListF
 89 |     implements Functor<HKTList<?>> {
 90 | 
 91 |     public <A, B> HKT<HKTList<?>, B>
 92 |     map(HKT<HKTList<?>, A> ma, 
 93 |         Function<A, B> f) {
 94 | 
 95 |         return HKTList.narrow(ma)
 96 |             .value.stream().map(f)
 97 |             .collect(HKTList.collector());
 98 |     }
 99 | }
100 | 101 | 102 | -------------------------------------------------------------------------------- /html/Lifting.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:提升

28 |

By 「玩火」

29 |
30 |

前置技能:Java基础,HKT,Monad

31 |
32 |

概念

33 |

提升(Lifting)指的是把一个通用函数变成容器映射函数的操作。

34 |

比如把 Function<A, B> 变成 Function<M<A>, M<B>> 就是一种提升操作。而由于被操作的函数有一个参数所以这个操作也叫 lift1

35 |

注意被提升的函数可以有不止一个参数,我们也可以把 BiFunction<A, B, C> 提升为 BiFunction<M<A>, M<B>, M<C>> 。这样两个参数的提升可以称为 lift2

36 |

同样,被提升的函数可以没有参数,这时候我们可以看成没有这个函数,也就是把 A 提升为 M<A> 。这样的提升可以称为 lift0 。实际上它也和 Monad 中的 pure 是同构的。

37 |

也就是说:

38 |
<A> M<A> 
39 |     lift0(A f) {}
40 | <A, B> Function<M<A>, M<B>> 
41 |     lift1(Function<A, B> f) {}
42 | <A, B, C> BiFunction<M<A>, M<B>, M<C>>
43 |     lift2(Function<A, B, C>) {}
44 |

fmap

45 |

看到这个函数签名肯定有人会拍案而起:这不就是 fmap 么?

46 |

fmap is a lifting surly. 因为它符合 lifting 的函数签名,但是 lifting 并不一定是 fmap 。只要符合这样的函数签名就可以说是一个 lifting 。

47 |

比如对于 list 来说 f -> x -> x.tail().map(f) 也符合 lifting 的函数签名但很显然它不是一个 fmap 函数。或者说很多改变结构的函数和 fmap 组合还是一个 lifting 函数。

48 |

除此之外呢

49 |

回到上面那个函数签名,里面有个非泛型的参数 M ,这个 M 可以是个泛型参数,可以是个包装器比如 Maybe ,也可以是个线性容器比如 List ,可以是个非线性的容器比如 Set ,甚至可以是抽象容器比如 Function

50 |

同时提升操作也可能对容器结构做出一些改变,尤其是对于多参函数的提升可能会对函数的参数做出一些组合。比如对于 List 来说 lift2 既可以是 zipMap 也可也是以 f 为操作的卷积。

51 |

liftM

52 |

对于 Monad 来说,存在一种通用的提升操作叫 liftM ,比如对于 List 来说 liftM2 就是:

53 |
<A, B, C>
54 | BiFunction<List<A>, List<B>, List<C>>
55 | liftM2List(BiFunction<A, B, C> f) {
56 |     HKTListM m = new HKTListM();
57 |     return (ma, mb) -> HKTList.narrow(
58 |              m.flatMap(new HKTList<>(ma),
59 |         a -> m.flatMap(new HKTList<>(mb),
60 |         b -> m.pure(f.apply(a, b))))
61 |     ).value;
62 | }
63 |

而对 Integer::sum 进行提升以后的函数输入 [1, 2, 3][2, 3, 4] 就会得到 [3, 4, 5, 4, 5, 6, 5, 6, 7] 。实际上就是对于任意两个元素组合操作。

64 |

再比如 liftM5Haskell 中的表述为:

65 |
liftM5 f ma mb mc md me = do
66 |   a <- ma
67 |   b <- mb
68 |   c <- mc
69 |   d <- md
70 |   e <- me
71 |   pure (f a b c d e)
72 |

也就是 liftM[n] 就相当于嵌套 nflatMap 提取 Monad 中的值然后应用给被提升的函数。

73 | 74 | 75 | -------------------------------------------------------------------------------- /html/Monad.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:单子

28 |

By 「玩火」

29 |
30 |

前置技能:Java基础,HKT

31 |
32 |

单子

33 |

单子(Monad)是指一种有一个类型参数的数据结构,拥有 pure (也叫 unit 或者 return )和 flatMap (也叫 bind 或者 >>= )两种操作:

34 |
interface Monad<M> {
 35 |     <A> HKT<M, A> pure(A v);
 36 | 
 37 |     <A, B> HKT<M, B>
 38 |     flatMap(HKT<M, A> ma, 
 39 |             Function<A, HKT<M, B>> f);
 40 | }
41 |

其中 pure 要求返回一个包含参数类型内容的数据结构, flatMap 要求把 ma 中的值经过 f 以后再串起来。

42 |

举个最经典的例子:

43 |

List Monad

44 |
class HKTListM implements Monad<HKTList<?>> {
 45 |     public <A> HKT<HKTList<?>, A> pure(A v) {
 46 |         ArrayList<A> list = new ArrayList<>();
 47 |         list.add(v);
 48 |         return new HKTList<>(list);
 49 |     }
 50 | 
 51 |     public <A, B> HKT<HKTList<?>, B> 
 52 |     flatMap(HKT<HKTList<?>, A> ma, 
 53 |            Function<A, HKT<HKTList<?>, B>> f) {
 54 |         return HKTList.narrow(ma).value
 55 |             .stream().flatMap(v -> 
 56 |                 HKTList.narrow(f.apply(v))
 57 |                     .value.stream())
 58 |             .collect(HKTList.collector());
 59 |     }
 60 | }
61 |

简单来说 pure(v) 将得到 {v} ,而 flatMap({1, 2, 3}, v -> {v + 1, v + 2}) 将得到 {2, 3, 3, 4, 4, 5} 。这都是 Java 里面非常常见的操作了,并没有什么新意。

62 |

Maybe Monad

63 |

Java 不是一个空安全的语言,也就是说任何对象类型的变量都有可能为 null 。对于一串可能出现空值的逻辑来说,判空常常是件麻烦事:

64 |
static Maybe<Integer>
 65 | addI(Maybe<Integer> ma, Maybe<Integer> mb) {
 66 |     if (ma.value == null) return new Maybe<>();
 67 |     if (mb.value == null) return new Maybe<>();
 68 |     return new Maybe<>(ma.value + mb.value);
 69 | }
70 |

其中 Maybe 是个 HKT 的包装类型:

71 |
class Maybe<A> implements HKT<Maybe<?>, A> {
 72 |     A value;
 73 |     Maybe() { value = null; }
 74 |     Maybe(A v) { value = v; }
 75 |     static <T> Maybe<T> 
 76 |     narrow(HKT<Maybe<?>, T> v) {
 77 |         return (Maybe<T>) v;
 78 |     }
 79 | }
80 |

像这样定义 Maybe Monad

81 |
class MaybeM implements Monad<Maybe<?>> {
 82 |     public <A> HKT<Maybe<?>, A> pure(A v) {
 83 |         return new Maybe<>(v);
 84 |     }
 85 |     public <A, B> HKT<Maybe<?>, B>
 86 |     flatMap(HKT<Maybe<?>, A> ma, 
 87 |             Function<A, HKT<Maybe<?>, B>> f) {
 88 |         A v = Maybe.narrow(ma).value;
 89 |         if (v == null) return new Maybe<>();
 90 |         else return f.apply(v);
 91 |     }
 92 | }
93 |

上面 addI 的代码就可以改成:

94 |
static Maybe<Integer>
 95 | addM(Maybe<Integer> ma, Maybe<Integer> mb) {
 96 |     MaybeM m = new MaybeM();
 97 |     return Maybe.narrow(   // do
 98 |         m.flatMap(ma, a -> //   a <- ma
 99 |         m.flatMap(mb, b -> //   b <- mb
100 |         m.pure(a + b)))    //   pure (a + b)
101 |     );
102 | }
103 |

这样看上去就比上面的连续 if-return 优雅很多。在一些有语法糖的语言 (Haskell) 里面 Monad 的逻辑甚至可以像上面右边的注释一样简单明了。

104 |
105 |

我知道会有人说,啊,我有更简单的写法:

106 |
static Maybe<Integer>
107 | addE(Maybe<Integer> ma, 
108 |      Maybe<Integer> mb) {
109 |     try { return new Maybe<>(
110 |             ma.value + mb.value);
111 |     } catch (Exception e) {
112 |         return new Maybe<>();
113 |     }
114 | }
115 |

确实,这样写也挺简洁直观的, Maybe Monad 在有异常的 Java 里面确实不是一个很好的例子,不过 Maybe Monad 确实是在其他没有异常的函数式语言里面最为常见的 Monad 用法之一。而之后我也会介绍一些异常也无能为力的 Monad 用法。

116 |
117 | 118 | 119 | -------------------------------------------------------------------------------- /html/Monoid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:单位半群

28 |

By 「玩火」

29 |
30 |

前置技能:Java基础

31 |
32 |

半群(Semigroup)

33 |

半群是一种代数结构,在集合 A 上包含一个将两个 A 的元素映射到 A 上的运算即 <> : (A, A) -> A​ ,同时该运算满足结合律(a <> b) <> c == a <> (b <> c) ,那么代数结构 {<>, A} 就是一个半群。

34 |

比如在自然数集上的加法或者乘法可以构成一个半群,再比如字符串集上字符串的连接构成一个半群。

35 |

单位半群(Monoid)

36 |

单位半群是一种带单位元的半群,对于集合 A 上的半群 {<>, A}A 中的元素 a 使 A 中的所有元素 x 满足 x <> aa <> x 都等于 x,则 a 就是 {<>, A} 上的单位元。

37 |

举个例子, {+, 自然数集} 的单位元就是 0 , {*, 自然数集} 的单位元就是 1 , {+, 字符串集} 的单位元就是空串 ""

38 |

用 Java 代码可以表示为:

39 |
interface Monoid<T> {
 40 |     T empty();
 41 |     T append(T a, T b);
 42 |     default T appends(Stream<T> x) {
 43 |         return x.reduce(empty(), this::append);
 44 |     }
 45 | }
46 |

应用:Optional

47 |

在 Java8 中有个新的类 Optional 可以用来表示可能有值的类型,而我们可以对它定义个 Monoid :

48 |
class OptionalM<T> implements Monoid<Optional<T>> {
 49 |     public Optional<T> empty() {
 50 |         return Optional<T>.empty();
 51 |     }
 52 |     public Optional<T> 
 53 |     append(Optional<T> a, Optional<T> b) {
 54 |         if (a.isPresent()) return a;
 55 |         else return b;
 56 |     }
 57 | }
58 |

这样对于 appends 来说我们将获得一串 Optional 中第一个不为空的值,对于需要进行一连串尝试操作可以这样写:

59 |
new OptionalM<int>.appends(Stream.of(try1(), try2(), try3(), try4()))
60 |

应用:Ordering

61 |

对于 Comparable 接口可以构造出:

62 |
class OrderingM implements Monoid<int> {
 63 |     public int empty() { return 0; }
 64 |     public int append(int a, int b) {
 65 |         if (a == 0) return b;
 66 |         else return a;
 67 |     }
 68 | }
69 |

同样如果有一串带有优先级的比较操作就可以用 appends 串起来,比如:

70 |
class Student implements Comparable {
 71 |     String name;
 72 |     String sex;
 73 |     Date birthday;
 74 |     String from;
 75 |     public int compareTo(Object o) {
 76 |         Student s = (Student)(o);
 77 |         return new OrderingM.appends(Stream.of(
 78 |             name.compareTo(s.name),
 79 |             sex.compareTo(s.sex),
 80 |             birthday.compareTo(s.birthday),
 81 |             from.compareTo(s.from)
 82 |         ));
 83 |     }
 84 | }
85 |

这样的写法比一连串 if-else 优雅太多。

86 |

扩展

87 |

在 Monoid 接口里面加 default 方法可以支持更多方便的操作:

88 |
interface Monoid<T> {
 89 |     //...
 90 |     default T when(boolean c, T then) {
 91 |         if (c) return then;
 92 |         else return empty();
 93 |     }
 94 |     default T cond(boolean c, T then, T els) {
 95 |         if (c) return then;
 96 |         else return els;
 97 |     }
 98 | }
 99 | 
100 | class Todo implements Monoid<Runnable> {
101 |     public Runnable empty() {
102 |         return () -> {};
103 |     }
104 |     public Runnable 
105 |     append(Runnable a, Runnable b) {
106 |         return () -> { a(); b(); };
107 |     }
108 | }
109 |

然后就可以像下面这样使用上面的定义:

110 |
new Todo.appends(Stream.of(
111 |     logic1,
112 |     () -> { logic2(); },
113 |     Todo.when(condition1, logic3)
114 | ))
115 |
116 |

注:上面的 Optional 并不是 lazy 的,实际运用中加上非空短路能提高效率。

117 |
118 | 119 | 120 | -------------------------------------------------------------------------------- /html/Parsec.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:解析器组合子

28 |

By 「玩火」

29 |
30 |

前置技能:Java基础,HKT,Monad

31 |
32 |

常见组合子

33 |

解析器单子那期的最后给出了 mapcombine 的定义,而 combine 可以进一步组合出只取自己结果和只取参数结果的组合子:

34 |
// class Parser<A>
 35 | 
 36 | // 忽略参数解析器的解析结果
 37 | <B> Parser<A>
 38 | skip(Parser<B> p) {
 39 |     return combine(p, (a, b) -> a);
 40 | }
 41 | // 使用参数解析器的解析结果
 42 | <B> Parser<B>
 43 | use(Parser<B> p) {
 44 |     return combine(p, (a, b) -> b);
 45 | }
46 |

or 组合子可以在解析失败的时候用参数解析器来重新解析:

47 |
// class Parser<A>
 48 | Parser<A>
 49 | or(Parser<A> p) {
 50 |     return new Parser<>(s -> {
 51 |         Maybe<Pair<A, ParseState>>
 52 |             r = runParser(s);
 53 |         if (r.value == null)
 54 |             return p.runParser(s);
 55 |         return r;
 56 |     });
 57 | }
58 |

many 组合子用来构造匹配任意个相同的解析器的解析器,用了 List 来返回结果,所以代码比较复杂:

59 |
// class Parser<A>
 60 | Parser<List<A>>
 61 | many() {
 62 |     return new Parser<>(s -> {
 63 |         Maybe<Pair<A, ParseState>>
 64 |             r = runParser(s);
 65 |         if (r.value == null)
 66 |             return new Maybe<>(
 67 |                 new Pair<>(
 68 |                 new ArrayList<>(), s));
 69 |         Maybe<Pair<List<A>, ParseState>>
 70 |             rs = many().runParser(
 71 |                         r.value.second);
 72 |         rs.value.first.add(0,
 73 |                          r.value.first);
 74 |         return rs;
 75 |     });
 76 | }
77 |

some 组合子利用 many 定义,用来构造匹配一个及以上相同的解析器的解析器:

78 |
// class Parser<A>
 79 | Parser<List<A>>
 80 | some() {
 81 |     return combine(many(), (x, xs) -> {
 82 |         xs.add(0, x);
 83 |         return xs;
 84 |     });
 85 | }
86 |

常见解析器

87 |

最基本的是 id 解析器,解析任意一个字符并作为解析结果返回:

88 |
static Parser<Character> id =
 89 |         new Parser<>(s -> {
 90 | 
 91 |     if (s.p == s.s.length())
 92 |         return new Maybe<>();
 93 | 
 94 |     return new Maybe<>(
 95 |         new Pair<>(s.s.charAt(s.p),
 96 |                     s.next()));
 97 | 
 98 | });
99 |

有了 id 解析器之后构造的解析器就只需要把 id 和组合子组合而不需要再关心解析一个字符的细节。

100 |

最常用的解析器就是 pred ,解析一个符合要求的字符:

101 |
static Parser<Character>
102 | pred(Predicate<Character> f) {
103 |     ParserM m = new ParserM();
104 |     return narrow(
105 |               m.flatMap(id,
106 |          c -> f.test(c) ?
107 |                   m.pure(c) :
108 |                   m.fail()));
109 | }
110 |

另一个常用的解析器是 character ,解析特定字符:

111 |
static Parser<Character>
112 | character(char x) {
113 |     return pred(c -> c == x);
114 | }
115 |

组合

116 |

既然叫解析器组合子,那么它们是用来组合的,这里给出如何用它们组合出一个解析浮点数的解析器例子:

117 |
// 解析一个数字字符
118 | static Parser<Integer> digit =
119 |     pred(c -> '0' <= c && c <= '9')
120 |                     p(c -> c - '0');
121 | // 解析一个自然数
122 | static Parser<Integer> nat =
123 |         digit.some().map(xs -> {
124 |     int x = 0;
125 |     for (int i : xs) x = x * 10 + i;
126 |     return x;
127 | });
128 | // 解析一个整数
129 | static Parser<Integer> integer =
130 |     (character('-').use(nat).map(x -> -x))
131 |                    .or(nat);
132 | // 解析一个浮点数
133 | static Parser<Double> real =
134 |     (integer.combine(character('.')
135 |         .use(digit.some()).map(xs -> {
136 |             double ans = 0, base = 0.1;
137 |                 for (int i : xs) {
138 |                     ans += base * i;
139 |                     base *= 0.1;
140 |                 }
141 |                 return ans;
142 |             }),
143 |             Double::sum)).or(integer
144 |             .map(Integer::doubleValue));
145 | 146 | 147 | -------------------------------------------------------------------------------- /html/ParserM.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:解析器单子

28 |

By 「玩火」

29 |
30 |

前置技能:Java基础,HKT,Monad

31 |
32 |

解析器(Parser)是编译器的一部分,它读取源代码(Source Code),输出一个抽象语法树(Abstract Syntax Tree, AST)。某种程度上来说,解析器是一种可组合的东西,字符解析器组成了整数解析器,整数解析器组成了浮点数解析器。

33 |

这样可组合的解析器有一个抽象的函数表达:

34 |
Function<ParseState, 
 35 |          Maybe<Pair<A, ParseState>>>
36 |

其中 ParseState 是包含当前解析位置的源文本的类型。返回值用 Maybe 包起来因为解析可能失败。 A 就是解析出来的具体数据类型。返回值中包括解析后的状态 ParseState ,这样就可以传递给下一个解析器函数,从而组合多个解析器。

37 |

而且很显然这个函数是一个 Monad ,为了为它实现 Monad 需要用 HKT 包装一下:

38 |
class Parser<A>
 39 |         implements HKT<Parser<?>, A> {
 40 | 
 41 |     static <A> Parser<A>
 42 |     narrow(HKT<Parser<?>, A> v) {
 43 |         return (Parser<A>) v;
 44 |     }
 45 | 
 46 |     Function<ParseState,
 47 |              Maybe<Pair<A, ParseState>>>
 48 |                  parser;
 49 | 
 50 |     Parser(Function<ParseState,
 51 |         Maybe<Pair<A, ParseState>>> f) {
 52 |         parser = f;
 53 |     }
 54 | 
 55 |     Maybe<Pair<A, ParseState>>
 56 |     runParser(ParseState s) {
 57 |         return parser.apply(s);
 58 |     }
 59 | 
 60 |     Maybe<A> parse(String s) {
 61 |         MaybeM m = new MaybeM();
 62 |         return Maybe.narrow(
 63 |                  m.flatMap(runParser(
 64 |                      new ParseState(s)),
 65 |             r -> m.pure(r.first)));
 66 |     }
 67 | }
68 |

然后就可以实现 Parser Monad 了:

69 |
class ParserM 
 70 |     implements Monad<Parser<?>> {
 71 | 
 72 |     public <A> HKT<Parser<?>, A> 
 73 |     pure(A v) {
 74 |         return new Parser<>(s ->
 75 |                new Maybe<>(
 76 |                new Pair<>(v, s)));
 77 |     }
 78 | 
 79 |     public <A> HKT<Parser<?>, A> 
 80 |     fail() {
 81 |         return new Parser<>(s -> 
 82 |                new Maybe<>());
 83 |     }
 84 | 
 85 |     public <A, B> HKT<Parser<?>, B>
 86 |     flatMap(HKT<Parser<?>, A> ma,
 87 |         Function<A, 
 88 |             HKT<Parser<?>, B>> f) {
 89 | 
 90 |         return new Parser<>(s -> {
 91 |             MaybeM m = new MaybeM();
 92 |             // 一点伪代码(not Haskell)
 93 |             // do
 94 |             //   r <- ma.runParser(s)
 95 |             //   f(r.first).runParser(
 96 |             //       r.second)
 97 |             return Maybe.narrow(
 98 |                      m.flatMap(Parser
 99 |                          .narrow(ma)
100 |                          .runParser(s),
101 |                 r -> Parser
102 |                      .narrow(f.apply(
103 |                          r.first))
104 |                      .runParser(
105 |                          r.second))
106 |             );
107 |         });
108 |     }
109 | }
110 | 
111 |

实现了 Monad 以后写 Parser 就可以不用管理错误回溯也不用手动传递解析状态,只需要把解析器看成一个抽象的容器,取出解析结果,组合,再放回容器。

112 |

这里举两个用 Parser Monad 写的组合函数:

113 |
// class Parser<A>
114 | <B> Parser<B>
115 | map(Function<A, B> f) {
116 |     ParserM m = new ParserM();
117 |     // do
118 |     //   x <- this
119 |     //   pure (f(x))
120 |     return narrow(
121 |              m.flatMap(this,
122 |         x -> m.pure(f.apply(x))));
123 | }
124 | 
125 | <B, C> Parser<C>
126 | combine(Parser<B> p,
127 |         BiFunction<A, B, C> f) {
128 |     ParserM m = new ParserM();
129 |     // do
130 |     //   a <- this
131 |     //   b <- p
132 |     //   pure (f(a, b))
133 |     return narrow(
134 |              m.flatMap(this,
135 |         a -> m.flatMap(p,
136 |         b -> m.pure(f.apply(a, b)))));
137 | }
138 | 139 | 140 | -------------------------------------------------------------------------------- /html/PiSigma.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:π 类型和 Σ 类型

28 |

By 「玩火」

29 |
30 |

前置技能:ADT,构造演算

31 |
32 |

π 类型

33 |

构造演算中包含一种 * → □ 的函数,这种函数的类型被称为 π 类型。名字很奇怪,这个类型和 π 毫无关系的啊。实际上这个名字是这么来的:

34 |

首先构造一个 π 类型的函数 F: T → U ,在其定义域 T 中任取一个值 x 那么可以构造类型 F(x) ,那么实际上可以对定义域中每个值都这么操作并且把构造出的类型全部统统放入一个有名元组中

35 |
(x_0: F(x_0), x_1: F(x_1), ... , x_n: F(x_n))
36 |

这样的定义实际上是和原来的函数等价的,而且这个元组的类型实际上就是一个 ADT 中的积类型

37 |
F(x_0) * F(x_1) * ... * F(x_n)
38 |

用数学的连乘符号可以表示为

39 |
π_{x: T} F(x)
40 |

这里有个 π 所以它就被称为 π 类型。

41 |

Σ 类型

42 |

设值 x: T ,和 π 类型函数 F: T → U 那么元组 (x, F(x)) 的类型就是 Σ 类型。实际上有了上面的例子,这里也挺好理解的,对于类型 T 的所有 x 可以构造出

43 |
x_0: F(x_0) | x_1: F(x_1) | ... | x_n: F(x_n)
44 |

这个类型和那个元组的类型是等价的,而它就是 ADT 中的和类型

45 |
F(x_0) + F(x_1) + ... + F(x_n)
46 |

用数学的求和符号可以表示为

47 |
Σ_{x: T} F(x)
48 |

所以它就被称为 Σ 类型。

49 |
50 |

题外话:个人感觉这样命名类型挺离谱的,拿个符号就来命名,本来符号意思就多,放在这里一眼看上去真猜不出来是啥意思,我觉得叫总积/求和类型都看上去好理解些。

51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /html/ScottE.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:斯科特编码

28 |

By 「玩火」

29 |
30 |

前置技能:构造演算, ADT ,μ

31 |
32 |

斯科特编码(Scott Encoding)可以在 λ 演算上编码 ADT 。其核心思想就是利用解构函数来处理和类型不同的分支,比如对于如下类型:

33 |
Either<A, B> = Left<A> + Right<B>
34 |

在构造演算中拥有类型:

35 |
Either = λ A: *. λ B: *. (π C: *. (A → C) → (B → C) → C)
36 |

它接受两个解构函数,分别用来处理 Left 分支和 Right 分支然后返回其中一个分支的处理结果。可以按照这个类型签名构造出以下两个类型构造器:

37 |
Left  = λ A: *. λ B: *. λ val: A. (λ C: *. λ l: A → C. λ r: B → C. l val)
38 | Right = λ A: *. λ B: *. λ val: B. (λ C: *. λ l: A → C. λ r: B → C. r val)
39 |

乍一看挺复杂的,不过两个构造器具有非常相似的结构,区别仅仅是 val 的类型和最内侧调用的函数。实际上构造一个 Left 的值时先填入对应 Either 的类型参数然后再填入储存的值就可以得到一个符合 Either 类型签名的实例,解构时填入不同分支的解构函数就一定会得到 Left 分支解构函数处理的结果。

40 |

再举个 List 的例子:

41 |
List = λ T: *. (μ L: *. (π R: *. R → (T → L → R) → R))
42 | 
43 | Nil  = λ T: *. (λ R: *. λ nil: R. λ cons: T → List T → R. nil)
44 | Cons = λ T: *. λ val: T. λ next: List T. 
45 |     (λ R: *. λ nil: R. λ cons: T → List T → T. cons val next)
46 | 
47 | map = λ A: *. λ B: *. λ f: A → B. μ m: List A → List B.
48 |     λ list: List A. 
49 |     list (List B)
50 |     (Nil B)
51 |     (λ x: A. λ xs: List A. Cons B (f x) (m xs))
52 |

也就是说,积类型 A * B * ... * Z 会被翻译为

53 |
π A: *. π B: *. ... π Z: *. 
54 |     (π Res: *. (A → B → ... → Z → Res) → Res)
55 |

和类型 A + B + ... + Z 会被翻译为

56 |
π A: *. π B: *. ... π Z: *. 
57 |     (π Res: *. (A → Res) → (B → Res) → ... → (Z → Res) → Res)
58 |

并且两者可以互相嵌套从而构成复杂的类型。

59 |

如果给和类型的每个分支取个名字,并且允许在解构调用的时候按照名字索引,随意改变分支顺序,在解糖阶段把解构函数调整成正确的顺序那么就可以得到很多函数式语言里面的模式匹配(Pattern match)。然后就可以像这样表示 List

60 |
List = λ T: *. (μ L: *. Nil | Cons T L)
61 |

像这样使用 List

62 |
map = λ A: *. λ B: *. λ f: A → B. μ m: List A → List B. 
63 |     λ list: List A. 
64 |     match list (List B)
65 |     | Cons → λ x: A. λ xs: List A. Cons B (f x) (m xs)
66 |     | Nil  → Nil B
67 | 68 | 69 | -------------------------------------------------------------------------------- /html/YCombinator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习:Y 组合子

28 |

By 「玩火」

29 |
30 |

前置技能:Java 基础,λ 演算,λ 演算编码

31 |
32 |

递归

33 |

在 Java 里面实现递归非常简单,只需要在函数内调用函数本身就好了,比如下面的求和程序:

34 |
int sum(int n) {
35 |     if (n == 0) return 0;
36 |     else return n + sum(n - 1);
37 | }
38 |

这时候就会注意到看起来递归必须要函数有名字,不然怎么调用时表示自己呢?实际上有个很显然的例子:

39 |
(λ x. x x) (λ x. x x)
40 |

这个表达式无论怎么求值都会得到它自身,实际上这就是个无限递归的例子。而在它的基础上稍加修改就可以得到 Y 组合子(Y Combinator):

41 |
Y = λ f. (λ x. f (x x)) (λ x. f (x x))
42 |

而往 Y 组合子上应用一个函数就会得到:

43 |
Y g = (λ x. g (x x)) (λ x. g (x x))
44 |     = g ((λ x. g (x x)) (λ x. g (x x)))
45 |     = g (Y g)
46 |

这样 g 就拿到了 Y g 也就是它自己的函数作为参数,那么就可以递归了,比如上面的 sum 就可以写成:

47 |
sum' = λ self. λ n.
48 |     isZero n
49 |         n
50 |         (+ n (self (prev n)))
51 | sum = Y sum'
52 |

n 是个丘奇数, isZero 判断数字是不是 0 并得到一个 λ 演算编码的布尔值, + 函数把两个丘奇数相加, prev 函数得到比 n 小一的数。 sum 在递归到 n 为 0 时停止递归。

53 |

求值策略

54 |

很显然如果直接使用严格求值会无限展开 Y 算子而得不到结果,如果使用惰性求值会得不到易于阅读的结果。这时候就要用一种介于两者之间的求值策略:

55 |
// class Fun
56 | public Expr fullReduce() {
57 |     return new Fun(x, e.fullReduce());
58 | }
59 | // class App
60 | public Expr fullReduce() {
61 |     Expr fr = f.reduce();
62 |     if (fr instanceof Fun) {
63 |         Fun fun = (Fun) fr;
64 |         return fun.e.apply(fun.x, x).fullReduce();
65 |     }
66 |     return new App(fr.fullReduce(), x.fullReduce());
67 | }
68 |

它只有在尝试应用函数失败的时候才会进行完全展开,这样每次只展开一点就可以避免陷入无限递归。

69 |

循环

70 |

在编码那期中介绍了如何在 λ 演算中构造分支结构,而循环循环可以用递归来表示,每个循环都可以写成循环变量作为参数的尾递归函数,实际上如下的循环:

71 |
State state;
72 | while (needLoop(state)) {
73 |     doSomething();
74 |     state = update(state);
75 | }
76 |

都可以写成如下的递归函数:

77 |
State While(State state) {
78 |     if (needLoop(state)) 
79 |         return While(update(state));
80 |     else return state;
81 | }
82 |

这样就可以把任意的循环改写成 λ 演算的形式了。

83 | 84 | 85 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 十分钟魔法练习 7 | 23 | 24 | 25 |

26 | ⭐Star me on GitHub⭐

27 |

十分钟魔法练习

28 |

Rust版-光量子 | 29 | C++版-图斯卡蓝瑟 | 30 | C#版-CWKSC

31 |

抽象与组合

32 |

希望能在十分钟内教会你一样魔法

33 |

QQ群:1070975853 | 34 | Telegram Group

35 |
36 |

目录中方括号里的是前置技能。

37 |
38 |

类型系统

39 |

偏易 | 代数数据类型(Algebraic Data Type)[Java 基础] | 40 | Markdown | 41 | HTML

42 |

偏易 | 广义代数数据类型(Generalized Algebriac Data Type)[Java 基础, ADT] | 43 | Markdown | 44 | HTML

45 |

偏易 | 余代数数据类型(Coalgebraic Data Type)[Java 基础, ADT] | 46 | Markdown | 47 | HTML

48 |

偏易 | 单位半群(Monoid)[Java 基础] | 49 | Markdown | 50 | HTML

51 |

较难 | 高阶类型(Higher Kinded Type)[Java 基础] | 52 | Markdown | 53 | HTML

54 |

中等 | 单子(Monad)[Java 基础, HKT] | 55 | Markdown | 56 | HTML

57 |

较难 | 状态单子(State Monad)[Java 基础, HKT , Monad] | 58 | Markdown | 59 | HTML

60 |

中等 | 简单类型 λ 演算(Simply-Typed Lambda Calculus)[Java 基础, ADT ,λ 演算] | 61 | Markdown | 62 | HTML

63 |

中等 | 系统 F(System F)[Java 基础, ADT ,简单类型 λ 演算] | 64 | Markdown | 65 | HTML

66 |

中等 | 系统 F ω(System F ω)[Java 基础, ADT ,系统 F] | 67 | Markdown | 68 | HTML

69 |

较难 | 构造演算(Calculus of Construction)[Java 基础, ADT ,系统 F ω] | 70 | Markdown | 71 | HTML

72 |

偏易 | π 类型和 Σ 类型(Pi type & Sigma type)[ADT ,构造演算] | 73 | Markdown | 74 | HTML

75 |

计算理论

76 |

较难 | λ 演算(Lambda Calculus)[Java 基础, ADT] | 77 | Markdown | 78 | HTML

79 |

偏易 | 求值策略(Evaluation Strategy)[Java 基础, λ 演算] | 80 | Markdown | 81 | HTML

82 |

较难 | 丘奇编码(Church Encoding)[λ 演算] | 83 | Markdown | 84 | HTML

85 |

很难 | 斯科特编码(Scott Encoding)[构造演算, ADT , μ] | 86 | Markdown | 87 | HTML

88 |

中等 | Y 组合子(Y Combinator)[Java 基础,λ 演算,λ 演算编码] | 89 | Markdown | 90 | HTML

91 |

中等 | μ(Mu)[Java 基础,构造演算, Y 组合子] | 92 | Markdown | 93 | HTML

94 |

编程范式

95 |

简单 | 表驱动编程(Table-Driven Programming)[简单 Java 基础] | 96 | Markdown | 97 | HTML

98 |

简单 | 续延(Continuation)[简单 Java 基础] | 99 | Markdown | 100 | HTML

101 |

中等 | 代数作用(Algebraic Effect)[简单 Java 基础,续延] | 102 | Markdown | 103 | HTML

104 |

中等 | 依赖注入(Dependency Injection)[Java 基础, Monad ,代数作用] | 105 | Markdown | 106 | HTML

107 |

中等 | 提升(Lifting)[Java 基础, HKT , Monad] | 108 | Markdown | 109 | HTML

110 |

编译原理

111 |

较难 | 解析器单子(Parser Monad)[Java 基础, HKT , Monad] | 112 | Markdown | 113 | HTML

114 |

中等 | 解析器组合子(Parser Combinator)[Java 基础, HKT , Monad] | 115 | Markdown | 116 | HTML

117 | 118 | 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@types/marked": "^1.2.1", 4 | "highlight.js": "^10.4.1", 5 | "marked": "^1.2.5" 6 | }, 7 | "devDependencies": {} 8 | } 9 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 十分钟魔法练习 2 | 3 | Language: [zh-Hant 繁體中文](readme_zh-Hant.md) 4 | 5 | 改写自 [十分钟魔法练习-玩火](https://github.com/goldimax/magic-in-ten-mins) ,C# 版本 6 | 7 | 其他版本:[Rust版 - 光量子](https://github.com/PhotonQuantum/magic-in-ten-mins-rs) | [C++版 - 图斯卡蓝瑟](https://github.com/tusikalanse/magic-in-ten-mins-cpp) 8 | 9 | 抽象与组合 10 | 11 | 希望能在十分钟内教会你一样魔法 12 | 13 | QQ群:1070975853 | [Telegram Group](https://t.me/joinchat/Gla40h2ZvlSrqImOMaMUEA) 14 | 15 | > 目录中方括号里的是前置技能。 16 | 17 | ## 类型系统 18 | 19 | [偏易 | 代数数据类型(Algebraic Data Type)[C# (interface, class)]](doc/ADT.md) 20 | 21 | [偏易 | 广义代数数据类型(Generalized Algebriac Data Type)[C# (interface, class),ADT]](doc/GADT.md) 22 | 23 | [偏易 | 余代数数据类型(Coalgebraic Data Type)[C# (Func, delegate),ADT]](doc/CoData.md) 24 | 25 | [偏易 | 单位半群(Monoid)[C# (IEnumerable, Aggregate(), Extension Methods, IComparable)]](doc/Monoid.md) 26 | 27 | [较难 | 高阶类型(Higher Kinded Type)[C# (interface, List)]](doc/HKT.md) 28 | 29 | [中等 | 单子(Monad)[C# 基础,HKT]](doc/Monad.md) 30 | 31 | [较难 | 状态单子(State Monad)[C# (Func, Tuple),HKT,Monad)]](doc/StateMonad.md) 32 | 33 | > 中等 | 简单类型 λ 演算(Simply-Typed Lambda Calculus)[Java 基础,ADT,λ 演算] | 34 | > [Markdown](doc/STLC.md) | 35 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/STLC.html) 36 | 37 | > 中等 | 系统 F(System F)[Java 基础,ADT,简单类型 λ 演算] | 38 | > [Markdown](doc/SystemF.md) | 39 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/SystemF.html) 40 | 41 | > 中等 | 系统 F ω(System F ω)[Java 基础,ADT,系统 F] | 42 | > [Markdown](doc/SysFO.md) | 43 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/SysFO.html) 44 | 45 | ## 计算理论 46 | 47 | [较难 | λ 演算(Lambda Calculus)[C# (Guid),ADT]](doc/Lambda.md) 48 | 49 | > 偏易 | 求值策略(Evaluation Strategy)[Java基础,λ演算] | 50 | > [Markdown](doc/EvalStrategy.md) | 51 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/EvalStrategy.html) 52 | 53 | > 较难 | 编码(Encode)[λ演算] | 54 | > [Markdown](doc/Encode.md) | 55 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/Encode.html) 56 | 57 | > 中等 | Y 组合子(Y Combinator)[Java 基础,λ 演算,λ 演算编码] | 58 | > [Markdown](doc/YCombinator.md) | 59 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/YCombinator.html) 60 | 61 | ## 编程范式 62 | 63 | >简单 | 表驱动编程(Table-Driven Programming)[简单Java基础] | 64 | [Markdown](doc/TableDriven.md) | 65 | [HTML](https://goldimax.github.io/magic-in-ten-mins/html/TableDriven.html) 66 | 67 | > 简单 | 续延(Continuation)[简单Java基础] | 68 | > [Markdown](doc/Continuation.md) | 69 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/Continuation.html) 70 | 71 | > 中等 | 代数作用(Algebraic Effect)[简单Java基础,续延] | 72 | > [Markdown](doc/Algeff.md) | 73 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/Algeff.html) 74 | 75 | > 中等 | 依赖注入(Dependency Injection)[Java基础,Monad,代数作用] | 76 | > [Markdown](doc/DepsInj.md) | 77 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/DepsInj.html) 78 | 79 | > 中等 | 提升(Lifting)[Java基础,HKT,Monad] | 80 | > [Markdown](doc/Lifting.md) | 81 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/Lifting.html) 82 | 83 | ## 编译原理 84 | 85 | > 较难 | 解析器单子(Parser Monad)[Java基础,HKT,Monad] | 86 | > [Markdown](doc/ParserM.md) | 87 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/ParserM.html) 88 | 89 | > 中等 | 解析器组合子(Parser Combinator)[Java基础,HKT,Monad] | 90 | > [Markdown](doc/Parsec.md) | 91 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/Parsec.html) -------------------------------------------------------------------------------- /readme_zh-Hant.md: -------------------------------------------------------------------------------- 1 | # 十分鐘魔法練習 2 | 3 | Language: [zh-Hant](readme_zh-Hant.md) 4 | 5 | 改寫自 [十分鐘魔法練習-玩火](https://github.com/goldimax/magic-in-ten-mins) ,C# 版本 6 | 7 | 其他版本:[Rust版 - 光量子](https://github.com/PhotonQuantum/magic-in-ten-mins-rs) | [C++版 - 图斯卡蓝瑟](https://github.com/tusikalanse/magic-in-ten-mins-cpp) 8 | 9 | 抽象與組合 10 | 11 | 希望能在十分鐘內教會你一樣魔法 12 | 13 | QQ群:1070975853 | [Telegram Group](https://t.me/joinchat/Gla40h2ZvlSrqImOMaMUEA) 14 | 15 | > 目錄中方括號裡的是前置技能。 16 | 17 | ## 類型系統 18 | 19 | [偏易 | 代數數據類型(Algebraic Data Type)[C# 基礎]](doc/ADT_zh-Hant.md) 20 | 21 | [偏易 | 廣義代數數據類型(Generalized Algebriac Data Type)[C# 基礎,ADT]](doc/GADT_zh-Hant.md) 22 | 23 | [偏易 | 餘代數數據類型(Coalgebraic Data Type)[C# 基礎,ADT]](doc/CoData_zh-Hant.md) 24 | 25 | [偏易 | 單位半群(Monoid)[C# (IEnumerable, Aggregate(), Extension Methods, IComparable)]](doc/Monoid_zh-Hant.md) 26 | 27 | > 較難 | 高階類型(Higher Kinded Type)[Java基礎] | 28 | > [Markdown](doc/HKT.md) | 29 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/HKT.html) 30 | 31 | > 中等 | 單子(Monad)[Java基礎,HKT] | 32 | > [Markdown](doc/Monad.md) | 33 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/Monad.html) 34 | 35 | > 較難 | 狀態單子(State Monad)[Java基礎,HKT,Monad] | 36 | > [Markdown](doc/StateMonad.md) | 37 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/StateMonad.html) 38 | 39 | > 中等 | 簡單類型 λ 演算(Simply-Typed Lambda Calculus)[Java 基礎,ADT,λ 演算] | 40 | > [Markdown](doc/STLC.md) | 41 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/STLC.html) 42 | 43 | > 中等 | 系統 F(System F)[Java 基礎,ADT,簡單類型 λ 演算] | 44 | > [Markdown](doc/SystemF.md) | 45 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/SystemF.html) 46 | 47 | > 中等 | 系統 F ω(System F ω)[Java 基礎,ADT,系統 F] | 48 | > [Markdown](doc/SysFO.md) | 49 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/SysFO.html) 50 | 51 | ## 計算理論 52 | 53 | > 較難 | λ 演算(Lambda Calculus)[Java基礎,ADT] | 54 | > [Markdown](doc/Lambda.md) | 55 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/Lambda.html) 56 | 57 | > 偏易 | 求值策略(Evaluation Strategy)[Java基礎,λ演算] | 58 | > [Markdown](doc/EvalStrategy.md) | 59 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/EvalStrategy.html) 60 | 61 | > 較難 | 編碼(Encode)[λ演算] | 62 | > [Markdown](doc/Encode.md) | 63 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/Encode.html) 64 | 65 | > 中等 | Y 組合子(Y Combinator)[Java 基礎,λ 演算,λ 演算編碼] | 66 | > [Markdown](doc/YCombinator.md) | 67 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/YCombinator.html) 68 | 69 | ## 編程範式 70 | 71 | >簡單 | 表驅動編程(Table-Driven Programming)[簡單Java基礎] | 72 | >[Markdown](doc/TableDriven.md) | 73 | >[HTML](https://goldimax.github.io/magic-in-ten-mins/html/TableDriven.html) 74 | 75 | > 簡單 | 續延(Continuation)[簡單Java基礎] | 76 | > [Markdown](doc/Continuation.md) | 77 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/Continuation.html) 78 | 79 | > 中等 | 代數作用(Algebraic Effect)[簡單Java基礎,續延] | 80 | > [Markdown](doc/Algeff.md) | 81 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/Algeff.html) 82 | 83 | > 中等 | 依賴注入(Dependency Injection)[Java基礎,Monad,代數作用] | 84 | > [Markdown](doc/DepsInj.md) | 85 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/DepsInj.html) 86 | 87 | > 中等 | 提升(Lifting)[Java基礎,HKT,Monad] | 88 | > [Markdown](doc/Lifting.md) | 89 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/Lifting.html) 90 | 91 | ## 編譯原理 92 | 93 | > 較難 | 解析器單子(Parser Monad)[Java基礎,HKT,Monad] | 94 | > [Markdown](doc/ParserM.md) | 95 | > [HTML](https://goldimax.github.io/magic-in-ten-mins/html/ParserM.html) 96 | 97 | > 中等 | 解析器組合子(Parser Combinator)[Java基礎,HKT,Monad] | 98 | > [Markdown](doc/Parsec.md) | 99 | > [HTML]( --------------------------------------------------------------------------------