什么是棱镜?
What are Prisms?
我试图更深入地了解 lens
库,所以我尝试使用它提供的类型。我已经有了一些使用镜头的经验,知道它们有多么强大和方便。所以我转向棱镜,我有点迷路了。棱镜似乎允许两件事:
- 确定实体是否属于总和类型的特定分支,如果属于,则捕获元组或单例中的基础数据。
- 解构和重建一个实体,可能在过程中对其进行修改。
第一点似乎很有用,但通常不需要实体的所有数据,如果所讨论的字段不属于,^?
使用普通镜片可以得到 Nothing
实体代表的分支,就像棱镜一样。
第二点...我不知道,可能有用吗?
所以问题是:我可以用棱镜做什么,而其他光学器件不能?
编辑:感谢大家的精彩回答和进一步阅读的链接!我希望我能全部接受。
透镜表征有关系;棱镜表征 is-a 关系。
A Lens s a
说“s
有一个 a
”;它有一些方法可以从 s
中恰好得到一个 a
并在 s
中恰好覆盖一个 a
。 Prism s a
表示“a
是一个 s
”;它有方法将 a
向上转换为 s
并(尝试)将 s
向下转换为 a
.
将这种直觉转化为代码可为您提供熟悉的“get-set”(或“costate comonad coalgebra”)镜头公式,
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
and down
is preview
. There’s no Prism
constructor; you use the prism'
smart constructor。
你可以用 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
将 ^?
与真正的 Lens
一起使用永远不会 return Nothing
,因为 Lens s a
在 s
。
当遇到部分记录字段时,
data Wibble = Wobble { _wobble :: Int } | Wubble { _wubble :: Bool }
makeLenses
将生成 Traversal
,而不是 Lens
。
wobble :: Traversal' Wibble Int
wubble :: Traversal' Wibble Bool
有关如何在实践中应用 Prism
的示例,请查看 Control.Exception.Lens
,它提供了 Prism
到 Haskell 的集合可扩展 Exception
层次结构。这使您可以对 SomeException
执行运行时类型测试并将特定异常注入 SomeException
.
_ArithException :: Prism' SomeException ArithException
_AsyncException :: Prism' SomeException AsyncException
-- etc.
(这些是实际类型的略微简化版本。实际上,这些棱镜重载了 class 方法。)
在更高层次上思考,某些整个程序可以被认为是“基本上是一个Prism
”。编码和解码数据就是一个例子:您总是可以将结构化数据转换为 String
,但并非每个 String
都可以解析回来:
showRead :: (Show a, Read a) => Prism String a
showRead = Prism {
up = show,
down = listToMaybe . fmap fst . reads
}
总而言之,Lens
es 和 Prism
s 共同编码了面向对象编程的两个核心设计工具:组合和子类型。 Lens
es 是 Java 的 .
和 =
运算符的第一个 class 版本,Prism
s 是第一个 [= Java 的 instanceof
的 181=] 版本和隐式向上转换。
思考 Lens
es 的一种富有成效的方法是,它们为您提供了一种将复合 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
是 co-[=58=]。 Either
是 (,)
的绝对对偶; Prism
是 Lens
的绝对对偶。
您还可以在 "profunctor optics" formulation - Strong
and 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
s 以获得更大的 Prism
s(“a
是一个 s
, 是a p
") 使用 (.)
;将 Prism
与 Lens
组合在一起会得到 Traversal
.
我刚刚写了一篇博客 post,这可能有助于建立一些关于棱镜的直觉:棱镜是构造函数(透镜是场)。 http://oleg.fi/gists/posts/2018-06-19-prisms-are-constructors.html
棱镜可以作为first-class模式匹配引入,但这是一个
片面的看法。我会说它们是 广义构造函数 ,尽管也许
比实际构造更常用于模式匹配。
构造器(和合法棱镜)的重要属性是它们
内射性。虽然通常的棱镜定律没有直接说明,
可以推导出单射性属性。
引用 lens
-library 文档,棱镜定律是:
首先,如果我 review
一个带有 Prism
的值,然后 preview
,我会取回它:
preview l (review l b) ≡ Just b
其次,如果您可以使用 Prism
l
从值 s
中提取值 a,则
值 s
完全由 l
和 a
:
描述
preview l s ≡ Just a ⇒ review l a ≡ s
其实单凭第一定律就足以证明构造的内射性
通过 Prism
:
review l x ≡ review l y ⇒ x ≡ y
证明很简单:
review l x ≡ review l y
-- x ≡ y -> f x ≡ f y
preview l (review l x) ≡ preview l (review l y)
-- rewrite both sides with the first law
Just x ≡ Just y
-- injectivity of Just
x ≡ y
我们可以使用单射性 属性 作为方程式中的附加工具
推理工具箱。或者我们可以用它作为一个简单的 属性 检查来决定
某事是否合法Prism
。检查很容易,因为我们只
review
Prism
侧。许多智能构造函数,例如
规范化输入数据,不是合法的棱镜。
使用case-insensitive
的示例:
-- Bad!
_CI :: FoldCase s => Prism' (CI s) s
_CI = prism' ci (Just . foldedCase)
λ> review _CI "FOO" == review _CI "foo"
True
λ> "FOO" == "foo"
False
也违反了第一定律:
λ> preview _CI (review _CI "FOO")
Just "foo"
除了其他出色的答案,我觉得 Iso
提供了一个很好的视角来考虑这个问题。
有一些 i :: Iso' s a
意味着如果你有一个 s
值,你也(实际上)有一个 a
值,反之亦然。 Iso'
给你两个转换函数,view i :: s -> a
和 review i :: a -> s
都保证成功和无损。
有一些l :: Lens' s a
意味着如果你有一个s
你也有一个a
,但反之则不然. view l :: s -> a
可能会在转换过程中丢失信息,因为转换不需要无损,因此如果您只有 a
(参见 set l :: a -> s -> s
,除了 a
值之外还需要 s
以提供缺失的信息)。
- 有一些
p :: Prism' s a
意味着如果你有一个 s
值,你 可能 也有一个 a
,但不能保证.转换 preview p :: s -> Maybe a
不保证成功。不过,你确实有另一个方向,review p :: a -> s
.
换句话说,Iso
是可逆的并且总是成功的。如果你放弃可逆性要求,你会得到 Lens
;如果你放弃成功保证,你会得到一个 Prism
。如果你同时放下两者,你会得到一个 affine traversal (which is not in lens as a separate type), and if you go a step further and give up on having at most one target you end up with a Traversal
. That is reflected in one of the diamonds of the lens subtype hierarchy:
Traversal
/ \
/ \
/ \
Lens Prism
\ /
\ /
\ /
Iso
我试图更深入地了解 lens
库,所以我尝试使用它提供的类型。我已经有了一些使用镜头的经验,知道它们有多么强大和方便。所以我转向棱镜,我有点迷路了。棱镜似乎允许两件事:
- 确定实体是否属于总和类型的特定分支,如果属于,则捕获元组或单例中的基础数据。
- 解构和重建一个实体,可能在过程中对其进行修改。
第一点似乎很有用,但通常不需要实体的所有数据,如果所讨论的字段不属于,^?
使用普通镜片可以得到 Nothing
实体代表的分支,就像棱镜一样。
第二点...我不知道,可能有用吗?
所以问题是:我可以用棱镜做什么,而其他光学器件不能?
编辑:感谢大家的精彩回答和进一步阅读的链接!我希望我能全部接受。
透镜表征有关系;棱镜表征 is-a 关系。
A Lens s a
说“s
有一个 a
”;它有一些方法可以从 s
中恰好得到一个 a
并在 s
中恰好覆盖一个 a
。 Prism s a
表示“a
是一个 s
”;它有方法将 a
向上转换为 s
并(尝试)将 s
向下转换为 a
.
将这种直觉转化为代码可为您提供熟悉的“get-set”(或“costate comonad coalgebra”)镜头公式,
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
and down
is preview
. There’s no Prism
constructor; you use the prism'
smart constructor。
你可以用 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 gettingNothing
if the field in question doesn't belong to the branch the entity represents将
^?
与真正的Lens
一起使用永远不会 returnNothing
,因为Lens s a
在s
。 当遇到部分记录字段时,data Wibble = Wobble { _wobble :: Int } | Wubble { _wubble :: Bool }
makeLenses
将生成Traversal
,而不是Lens
。wobble :: Traversal' Wibble Int wubble :: Traversal' Wibble Bool
有关如何在实践中应用 Prism
的示例,请查看 Control.Exception.Lens
,它提供了 Prism
到 Haskell 的集合可扩展 Exception
层次结构。这使您可以对 SomeException
执行运行时类型测试并将特定异常注入 SomeException
.
_ArithException :: Prism' SomeException ArithException
_AsyncException :: Prism' SomeException AsyncException
-- etc.
(这些是实际类型的略微简化版本。实际上,这些棱镜重载了 class 方法。)
在更高层次上思考,某些整个程序可以被认为是“基本上是一个Prism
”。编码和解码数据就是一个例子:您总是可以将结构化数据转换为 String
,但并非每个 String
都可以解析回来:
showRead :: (Show a, Read a) => Prism String a
showRead = Prism {
up = show,
down = listToMaybe . fmap fst . reads
}
总而言之,Lens
es 和 Prism
s 共同编码了面向对象编程的两个核心设计工具:组合和子类型。 Lens
es 是 Java 的 .
和 =
运算符的第一个 class 版本,Prism
s 是第一个 [= Java 的 instanceof
的 181=] 版本和隐式向上转换。
思考 Lens
es 的一种富有成效的方法是,它们为您提供了一种将复合 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
是 co-[=58=]。 Either
是 (,)
的绝对对偶; Prism
是 Lens
的绝对对偶。
您还可以在 "profunctor optics" formulation - Strong
and 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
s 以获得更大的 Prism
s(“a
是一个 s
, 是a p
") 使用 (.)
;将 Prism
与 Lens
组合在一起会得到 Traversal
.
我刚刚写了一篇博客 post,这可能有助于建立一些关于棱镜的直觉:棱镜是构造函数(透镜是场)。 http://oleg.fi/gists/posts/2018-06-19-prisms-are-constructors.html
棱镜可以作为first-class模式匹配引入,但这是一个 片面的看法。我会说它们是 广义构造函数 ,尽管也许 比实际构造更常用于模式匹配。
构造器(和合法棱镜)的重要属性是它们 内射性。虽然通常的棱镜定律没有直接说明, 可以推导出单射性属性。
引用 lens
-library 文档,棱镜定律是:
首先,如果我 review
一个带有 Prism
的值,然后 preview
,我会取回它:
preview l (review l b) ≡ Just b
其次,如果您可以使用 Prism
l
从值 s
中提取值 a,则
值 s
完全由 l
和 a
:
preview l s ≡ Just a ⇒ review l a ≡ s
其实单凭第一定律就足以证明构造的内射性
通过 Prism
:
review l x ≡ review l y ⇒ x ≡ y
证明很简单:
review l x ≡ review l y
-- x ≡ y -> f x ≡ f y
preview l (review l x) ≡ preview l (review l y)
-- rewrite both sides with the first law
Just x ≡ Just y
-- injectivity of Just
x ≡ y
我们可以使用单射性 属性 作为方程式中的附加工具
推理工具箱。或者我们可以用它作为一个简单的 属性 检查来决定
某事是否合法Prism
。检查很容易,因为我们只
review
Prism
侧。许多智能构造函数,例如
规范化输入数据,不是合法的棱镜。
使用case-insensitive
的示例:
-- Bad!
_CI :: FoldCase s => Prism' (CI s) s
_CI = prism' ci (Just . foldedCase)
λ> review _CI "FOO" == review _CI "foo"
True
λ> "FOO" == "foo"
False
也违反了第一定律:
λ> preview _CI (review _CI "FOO")
Just "foo"
除了其他出色的答案,我觉得 Iso
提供了一个很好的视角来考虑这个问题。
有一些
i :: Iso' s a
意味着如果你有一个s
值,你也(实际上)有一个a
值,反之亦然。Iso'
给你两个转换函数,view i :: s -> a
和review i :: a -> s
都保证成功和无损。有一些
l :: Lens' s a
意味着如果你有一个s
你也有一个a
,但反之则不然.view l :: s -> a
可能会在转换过程中丢失信息,因为转换不需要无损,因此如果您只有a
(参见set l :: a -> s -> s
,除了a
值之外还需要s
以提供缺失的信息)。- 有一些
p :: Prism' s a
意味着如果你有一个s
值,你 可能 也有一个a
,但不能保证.转换preview p :: s -> Maybe a
不保证成功。不过,你确实有另一个方向,review p :: a -> s
.
换句话说,Iso
是可逆的并且总是成功的。如果你放弃可逆性要求,你会得到 Lens
;如果你放弃成功保证,你会得到一个 Prism
。如果你同时放下两者,你会得到一个 affine traversal (which is not in lens as a separate type), and if you go a step further and give up on having at most one target you end up with a Traversal
. That is reflected in one of the diamonds of the lens subtype hierarchy:
Traversal
/ \
/ \
/ \
Lens Prism
\ /
\ /
\ /
Iso