Programming Language Roam: Isomorphism Lens

isomorphism lens 是 Twan van Laarhoven 继 「Van Laarhoven Lens」后发明的一种新的 lens 实现。 这种实现更加简洁、更符合直觉,所以易于理解,在部分情况下比前者更加高效,但大多数场景下可能没有前者使用方便。 定义 isomorphism lens 的实现思想十分富有创意,他将 lens 抽象定义为: A lens from type a to b is a bijection between a and a pair of b and some residual r. 翻译过来就是: 一个 a 到 b 的 lens 是 a 到 (b, r) 的双射,其中 r 为 a 中除去 b 后的剩余部分 简单解释下: 设类型 a 为一个集合,集合内各个元素为其字段 b 为 a 中的一个字段...

May 4, 2022 · firest

Programming Language Roam: Van Laarhoven lens

lens 是一种函数式引用,可以实现对数据的任意部分进行访问和修改,宽松点说,lens 可以看作是一种函数式「指针」。 现在最常用的 lens 实现是由 Twan van Laarhoven 发明的 CPS based functional references,原理虽然很简单, 但却相当惊艳。 这篇算是我自己的复习吧,将需要的概念都简单温习下。 getter and setter 任何一个可以读写的数据都至少有两个操作:读和写,对数据的读和写进行封装,以函数的方式对外提供的操作一般称作「getter」和「setter」。 之所以需要对读/写进行封装,是因为很多场景下,数据的写入往往伴随着一定的副作用,而对读进行封装,则可以实现延迟计算、单例模型 等功能。大部分现代编程语言,比如 C#,都在语法层面提供了简洁的 getter/setter 支持 immutable object 和 函数式语言 在部分编程语言中存在不可变对象,比如在区分「值类型」和「引用类型」的编程语言中,值类型一般就是不可变的。对不可变对象 进行更新操作,一般都是先复制出一个新的副本,然后将更新的值作用在这个副本之上,从而得到更新后的对象。而在大多数函数式语言中, 任何对象几乎都是不可变的。 不可变特性导致了一个很难受的问题:对复杂数据,尤其是层级很深的数据进行更新将是灾难性的,每次更新实际上都需要从当前层级 一直向上更新到根层级,这导致了更新的代码十分冗长。 所以有没有一种简单的方式,可以像 Cee 的指针,或者 C++ 的引用那样,对任意层级的任意数据进行访问和更新? 基于这种需求,lens 技术诞生了 最原始而朴素的 lens 最早期的 lens 技术十分朴素:将 getter 和 setter 放在一个元组中 type getter<'s, 'a> = 's -> 'a type setter<'s, 'a> = 's -> 'a -> 's type lens = getter * setter 当需要对一个字段进行操作时,只需要将从根目录到该字段沿途所有的这种 lens 元组,进行函数复合即可。...

April 20, 2022 · firest

Programming Language Roam: Load-Time Calculation

「加载期计算」,是指: 在代码加载时执行运算或者这种能力 和 编译期计算 相比,加载期计算可能适用范围更广 场景 加载期计算主要适用于以下的场景: 程序运行时需要的某些信息,如环境变量等,无法在编译期间确定 这些信息在程序启动后几乎不会发生变化 程序运行时需要根据不同的环境信息,执行不同的运算 比较典型的场景就是跨平台应用,以及使用静态配置的应用,这些应用在真正运行前是无法获得目标平台、或者目标配置的 值。而运行后,要么对这些信息进行缓存以便反复使用,要么就是每次使用这些信息时,都重新进行一次读取/查询。 而如果程序支持「加载期计算」,则可以在代码加载时,读取相关信息,然后直接将信息写入到调用的代码处,甚至,可以根据 信息的值来选择性生成不同的处理代码。 这样,对于程序而言,这些信息就变成了静态的常量了,且无用的代码分支也会被裁剪掉,这样就减少了程序在缓存、调用、 分支预测上的开销了 Load-Time Hook/Callback 部分语言,比如 Erlang、C#(Unity) 等支持在模块、类、程序集加载时,执行某个函数,这种行为可以看作是代码加载时的一个 回调,或者说是一种受限的「加载期计算」,因为这样的计算,执行的都是编译期已经生成好的代码,无法通过在加载期间执行的运算来影响到原有的代码逻辑。 C# (.Net) 可以在加载时,找到对应属性的程序集、类、方法、乃至字段等,然后动态修改、生成代码。 这种操作,可以算是在代码加载期间执行的 JIT 操作,而「加载期计算」更多的是强调「计算」或者说 「求值」, 但总的来说,区别不大,可以看作是一回事。 示例,零开销配置 这里做一个简单的演示,使用 message 文本文件做为配置文件,里面的内容为: Hello, World! 定义函数 get-message ,直接读取整个文本文件,当作是配置读取操作: (defun get-message () (uiop:read-file-string "~/message")) 定义函数 test , 直接使用 get-message 模拟读取配置操作 (defun test () (let ((msg (get-message))) msg)) 为了防止编译器优化,这里写的很啰嗦,第三行的 msg 实际上可以看作是具体的配置操作过程,只是这里简单的用返回这个行为 来模拟 定义一个函数 test2 , 使用加载期计算,在代码加载时,求出 get-message 的值,然后直接写入到 test2 的代码内...

March 20, 2022 · firest

Programming Language Roam: 编译期计算

