如何使用镜头访问总和类型后面的记录字段



我正在尝试使用 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 :: aJust 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

这不是镜片或任何东西的基本理论特性。 这只是makeLensesmakePrisms使用的命名和键入约定。 使用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这里)是一个视光(遍历),它专注于场,但只有当它存在时(即,当值是BarBaz时,而不是当它是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,一切都会顺利进行——没有神奇的元组。

相关内容

  • 没有找到相关文章