棱镜是什么?

45
我正在尝试更深入地了解lens库,因此我在使用它提供的类型进行实践。我已经有一些使用镜头的经验,并知道它们是多么强大和方便。所以我转向了Prism,但有点迷失了。似乎prisms允许两件事情:
  1. 确定实体是否属于和一个总类型中的特定分支,如果是,则在元组或单例中捕获底层数据。
  2. 分解和重建实体,可能在过程中修改它。
第一点似乎很有用,但通常人们不需要实体中的所有数据,而^?与普通的lenses一样,允许获取Nothing,如果所询问的字段不属于实体代表的分支,就像prisms一样。
对于第二个点,我不知道,可能会有用途?
因此问题是:使用Prism我可以做什么,而其他的光学工具无法完成? 编辑:谢谢大家提供的出色答案和进一步阅读的链接!我希望我可以接受它们所有的回答。

我已经修改了你的问题标题。希望你不介意! - Benjamin Hodgson
3个回答

44
镜片(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
}

upa 注入到 s 中(不添加任何信息),而 down 则测试 s 是否为 a

lens 中,up 的拼写为review,而 downpreview。没有 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 Traversals 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
}

总之,LensPrism一起编码了面向对象编程的两个核心设计工具:组合和子类型。 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(,)的范畴对偶;PrismLens的范畴对偶。

您还可以在“Profunctor Optics”公式中观察到这种二元性 - StrongChoice是对偶的。

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使用的方式,因为这些LensPrism非常可组合。您可以使用(.)Prism组合在一起,以获取更大的Prism(“as,其中是一个p”); 将PrismLens组合会给您一个Traversal


仅仅那一行代码就解决了很多困惑。谢谢。 - cobbal

17

我刚写了一篇博客文章,它可以帮助建立有关棱镜的直觉:棱镜是构造函数(镜头是字段)。http://oleg.fi/gists/posts/2018-06-19-prisms-are-constructors.html


棱镜可以作为一流模式匹配来介绍,但这只是一个单向观点。我认为它们是广义构造函数,虽然它们更常用于模式匹配而不是实际构造。

构造函数的重要属性(和合法的棱镜)是它们的可注入性。尽管通常的棱镜定律没有直接说明,但可推导出可注入性属性。

引用lens库文档,棱镜定律如下:

首先,如果我用Prism回顾一个值,然后再用preview,我会得到它回来:

preview l (review l b) ≡ Just b

其次,如果您可以使用 Prism l 从值 s 中提取一个值 a,那么值 s 就完全由 la 描述:

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的属性。 这个检查很容易,因为我们只需要关注Prismreview方向。许多智能构造函数(例如规范化输入数据)不是合法的prisms。

使用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"

非常好。到目前为止,如果我理解正确,我们只是展示了每个合法棱镜的“review”操作是一个注入函数。我们还能否反过来证明每个注入函数都引出一个合法的棱镜? - Asad Saeeduddin
@AsadSaeeduddin 并非每个注入都会引起棱镜。我们需要所谓的“可决嵌入(注入)”。实际上,这些是等价的:可决定性要求本质上是要求给出棱镜的“预览”/“匹配”部分。一个非Haskell示例是注入到“Type”中。一个非可决注入的Haskell示例可能是forward :: (Integer -> Natural) -> (Integer -> Integer); forward f = toInteger . f。给定一个g :: Integer -> Integer,您需要测试所有n :: Integer以确保g n为非负数。这是不可计算的。 - phadej

10

除了其他出色的答案外,我认为 Iso 为考虑此问题提供了一个很好的视角。

  • 如果存在一些 i :: Iso' s a,这意味着如果您有一个 s 值,您也(实际上)有一个 a 值,反之亦然。 Iso' 提供了两个转换函数,view i :: s -> areview i :: a -> s,它们都保证成功且无损失。

  • 如果存在一些 l :: Lens' s a,这意味着如果您有一个 s,您也有一个 a,但反过来不一定成立。由于转换不需要是无损的,因此 view l :: s -> a 可能会丢失信息,在这种情况下,如果您只有一个 a,则不能走另一条路径(与 set l :: a -> s -> s 相比,后者还需要一个 s 来提供缺失的信息)。

  • 如果存在一些 p :: Prism' s a,这意味着如果您有一个 s 值,您可能也有一个 a,但不能保证。转换 preview p :: s -> Maybe a 无法保证成功。尽管如此,您还是可以使用另一种方法 review p :: a -> s

换句话说,Iso 是可逆且始终成功的。如果放弃可逆性要求,则会得到一个 Lens;如果放弃成功保证,则会得到一个 Prism。如果两者都放弃,则会得到一个仿射变换(作为单独类型不在 lens 中),如果进一步放弃至多有一个目标的要求,则会得到一个 Traversal。这反映在 lens 子类型层次结构中的一个菱形图中:

 Traversal
    / \
   /   \
  /     \
Lens   Prism
  \     /
   \   /
    \ /
    Iso

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接