最近在工作中的一些场景,让我想到了「编译期计算」这个概念,然而一直没有时间整理、复习下相关概念。 什么是编译期计算 「编译期计算」是指: 可以在编译期间执行的的运算或者这种能力 这并不是什么新的概念,最古老的语言之一的 C 的 宏 就具有很弱的编译期计算能力,而之后的编译语言中,凡是支持 元编程的,多多少少都能支持编译期间预算。 而另外一方面,编译期计算也是一种常见的编译优化手段,高优化的编译器会尽可能的,对在编译期间可以求值的计算过程进行 求值,然后内联其计算结果,比如:如果一个循环内所有变量都是常量,在 -O2 和 -O3 的情况下,GCC 生成的代码多半只有 循环结果,而没有循环的过程。 但是,相对而言,大部分编程语言所支持的 编译期计算 和 Lisp 比起来,都很弱 做为最古老的编程语言之一,Lisp 发明了很多 超时代 的特性,最出名的莫过于 垃圾回收(GC) 了,但是 Lisp 对 计算的控制粒度和灵活度,目前也鲜有对手。 Common Lisp 和 编译期计算 C++11 引入了一个关键字 constexpr,用来声明一个函数或者变量,在编译期是可能进行求值的,相比 宏 和 模板 , 这是一个更加强大的编译期计算的能力。 不过,这种能力却是 30 多年前的 Common Lisp 里面的内建功能,这里用一个求和函数进行演示 (defpackage ct (:use :cl) (:export :test1 :test2)) (in-package ct) ;;; 声明一个求和函数 sum ;;; 设定函数 sum 具有在编译期和执行时进行运算的能力 ;;; 默认情况下函数只能在执行时(execute)才能进行计算 (eval-when (:compile-toplevel :execute) (defun sum (&rest args) (apply #'+ args))) ;;; 声明函数 test1,使用求和函数求值 (defun test1 () (let ((x (sum 1 2 3 ))) x)) ;;; 声明函数 test2,使用语言自带的加法运算求值 (defun test2 () (let ((x (+ 1 2 3))) x)) 然后在 REPL 中执行编译和加载...

March 18, 2022 · firest

通过C#理解Monad

关于什么是 Monad 讲解 Monad 是什么的文章很多,但是基本上都是以范畴论的概念来展开说明的,这导致了理解成本特别高 从一个小坑掉入了一个大坑 这里不谈理论,将通过下面的 C#(伪)代码来说明什么是 Monad,所以其实并不严谨 public delegate Gold F(int x); public class Gold { private int gold; public Gold(int x) => gold = x; public static Gold operator + (F f) => f(gold); public static Gold Double(int x) => new Gold(x * 2); public static Gold Zero(int x) => new Gold(0); } 计算表达式 微软在设计 F#时,为了避免使用 Monad 这个单词,发明了「计算表达式」这个概念,这个概念其实很好的反映了 Monad 的本质 先来看个小学级别的计算式子: α = 1 + 2 + 3 + 4 + 5 然后我们把里面的数字全部换成 Gold 类型,得到: β = Gold + Gold + Gold + Gold + Gold 但是上面我们并没有实现 Gold + Gold, 而是实现了 Gold + F, 不过 Gold + F -> Gold, 所以我们可以将 β 里面的 Gold 替换为 Gold + F, 从而得到: γ = (((Gold + F) + F) + F) + F 对 γ 的扩展 首先我们能看到,在上面的代码中,F可以是「Double」也可以是「Zero」,也就是这个表达式并不关心执行的具体流程,只要该流程满足 F 的声明即可 其次我们将表达式中的 Gold 换成 Diamond、HP、MP,并不影响 γ 这个表达式的运算,也就是说这个表达式并不关心作用对象的类型 最后,我们可以发现 γ 这个表达式并不关心 Gold 里面是 int gold 还是 long gold,或者 string gold, 也就是说这个表达式并不关心对象的内部环境 总结: 我们将 1 中的执行流程用 F 表示 2 中提到的对象用 M 表示 3 中提到的对象内部环境用 a 表示 那么得到新的表达式: δ = ((((M a) + F) + F) + F) 其中: F = a -> M a 回到 Monad 上面的公式 δ 其实和 Haskell 中的 Monad 很类似了,Monad 可以看作是对公式 δ 这一类运算的抽象 从 cshap 的角度理解, Monad 可以看作是一个接口或者抽象类, (伪)代码如下: public interface Monad<T<U>> { Monad<T<U>> Return(U a); static Monad<T<U>> Bind (Monad<T<U>> m, Func<U, Monad<T<U>>> f) } 其中: U 是被包裹的类,比如 Gold 里面的 gold 的类型 int 其次 T 是外层的包裹类,比如 Gold 然后「Return」用于将一个被包裹的类提升为一个 Monad 的类 然后 「Bind」则是 δ 中的 「+」一个二元运算 这些基本上也是 Haskell 中实现一个 Monad 需要完成的"接口"实现(当然上面的代码在 C#里面是没法运行的,C#只能有限的模拟 Haskell 中的 moand) 也就是说只要有类和该类的行为,满足或者说实现这个接口,这个类就可以被看作是一个 Monad 所以,抛开理论上的定义,对 Monad 的使用可以看作是通过二元运算,串联起来的一连串的运算,而不同的二元运算可以实现不同的逻辑,从而达到对运行流程的控制、对副作用的管理等功能

July 12, 2021 · firest