什么是棱镜?

Mic*_*ail 35 haskell haskell-lens

我正在努力更深入地了解lens库,因此我会使用它提供的类型.我已经有了一些镜片经验,并且知道它们有多么强大和方便.所以我转向棱镜,我有点迷失了.似乎棱镜允许两件事:

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

第一点看起来很有用,但通常一个不需要来自实体的所有数据,并且^?使用普通镜头可以获得所讨论Nothing的字段不属于实体所代表的分支,就像使用棱镜一样.

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

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

编辑:谢谢大家的优秀答案和进一步阅读的链接!我希望我能接受他们.

Ben*_*son 34

镜片描述了这种关系; 棱镜描述了is-a关系.

A Lens s a说" s a "; 它有一些方法可以从中获得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
}
Run Code Online (Sandbox Code Playgroud)

和棱镜的"向上倾斜"表示,

data Prism s a = Prism {
    up :: a -> s,
    down :: s -> Maybe a
}
Run Code Online (Sandbox Code Playgroud)

up注入一个as(不添加任何信息),并down测试是否s是一个a.

lens,up拼写reviewdownpreview.没有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
}
Run Code Online (Sandbox Code Playgroud)

镜头不支持这个 - 你不能写一个Lens (Either a b) a因为你无法实现get :: Either a b -> a.实际上,你可以编写一个Traversal (Either a b) a,但是这不允许你创建Either a b一个a- 它只会让你覆盖a已经存在的东西.

旁白:我认为关于Traversals的这个微妙的观点是你对部分记录字段混淆的根源.

^?Nothing如果相关字段不属于实体所代表的分支,则使用普通镜头可以获得

使用^?真实Lens将永远不会返回Nothing,因为a Lens s a确切地标识了一个a内部s.当遇到部分记录字段时,

data Wibble = Wobble { _wobble :: Int } | Wubble { _wubble :: Bool }
Run Code Online (Sandbox Code Playgroud)

makeLenses会生成一个Traversal,而不是一个Lens.

wobble :: Traversal' Wibble Int
wubble :: Traversal' Wibble Bool
Run Code Online (Sandbox Code Playgroud)

有关如何Prism在实践中应用s的一个示例,请查看Control.Exception.Lens,它将一组Prisms提供到Haskell的可扩展Exception层次结构中.这使您可以对SomeExceptions 执行运行时类型测试并将特定异常注入SomeException.

_ArithException :: Prism' SomeException ArithException
_AsyncException :: Prism' SomeException AsyncException
-- etc.
Run Code Online (Sandbox Code Playgroud)

(这些是实际类型的略微简化版本.实际上这些棱镜是重载的类方法.)

在更高层次上思考,某些整个程序可以被认为是"基本上Prism".编码和解码数据就是一个例子:您始终可以将结构化数据转换为a String,但不是每个String都可以解析回来:

showRead :: (Show a, Read a) => Prism String a
showRead = Prism {
    up = show,
    down = listToMaybe . fmap fst . reads
}
Run Code Online (Sandbox Code Playgroud)

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


一种富有成效的思考方式Lens是,它们为您提供了一种将复合s分解为聚焦值a和某种上下文的方法c.伪代码:

type Lens s a = exists c. s <-> (a, c)
Run Code Online (Sandbox Code Playgroud)

在这个框架中,a Prism为您提供了一种将a s视为一个a或一些上下文的方法c.

type Prism s a = exists c. s <-> Either a c
Run Code Online (Sandbox Code Playgroud)

(我会告诉你自己这些与我上面演示的简单表示形式是同构的.尝试为这些类型实现get/ set/ up/ down!)

在这个意义上,a Prism是一个共同的Lens.Either是分类的双重(,); Prism是绝对的双重Lens.

您还可以在"探测器光学"配方中观察到这种二元性- 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
Run Code Online (Sandbox Code Playgroud)

这或多或少是lens使用的表示,因为这些Lenses和Prisms是非常可组合的.你可以编写Prisms来获得更大的Prisms(" a 是一个 s,这是一个 p ")(.); Prism用a 组成a Lens给你一个Traversal.


pha*_*dej 15

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


棱镜可以作为一流的模式匹配引入,但这是一种片面的观点.我会说它们是广义构造函数,虽然可能更常用于模式匹配而不是实际构造.

建构者(和合法棱镜)的重要属性是它们的注入性.虽然通常的棱镜法没有直接说明,但可以推断出注入性.

引用 - lens图书馆文献,棱镜法律是:

首先,如果我review是a的值Prism然后preview,我会得到它:

preview l (review l b) ? Just b
Run Code Online (Sandbox Code Playgroud)

其次,如果可以提取的值的使用Prism l从一个值s,则该值s完全被描述la:

preview l s ? Just a ? review l a ? s
Run Code Online (Sandbox Code Playgroud)

事实上,仅凭第一定律足以通过以下方式证明建筑的注入性Prism:

review l x ? review l y ? x ? y
Run Code Online (Sandbox Code Playgroud)

证据很简单:

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
Run Code Online (Sandbox Code Playgroud)

我们可以使用injectivity属性作为等式推理工具箱中的附加工具.或者我们可以使用它作为一个简单的属性来检查以确定某些东西是否合法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
Run Code Online (Sandbox Code Playgroud)

第一部法律也遭到违反:

?> preview _CI (review _CI "FOO")
Just "foo"
Run Code Online (Sandbox Code Playgroud)


dup*_*ode 10

除了其他优秀的答案之外,我觉得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,s除了a价值之外还要求除了提供缺失的信息).

  • 有一些p :: Prism' s a方法,如果你有一个s可能也有的价值a,但没有保证.转换preview p :: s -> Maybe a不能保证成功.不过,你确实有另一个方向review p :: a -> s.

换句话说,一个Iso是可逆的并且总是成功的.如果你放弃了可逆性要求,你会得到一个Lens; 如果你放弃成功保证,你得到一个Prism.如果你放弃两者,你会得到一个仿射遍历(不是透镜作为一个单独的类型),如果你更进一步,放弃最多只有一个目标你最终得到一个Traversal.这反映在的钻石之一透镜亚型层次结构:

 Traversal
    / \
   /   \
  /     \
Lens   Prism
  \     /
   \   /
    \ /
    Iso
Run Code Online (Sandbox Code Playgroud)