如何使用 lens 访问 sum 类型后面的记录字段

How to use lens to access a record field behind a sum type

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

而且我对棱镜的工作原理一无所知。虽然棱镜给我一个元组似乎是合乎逻辑的,但与此同时,我期待一些可组合的东西,因为我可以使用透镜更深入地了解我的嵌套结构。我是否必须为此手动派生实例?

更新: 根据评论,我修正了一些错误并在 [[双方括号]].

中添加了一些旁白

这是 how/why 你的第一个 mMistake 作品...

棱镜是一种聚焦于“部分”的光学器件,“部分”可能存在也可能不存在于“整体”中。 [[从技术上讲,它着重于可用于重建整个整体的部分类型,因此它实际上属于可以以多种替代形式出现的整体(如求和类型),其中“部分" 是其中一种替代形式。但是,如果您只是使用棱镜进行观察而不进行设置,那么这个附加功能就不太重要了。]]

在你的例子中,_StateRun_Just都是棱镜。 _Just 棱镜聚焦于 Maybe a 整体的 a 部分。这样的 a 可能存在也可能不存在。如果某些 x :: aMaybe 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,你创建了一个新的光学器件,结合了组合系列对焦操作。

[[正如评论中指出的那样,这种新光学器件不是棱镜;这是“唯一”的遍历。事实上,这是一种特殊的遍历,称为“仿射遍历”。 run-of-the-mill 遍历可以关注零个或多个部分,而仿射遍历恰好关注零个(部分不存在)或一个(唯一部分存在)。不过,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 still 不关注任何东西,因为即使 _StateRun可以聚焦在(x, Nothing)上,而_2可以聚焦在Nothing上,最后的_Just没有聚焦在任何东西上,所以整个光学元件无法聚焦。

特别是,镜头 _2 在处理 StatePause 并尝试引用缺失的第二个字段或类似内容时不会“失灵”。您使用 _StateRun 来关注 StateRun 构造函数的字段元组这一事实确保了如果整个光学器件聚焦,所需的字段将存在。

现在,这就是为什么你的第二个光学元件:

optic2 = stStep . _StateRun . _Just . stMMistake

没用...

其实有两个问题。首先,stStep . _StateRun取一个整体State,着重于一个部分(Int, Maybe Text)。这不是 Maybe 值,因此它还不能与 _Just 棱镜合成。你想先 select Maybe Text 场, 然后 应用 _Just 棱镜,所以你真正想要的是:

optic3 = stStep . _StateRun . stMMistake . _Just

这看起来确实应该可行,对吧? stStep 镜头对焦于 StateStep_StateRun 棱镜应仅在存在 StateRun x y 值时对焦,而镜头 stMMistake 应该让您对焦在 y :: Maybe Text 上,让 _Just 专注于 Text

不幸的是,这不是用 makePrisms 创建的棱镜的工作原理。 _StateRun 棱镜专注于具有未命名字段的普通旧元组,这些字段需要进一步 select 编辑 _1_2 等,而不是 stMMistake 正在尝试 select 命名字段。

事实上,如果你仔细看一下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 中加入漂亮的物镜,一切都会很好地进行——没有 magicked-up 个元组。