J. *_*son 12
为了描述部分镜头 - 我将在今后称之为Haskell lens命名法,棱镜(除了它们不是!请参见Ørjan的评论) - 我想首先对镜头本身进行不同的观察.
镜头Lens s a表示给定一个s我们可以"聚焦" s在类型的子组件上a,查看它,替换它,以及(如果我们使用镜头系列变体Lens s t a b)甚至改变它的类型.
一种看待这种情况的方法是Lens s a见证一种同构,等价,s以及(r, a)某种未知类型的元组类型r.
Lens s a ====== exists r . s ~ (r, a)
Run Code Online (Sandbox Code Playgroud)
这给了我们所需要的东西,因为我们可以拉a出来,替换它,然后通过等价向后运行,以获得一个新的s更新a.
现在让我们花一点时间通过代数数据类型刷新我们的高中代数.ADT中的两个关键操作是乘法和求和.我们写的类型a * b时,我们有一种由具有项目都在a和b我们写a + b的时候,我们有一个类型由它们的项目无论是 a或b.
在Haskell中,我们写a * b为(a, b)元组类型.我们写a + b为Either a b,两种类型.
产品将数据捆绑在一起,总和代表捆绑选项.产品可以表示只有一个你想要选择的东西(一次),而总和代表失败的想法,因为你希望采取一个选项(在左侧,比方说),而是定居于另一个(沿着右边).
最后,总和和产品是绝对双重的.正如大多数PL所做的那样,它们组合在一起而没有另一个,让你处于一个尴尬的地方.
那么让我们来看看当我们将上面的镜片配方二元化(部分)时会发生什么.
exists r . s ~ (r + a)
Run Code Online (Sandbox Code Playgroud)
这是一个声明,s就是无论是一个类型a 或一些其他的事情r.我们有一个类似lens的东西,体现了选择(和失败)的概念,深入到它的核心.
这恰好是一个棱镜(或部分镜头)
Prism s a ====== exists r . s ~ (r + a)
exists r . s ~ Either r a
Run Code Online (Sandbox Code Playgroud)
那么这对一些简单的例子有什么用呢?
好吧,考虑一下"无视"列表的棱镜:
uncons :: Prism [a] (a, [a])
Run Code Online (Sandbox Code Playgroud)
它等同于此
head :: exists r . [a] ~ (r + (a, [a]))
Run Code Online (Sandbox Code Playgroud)
r这里需要的是相对明显的:完全失败,因为我们有一个空列表!
为了证实这种类型,a ~ b我们需要编写一种方法来将a转换a为a b和a b转换为a使它们相互反转的方式.让我们写一下,以便通过神话功能来描述我们的棱镜
prism :: (s ~ exists r . Either r a) -> Prism s a
uncons = prism (iso fwd bck) where
fwd [] = Left () -- failure!
fwd (a:as) = Right (a, as)
bck (Left ()) = []
bck (Right (a, as)) = a:as
Run Code Online (Sandbox Code Playgroud)
这演示了如何使用这种等价(至少在原则上)来创建棱镜,并且还表明当我们使用类似列表的类型之类时,它们应该感觉非常自然.
镜头是一种"功能参考",允许您以更大的值提取和/或更新广义的"字段".对于普通的非部分镜头,该场总是需要在那里,对于任何包含类型的值.如果您想要查看可能并非总是存在的"字段"之类的内容,则会出现问题.例如,在"列表的第n个元素"(如Scalaz文档@ChrisMartin粘贴中列出)的情况下,列表可能太短.
因此,"部分透镜"将透镜概括为场可以或可以不总是以更大的值存在的情况.
Haskell lens库中至少有三件事你可以认为是"部分镜头",其中没有一件完全符合Scala版本:
它们都有它们的用途,但前两个太受限制而不包括所有情况,而Traversals"太笼统"了.在这三个中,只有Traversals支持"列表的第n个元素"示例.
对于" Lens给出一个Maybe包装好的价值"版本,镜头规则有什么突破:要有一个合适的镜头,你应该能够将其设置Nothing为删除可选字段,然后将其设置回原来的状态,然后获得回到相同的价值.这对于Map说法(并且Control.Lens.At.at为类似Map容器提供这样的镜头)工作正常,但不适用于列表,其中删除例如第0th个元素不能避免干扰后者.
A Prism在某种意义上是构造函数(在Scala中大致为case类)而不是字段的泛化.因此,它所提供的"字段"应该包含重新生成整个结构的所有信息(您可以使用该review函数执行此操作.)
A Traversal可以做"列表的第n个元素"就好了,实际上至少有两个不同的函数ix,element它们都适用于此(但与其他容器略有不同).
由于类型类魔术lens,任何Prism或Lens自动作为a Traversal,而Lens给予一个Maybe包装的可选字段可以通过组合变成Traversal一个普通的可选字段traverse.
但是,a Traversal在某种意义上太笼统,因为它不限于单个字段:A Traversal可以包含任意数量的"目标"字段.例如
elements odd
Run Code Online (Sandbox Code Playgroud)
是一个Traversal将愉快地通过列表的所有奇数索引元素,更新和/或从中提取信息的所有元素.
从理论上讲,你可以定义第四种变体("仿射遍历"@ J.Abrahamson提及)我认为可能更接近于Scala的版本,但由于lens图书馆本身以外的技术原因,它们不适合其他库的一部分 - 您必须明确地转换这样的"部分镜头"以使用它的一些Traversal操作.
而且,它不会比普通的Traversals 买得多,因为有一个简单的运算符(^?)只提取遍历的第一个元素.
(据我所知,技术原因是Pointed定义"仿射遍历"所需的类型类不是Applicative普通Traversal用途的超类.)
下面是Scalaz年代scaladocs LensFamily和PLensFamily,重点增加对diff文件.
镜片:
A镜头系列,提供访问和检索纯功能的手段场从型过渡
B1到类型B2从类型转换同时记录A1输入A2.scalaz.Lens是A1 =:= A2和,和的一个方便的别名B1 =:= B2.术语"字段"不应被限制性地解释为表示类的成员.例如,镜头系列可以解决a的成员资格问题
Set.
部分镜头:
部分透镜家庭,提供访问和检索一个纯粹的功能性装置的可选字段从型转变
B1键入B2在被同时从型转变的记录A1键入A2.scalaz.PLens是A1 =:= A2和,和的一个方便的别名B1 =:= B2.术语"字段"不应被限制性地解释为表示类的成员.例如,部分镜头族可以解决a的第n个元素
List.
对于那些不熟悉scalaz的人,我们应该指出符号类型别名:
type @>[A, B] = Lens[A, B]
type @?>[A, B] = PLens[A, B]
Run Code Online (Sandbox Code Playgroud)
在中缀表示法中,这意味着从类型B记录中检索类型字段的镜头类型A表示为A @> B,并且部分镜头表示为A @?> B.
Argonaut(一个JSON库)提供了很多部分镜头的例子,因为JSON的无模式特性意味着尝试从任意JSON值检索某些东西总是有失败的可能性.以下是Argonaut镜头构造功能的几个例子:
def jArrayPL: Json @?> JsonArray - 仅在JSON值为数组时检索值def jStringPL: Json @?> JsonString - 仅在JSON值为字符串时检索值def jsonObjectPL(f: JsonField): JsonObject @?> Json - 仅在JSON对象具有该字段时检索值 fdef jsonArrayPL(n: Int): JsonArray @?> Json - 仅当JSON数组在索引处具有元素时才检索值 n