Haskell 光学库——Optics
Posted 梁建溢的随想录
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Haskell 光学库——Optics相关的知识,希望对你有一定的参考价值。
我使用 Haskell 编写了一个用于定义和使用lense,traversal,prisms和其他光学类型的Haskell库,我把该库称为 optics。optics 在功能上与传统的镜头库大致相似,但使用抽象接口而不是暴露每种 optic 类型的底层实现。它的设计目的是比传统渲染引擎的照相机(lens)更容易理解,具有清晰的接口,简单的类型和友好的错误消息。
optics 的类型及其错误消息示例
我们可以在GHCi中直接使用 optics。先看看我对 lens 的定义:
*Optics> :info Lens
type Lens s t a b = Optic A_Lens NoIx s t a b
类型 Optic 统一了不同的光学类型。它的第一个类型参数是A_Lens,表示正在使用的光学类型。第二个参数NoIx,意味着这是一个没有索引的 Optic(为了这篇文章的目的,我将主要忽略具有索引的 Optic)。与 lens 一样,s和t参数表示外部结构的类型(前者为类型发生变更之前,后者为类型发生变更之后),a和b参数表示内部字段的类型。
照相机由 lens 的函数构造出来,它具有 getter 和 setter 访问器并返回一个 Lens(即代码中的 A_Lens:
*Optics> :type lens
lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
*Optics> let l = lens (\(x,_) -> x) (\(_,y) x -> (x,y))
l :: Lens (a1, b) (a2, b) a1 a2
给定一个镜头我们可以用 view 来查看外部结构中的内联结构体,或者设置一个新值:
*Optics> :type view
view :: Is k A_Getter => Optic' k is s a -> s -> a
*Optics> :type set
set :: Is k A_Setter => Optic k is s t a b -> b -> s -> t
请注意,这些类型在它们接受的光学类型中是多态的,但是非常清楚地指出它们需要什么样的光学元件(它们在 is 中也是多态的,因此它们可以与带有索引的 Optic 和不带索引的 Optic 一起使用。)。您可以将 view 应用于可以转换为Getter的任何光学类型 k。Is约束使用 typeclass 类型系统实现子类型,尤其是 A_Lens A_Getter 和 A_Lens A_Setter 的实例,使得我们的镜头 l 可以与两个算子一起使用:
*Optics> view l ('a','b')
'a'
*Optics> set l 'c' ('a','b')
('c','b')
如果您尝试使用不是所需类型的子类型的 Optic,则会给出明确的错误消息:
*Optics> :type sets
sets :: ((a -> b) -> s -> t) -> Setter s t a b
*Optics> :type view (sets fmap)
输出:
<interactive>:1:1: error:
• A_Setter cannot be used as A_Getter
• In the expression: view (sets fmap)
对 Optic 类型的组合
Optic 不是函数,因此它们不能与 (.) 算子组合。我觉得这是为了改进类型推导和设计更清晰的类型错误所付出的代价,但它在概念上很重要:我们将 Optic 视为一种抽象概念,与使用函数表示不同,因此用函数组合它们是没有意义的。下面使用 (%) 代替 (.):
*Optics> :type l % l
l % l :: Optic A_Lens '[] ((a, b1), b2) ((b3, b1), b2) a b3
*Optics> view (l % l) (('x','y'),'z')
'x'
只要它们具有共同的超类型,组合不同类型的 Optic 可以工作得很好,该组合子返回如下:
*Optics> :type l % sets fmap
l % sets fmap
:: Functor f => Optic A_Setter '[] (f a, b1) (f b2, b1) a b2
但是,某些 Optic 类型没有公共超类型,在这种情况下,尝试组合它们会导致类型错误:
*Optics> :type to
to :: (s -> a) -> Getter s a
*Optics> :type to fst % sets fmap
输出:
<interactive>:1:1: error:
• A_Getter cannot be composed with A_Setter
• In the expression: to fst % sets fmap
(%)本身的类型并不完全是微不足道的。它依赖于类型族 Join 来计算一对 Optic 类型的最小上界:
*Optics> :type (%)
(%)
:: (Is k (Join k l), Is l (Join k l)) =>
Optic k is s t u v
-> Optic l js u v a b -> Optic (Join k l) (Append is js) s t a b
但很少直接使用(%),只能看到结果。可以直接求值 Join 类型族以确定两种 Optic 类型的组成:
*Optics> :kind! Join A_Lens A_Setter
Join A_Lens A_Setter :: *
= A_Setter
*Optics> :kind! Join A_Getter A_Setter
Join A_Getter A_Setter :: *
= (TypeError ...)
Optics 的层次结构
所有的光学类型的层次结构是封闭的,即在不修改库的情况下不可能发现和利用新的光学种类。我们的目标是使其更容易理解不同光学类型的接口和用途,但这是以掩盖van Laarhoven或profunctor表示的一些基本常见结构为代价的。相对于[镜头]的一个具体限制是我们尚未探索对非空折叠和遍历的支持(Fold1和Traversal1)。
下图显示了初始版本支持的光学类型的层次结构。每个箭头从子类型指向其直接超类型,例如, 每个 lens都可以用作Getter:
下图显示子类型层次结构中Lens上方的每个光学元件都有一个附带的索引变量:
以上是关于Haskell 光学库——Optics的主要内容,如果未能解决你的问题,请参考以下文章