什么是棱镜?

What are Prisms?

我试图更深入地了解 lens 库,所以我尝试使用它提供的类型。我已经有了一些使用镜头的经验,知道它们有多么强大和方便。所以我转向棱镜,我有点迷路了。棱镜似乎允许两件事:

  1. 确定实体是否属于总和类型的特定分支,如果属于,则捕获元组或单例中的基础数据。
  2. 解构和重建一个实体,可能在过程中对其进行修改。

第一点似乎很有用,但通常不需要实体的所有数据,如果所讨论的字段不属于,^? 使用普通镜片可以得到 Nothing实体代表的分支,就像棱镜一样。

第二点...我不知道,可能有用吗?

所以问题是:我可以用棱镜做什么,而其他光学器件不能?

编辑:感谢大家的精彩回答和进一步阅读的链接!我希望我能全部接受。

透镜表征关系;棱镜表征 is-a 关系。

A Lens s a 说“s 有一个 a”;它有一些方法可以从 s 中恰好得到一个 a 并在 s 中恰好覆盖一个 aPrism 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
}

upa 注入 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 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

^? 与真正的 Lens 一起使用永远不会 return Nothing,因为 Lens s as。 当遇到部分记录字段时,

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
}

总而言之,Lenses 和 Prisms 共同编码了面向对象编程的两个核心设计工具:组合和子类型。 Lenses 是 Java 的 .= 运算符的第一个 class 版本,Prisms 是第一个 [= Java 的 instanceof 的 181=] 版本和隐式向上转换。


思考 Lenses 的一种富有成效的方法是,它们为您提供了一种将复合 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 对于这些类型!)

在这个意义上 Prismco-[​​=58=]Either(,) 的绝对对偶; PrismLens 的绝对对偶。

您还可以在 "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 使用的表示,因为这些 LensPrism 是非常可组合的。您可以组合 Prisms 以获得更大的 Prisms(“a 是一个 s 是a p") 使用 (.);将 PrismLens 组合在一起会得到 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 完全由 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。检查很容易,因为我们只 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 -> areview 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