镜片(Lenses)表征了“具有”(has-a)关系;棱镜(Prisms)则表征了“是一个”(is-a)关系。
一个 Lens s a 表示:“s 具有一个 a”,它具有从 s 获取一个 a 并覆盖 s 中一个 a 的方法。而 Prism s a 则表示:“a 是一个 s”,它具有将 a 向上类型转换为 s 和(尝试)将 s 向下类型转换为 a 的方法。
将这种直觉转化为代码,可以得到熟悉的“获取-设置”(或“余代共单子代数”)形式的镜片。
data Lens s a = Lens {
get :: s -> a,
set :: a -> s -> s
}
以及棱柱的“向上转型-向下转型”表示法。
data Prism s a = Prism {
up :: a -> s,
down :: s -> Maybe a
}
up
将 a
注入到 s
中(不添加任何信息),而 down
则测试 s
是否为 a
。
在 lens
中,up
的拼写为review
,而 down
是preview
。没有 Prism
构造函数;您可以使用智能构造函数 prism'
。
使用 Prism
可以注入和投影求和类型!
_Left :: Prism (Either a b) a
_Left = Prism {
up = Left,
down = either Just (const Nothing)
}
_Right :: Prism (Either a b) b
_Right = Prism {
up = Right,
down = either (const Nothing) Just
}
透镜不支持这个 - 你不能编写一个
Lens (Either a b) a
,因为你无法实现
get :: Either a b -> a
。实际上,你可以编写一个
Traversal (Either a b) a
,但这并不允许你从
a
创建一个
Either a b
- 它只会让你覆盖已经存在的
a
。
Aside: I think this subtle point about Traversal
s is the source of your confusion about partial record fields.
^?
with plain lenses allows getting Nothing
if the field in question doesn't belong to the branch the entity represents
Using ^?
with a real Lens
will never return Nothing
, because a Lens s a
identifies exactly one a
inside an s
.
When confronted with a partial record field,
data Wibble = Wobble { _wobble :: Int } | Wubble { _wubble :: Bool }
makeLenses
will generate a Traversal
, not a Lens
.
wobble :: Traversal' Wibble Int
wubble :: Traversal' Wibble Bool
要了解如何在实践中应用 Prism
,可以参考 Control.Exception.Lens
,其中提供了一组针对 Haskell 可扩展的 Exception
层次结构的 Prism
。这使您能够对 SomeException
进行运行时类型测试,并将特定异常注入到 SomeException
中。
_ArithException :: Prism' SomeException ArithException
_AsyncException :: Prism' SomeException AsyncException
-- etc.
(这些是实际类型的略微简化版本。实际上,这些棱镜是重载的类方法。)
从更高的层面思考,某些整个程序可以被认为是“基本上是一个
Prism
”。编码和解码数据就是一个例子:您总是可以将结构化数据转换为
String
,但并非每个
String
都可以解析回来:
showRead :: (Show a, Read a) => Prism String a
showRead = Prism {
up = show,
down = listToMaybe . fmap fst . reads
}
总之,
Lens
和
Prism
一起编码了面向对象编程的两个核心设计工具:组合和子类型。
Lens
是Java中
.
和
=
运算符的一级版本,而
Prism
是Java中
instanceof
和隐式向上转型的一级版本。
一种有成效的思考
Lens
的方法是,它们为您提供了一种将复合
s
拆分为聚焦值
a
和一些上下文
c
的方法。伪代码:
type Lens s a = exists c. s <-> (a, c)
在这个框架中,
Prism
为您提供了一种将
s
视为
a
或某些上下文
c
的方法。
type Prism s a = exists c. s <-> Either a c
我将留给你自己去证明这些与我上面演示的简单表示是同构的。尝试为这些类型实现get
/set
/up
/down
!
从这个意义上讲,Prism
是一个共变透镜。Either
是(,)
的范畴对偶;Prism
是Lens
的范畴对偶。
您还可以在“Profunctor Optics”公式中观察到这种二元性 - Strong
和Choice
是对偶的。
type Lens s t a b = forall p. Strong p => p a b -> p s t
type Prism s t a b = forall p. Choice p => p a b -> p s t
这就是或多或少表示lens
使用的方式,因为这些Lens
和Prism
非常可组合。您可以使用(.)
将Prism
组合在一起,以获取更大的Prism
(“a
是s
,其中是一个p
”); 将Prism
与Lens
组合会给您一个Traversal
。