我正在尝试使用 Haskell 中的透镜和棱镜访问嵌套记录:
import Data.Text (Text)
import Control.Lens.TH
data State = State
{ _stDone :: Bool
, _stStep :: StateStep
}
data StateStep
= StatePause
| StateRun
{ _stCounter :: Int
, _stMMistake :: Maybe Text
}
makeLenses ''State
makeLenses ''StateStep
makePrisms ''StateStep
main :: IO ()
main = do
let st = State False $ StateRun 0 Nothing
-- works, but the `_2` seems weird
mMistake = st ^? stStep . _StateStepRun . _2 . _Just
-- why not something like (the following does not compile)
mMistake = st ^. stStep . _StateStepRun . _Just . stMMistake
有效的路线留下了一些悬而未决的问题。我不确定类型是否巧合匹配。字段_stMMistake
的类型为Maybe Text
,但是呢
let st = State False StatePause
?我错过了明确的join
.
我对棱镜的工作原理一无所知。虽然棱镜给我一个元组似乎是合乎逻辑的,但与此同时,我希望有一些可组合的东西,因为我可以使用镜头更深入地进入我的嵌套结构。也许我必须为此手动派生我的实例吗?
更新:根据评论,我修复了一些错误并在[[双方括号]]中添加了一些旁白。
以下是您的第一个mMistake
工作的方式/原因......
棱镜是一种光学元件,它专注于"整体"中可能存在也可能不存在的"部分"。 [[从技术上讲,它侧重于可用于重建整个整体的部分类型,因此它实际上涉及可以以几种替代形式出现的整体(如总和类型的情况),其中"部分"是这些替代形式之一。 但是,如果您只使用棱镜进行查看而不进行设置,则此附加功能并不太重要。
在您的示例中,_StateRun
和_Just
都是棱镜。_Just
棱镜侧重于Maybe a
整体的a
部分。 这种a
可能存在,也可能不存在。 如果Maybe a
值对于某些x :: a
是Just x
的,则部分a
存在并且具有价值x
,这就是_Just
关注的。 如果Maybe a
值为Nothing
,则部分a
不存在,并且_Just
不关注任何内容。
对于您的棱镜_StateRun
来说,这有点相似. 如果整个StateStep
是一个StateRun x y
值,那么_StateRun
专注于该"部分",表示为StateRun
构造函数字段的元组,即(x, y) :: (Int, Maybe Text)
。 另一方面,如果整个StateStep
是一个StatePause
,则该部分不存在,棱镜不会聚焦于任何东西。
当您组合棱镜(如_StateRun
和_Just
)和透镜(如stStep
和_2
)时,您将创建一个结合了组合的一系列聚焦操作的新光学元件。
[[正如评论中指出的那样,这种新的光学器件不是棱镜;它"只是"遍历。 事实上,这是一种特殊的遍历,称为"仿射遍历"。 普通遍历可以聚焦于零个或多个零件,而仿射遍历则专注于零个(零件不存在)或一个(存在唯一零件)。 但是,lens
库没有区分仿射遍历和其他类型的遍历。 新光学器件"只是"仿射遍历而不是棱镜的原因与早期的技术点有关。 一旦你添加了镜头,你就失去了从单个"部分"重建整个"整体"的能力。 同样,如果您只使用光学器件进行观看,而不是设置,那将无关紧要。
无论如何,考虑光学(仿射遍历):
optic1 = stStep . _StateRun . _2 . _Just
该光学视图为整个State
型。 第一个镜头stStep
专注于其StateStep
领域。 如果该StateStep
是一个StateRun x (Just y)
值,则_StateRun
棱镜专注于(x, Just y)
部分,而_2
透镜进一步关注Just y
部分,而_Just
棱镜进一步关注y :: Text
部分。
另一方面,如果StateStep
场是StatePause
,光学optic1
不关注任何东西(因为第二分量棱镜_StateRun
不关注任何东西),如果是StateRun x Nothing
,光学optic1
仍然不关注任何东西,因为即使_StateRun
可以专注于(x, Nothing)
,_2
也可以专注于Nothing
, 最后的_Just
不会聚焦于任何东西,所以整个光学器件无法聚焦。
特别是,在处理StatePause
时,镜头_2
不会"失火"并尝试引用丢失的第二个场或类似的东西。 您使用_StateRun
来关注StateRun
构造函数的字段元组这一事实可确保如果整个光学聚焦,则所需的字段将存在。
现在,这就是您的第二个光学器件的原因:
optic2 = stStep . _StateRun . _Just . stMMistake
不行...
实际上有两个问题。 首先,stStep . _StateRun
需要整个State
,并专注于(Int, Maybe Text)
的一部分。 这不是一个Maybe
值,因此它还不能与_Just
棱镜组成。 您想先选择Maybe Text
字段,然后应用_Just
棱镜,因此您实际想要的更像是:
optic3 = stStep . _StateRun . stMMistake . _Just
这看起来真的应该有效,对吧?stStep
镜头聚焦在StateStep
上,_StateRun
棱镜应该只在存在StateRun x y
值时才聚焦,镜头stMMistake
应该让你聚焦在y :: Maybe Text
上,让_Just
聚焦在Text
上。
不幸的是,这不是用makePrisms
创建的棱镜的工作方式。_StateRun
棱镜专注于一个带有未命名字段的普通旧元组,这些字段需要使用_1
、_2
等进一步选择,而不是stMMistake
哪个试图选择一个命名字段。
事实上,如果你仔细看一下stMMistake
,你会发现 - 本身 - 它是一个光学(仿射遍历,或者就lens
库而言,只是一个遍历),它需要整个StateStep
并直接关注_stMMistake
字段部分,而无需指定构造函数。 因此,您实际上可以使用stMMistake
代替_StateStepRun . _2
,并且以下内容应该以相同的方式工作:
mMistake = st ^? stStep . _StateStepRun . _2 . _Just
mMistake = st ^? stStep . stMMistake . _Just
这不是镜片或任何东西的基本理论特性。 这只是makeLenses
和makePrisms
使用的命名和键入约定。 使用makeLenses
,您可以创建专注于数据结构命名字段的光学器件。 如果只有一个构造函数:
data Foo = Bar { _x :: Int, _y :: Double }
或者,如果有多个构造函数,但该字段存在于所有构造函数中:
data Foo = Bar { _x :: Int, _y :: Double }
| Baz { _x :: Int, _z :: Char }
那么视场光学(在本例中x
)是一个始终聚焦在该场上的透镜。 如果有多个构造函数,有些构造函数具有字段,有些没有:
data Foo = Bar { _x :: Int, _y :: Double }
| Baz { _x :: Int, _z :: Char }
| Quux { _f :: Int -> Double }
那么视场(x
这里)是一个视光(遍历),它专注于场,但只有当它存在时(即,当值是Bar
或Baz
时,而不是当它是Quux
时)。
另一方面,makePrisms
总是创建构造函数棱镜,这些棱镜将字段作为未命名元组,并且这些字段需要使用_1
、_2
等引用,而不是这些字段在该构造函数中碰巧具有的任何名称。
也许这回答了你的问题?
当 sum 类型构造函数每个最多有一个字段时,光学通常工作得更干净。在你的情况下,你可以写一些类似的东西
data StateStep
= StatePause
| StateRun {-# UNPACK #-} !Runny
data Runny = Runny
{ _ryCounter :: Int
, _ryNoMistake :: Maybe Text
}
使用严格字段和(因为该字段在-funpack-small-strict-fields
意义上并不"小"){-# UNPACK #-}
杂注,您可以确保StateStep
具有与代码中相同的运行时表示形式。但是现在你可以把漂亮的场镜头放到Runny
,一切都会顺利进行——没有神奇的元组。