如何使用 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 :: a
的 Maybe 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
这不是一些关于镜头或任何东西的基本理论 属性。这只是 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
中加入漂亮的物镜,一切都会很好地进行——没有 magicked-up 个元组。
我正在尝试使用 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 :: a
的 Maybe 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
这不是一些关于镜头或任何东西的基本理论 属性。这只是 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
中加入漂亮的物镜,一切都会很好地进行——没有 magicked-up 个元组。