head

Relens to Lens

之前也写过一篇lens的文章,学了新知识之后,现在看起来觉得不满意,于是打算重新再写一篇。

Lens的设计十分的精妙,是数学指导编程的一个非常典型的案例。

Lens要解决的问题

在Haskell中,只提供了模式匹配的语法来操作数据,操作深层的是一个非常麻烦的事情。比如我们要操作以下数据:

我们要修改l1的第二个端点的横坐标:

如果数据结构更加复杂的时候,代码就会变得更冗长。但如果用lens,刚刚的表达式就可以重写为:

其中(endLens . xLens)就是一个透镜组,先访问end,再访问x.~就是set。那么之前需要用语法特殊解决的问题,就可以靠lens的组合来解决(解决一个带field语法的语言没有的问题)。

 

 

抽象的getter和setter

对数据的操作,无非是get和set。get是从父组件拿到某个子组件;set在函数式语言的语境下是将修改了某个子组件父组件复制一份,也就是所谓的写时复制(Copy on Write):

 

将其组合起来就得到了Lens的第一种形式:

 

假如现在有一个ba的lens,我们就可以对ba进行get或set的操作了:

 

如果不限制set之后得到类型前后一致,我们还可以得到第一种形式的Lens的完全形式:

其余代码的实现方式不改变。

 

remark: Lens是Costate Comonad的Coalgebra。

从另外一个角度归纳出lens

其实平常我们就已经在用一些get和set的组合模式了:

统一一下形式,对get和fold作一个变换:

总结一下:

 

观察到,这时将Getter中的r看作常函子Const r作用的结果;将Setter中的at看作是单位函子Identity作用的结果:

这时就可以归纳出一个统一性非常强的形式,这将作为Lens的第二种形式,称为van Laarhoven form

 

在习惯上,我们称modifyoverget称为view并将set的参数的位置调整一下,并提供其中缀形式:

 

应用起来和lens的第一种形式,没有区别:

稍微可以复杂点:

 

两者形式的等价性

那么Lens的这两种形式是否等价呢?我们比较两者形式:

除去相同的部分s->,证明两者等价相当于证明(a, b -> t)forall f. Functor f => (a -> f b) -> f t之间的等价:

 

我们还能证明([a], [b] -> t)forall f. Applicative f => (a -> f b) -> f t之间的等价:

 

 

其实这两个证明是可以从米田引理中导出来的。

米田引理:对于局部小范畴中的对象,以及函子,函子的自然变换与之间一一对应:

要证明Lens两种形式的等价,我们需要先证明一个引理:

引理1:记函子,有

proof:

 

那么两者Lens之间的等价相当于证明:

定理1

proof:

 

。同样的我们还能证明

 

其实引理1我们还可以导出第三种形式的Lens,当我们取时,我们就可以得到

也就是a -> (s, t -> b)等价于forall x. (a, b->x) -> (s, t->x).

 

remark: Lens就是Costate Comonad之间的自然变换!

 

 

米田嵌入的角度来看——米田嵌入就是一个函子,将范畴“嵌入”到一个函子范畴中,用函子和自然变换表示中的对象和态射,米田引理就表示为——

 

embedding

两者的区别

那么说完了两者的等价性,那么这两种形式之间有什么又有什么区别呢。

感性地来说,函子范畴地性质会比原来的范畴有更好地性质,能完成原来不可行地构造(比如说求一些极限之类的)。而在使用上,大概有两点区别:

对自由变量f的范围的不同的限制,这就有了现在lens库中不同模块之间的UML图:

uml

 

remark: traverse就不能用第一种形式的Lens来表达。

进一步的抽象

在进一步抽象之前,还得引入Profunctor的概念,它在第一个参数上逆变,第二个参数上协变

(->)就是最典型的Profuncto。在第二种形式的Lens中,(- -> f -)也是一个Profunctor。在第三种形式中也是Profunctor。

 

那我们是否有:

我们尝试证明:

_中的的类型是p a b -> p (a, s) (b, s),Profunctor还缺了这么一个函数使得和第一种形式的Lens等价。补充之后,我们就得到了Strong Profunctor:

...

 

于是我们就得到了第四种形式的Lens:

不过我们通常还是使用第二种形式的Lens。可能是因为懒得去实现Strong相关的typeclass了吧

 

Store的题外话

我们通常笑话Haskell为了解决不存在的问题(访问record深层数据)而发明了Lens。那是不是Lens对于本身带有field语法的语言就没有什么卵用呢?以前我是这么觉得的,甚至觉得js上的Lens库是多此一举。

但最近我在Rust中找到这个问题的一部分答案,当然并不是把Lens搬到Rust上,而是发现Lens的一部分——Store在Rust中存在着它的价值。(在Haskell中我一直想不到这玩意儿究竟有什么用)

启发我的是猫老师的这篇文章

在Rust里访问所有者的数据 - 知乎 (zhihu.com)

我这里只是简单复述一下里面的观点。

 

在日常编程中,很可能会遇到一种情况:子组件引用父组件的其它部分进行操作。那么在c/cpp或者其它语言中,这就会造成自引用。在rust中,这样的代码是无法通过borrow check的,这时候有几种可能的操作:

那么还有一种方法就是用Store表示对子组件的引用。ts的子组件,用Store s t = (s, s -> t)来表示对t的引用,持有Store s t作为t的引用的同时,仍然可以访问到父组件s的引用。

用Rust表达就是: