[英]How to derive generic traversals that involve a type family
When configuring our applications, often the way that field is defined is the same as the way the field is used: 配置应用程序时,通常定义字段的方式与字段的使用方式相同:
data CfgMyHostName = CfgMyHostName Text
Other times, they differ. 其他时候,他们不同。 Let's make this formal in a typeclass:
让我们在类型类中做出正式的:
data UsagePhase = ConfigTime | RunTime -- Used for promotion to types
class Config (a :: UsagePhase -> *) where
type Phase (p :: UsagePhase) a = r | r -> a
toRunTime :: Phase ConfigTime a -> IO (Phase RunTime a)
data DatabaseConfig (p :: UsagePhase)
instance Config DatabaseConfig where
type Phase ConfigTime DatabaseConfig = ConnectInfo
type Phase RunTime DatabaseConfig = ConnectionPool
toRunTime = connect
A typical service config has many fields, with some in each category. 典型的服务配置有许多字段,每个字段中都有一些字段。 Parameterizing the smaller components that we will compose together lets us write the big composite record once, rather than twice (once for the config specification, once for the runtime data).
参数化我们将组合在一起的较小组件让我们编写一次大复合记录,而不是两次(一次用于配置规范,一次用于运行时数据)。 This is similar to the idea in the 'Trees that Grow' paper:
这类似于“树木成长”论文中的想法:
data UiServerConfig (p :: UsagePhase) = CfgUiServerC {
userDatabase :: Phase p DatabaseConfig
cmsDatabase :: Phase p DatabaseConfig
...
kinesisStream :: Phase p KinesisConfig
myHostName :: CfgMyHostName
myPort :: Int
}
UiServerConfig
is one of many such services I'd like to configure, so it would be nice to derive Generic
for such record types, and to add a default toRunTime
implementation to the Config
class. UiServerConfig
是我想要配置的众多此类服务之一,因此为这些记录类型派生Generic
并向Config
类添加默认的toRunTime
实现会更好。 This is where we get stuck. 这是我们陷入困境的地方。
Given a type parameterized like data Foo f = Foo { foo :: TypeFn f Int, bar :: String}
, how do I generically derive a traversal for any type like Foo
which affects every TypeFn
record field (recursively)? 给定一个类型参数化的
data Foo f = Foo { foo :: TypeFn f Int, bar :: String}
,我如何为像Foo
这样影响每个TypeFn
记录字段(递归)的任何类型一般导出遍历?
As just one example of my confusion, I attempted to use generics-sop like this: 作为我混淆的一个例子,我试图使用泛型 - 如下:
gToRunTime :: (Generic a, All2 Config xs)
=> Phase ConfigTime xs
-> IO (Phase RunTime xs)
gToRunTime = undefined
This fails because xs :: [[*]]
, but Config
takes a type argument with kind a :: ConfigPhase -> *
这失败是因为
xs :: [[*]]
,但Config
采用类型为a :: ConfigPhase -> *
的类型参数a :: ConfigPhase -> *
Any hints about what to read in order to get untangled would really be appreciated. 任何关于要阅读什么以获得解开的提示都会受到赞赏。 Full solutions are acceptable too :)
完整的解决方案也是可以的
Edit: Updated to automatically derive the AtoB
class. 编辑:已更新为自动派生
AtoB
类。
Here's a solution that appears to work. 这是一个似乎有效的解决方案。
Here are the preliminaries: 以下是预赛:
{-# LANGUAGE DataKinds, DeriveGeneric, FlexibleContexts,
FlexibleInstances, KindSignatures, MultiParamTypeClasses,
StandaloneDeriving, TypeFamilies, TypeFamilyDependencies,
TypeSynonymInstances, UndecidableInstances #-}
{-# OPTIONS_GHC -Wall #-}
import qualified GHC.Generics as GHC
import Generics.SOP
Now, suppose we have a Phase
: 现在,假设我们有一个
Phase
:
data Phase = A | B
and a Selector
for the field: 和该领域的
Selector
:
data Selector = Bar | Baz
with the idea that there's a type class with both (1) an associated type family giving the concrete field types associated with a selector for each possible phase and (2) an interface for mapping between phases: 认为存在一个带有(1)关联类型族的类型类,给出与每个可能阶段的选择器相关联的具体字段类型,以及(2)用于在阶段之间映射的接口:
class IsField (sel :: Selector) where
type Field (p :: Phase) sel = r | r -> sel
fieldAtoB :: Field 'A sel -> Field 'B sel
Given a record with a generic instance incorporating both Field
s and non- Field
s 给出包含
Field
s和非Field
s的通用实例的记录
data Foo p = Foo { bar :: Field p 'Bar
, baz :: Field p 'Baz
, num :: Int
} deriving (GHC.Generic)
deriving instance Show (Foo 'A)
deriving instance Show (Foo 'B)
instance Generic (Foo p)
and a Foo 'A
value: 和一个
Foo 'A
值:
foo0 :: Foo 'A
foo0 = Foo (BarA ()) (BazA ()) 1
we'd like to define a generic phase mapping gAtoB
: 我们想定义一个通用的相位映射
gAtoB
:
foo1 :: Foo 'B
foo1 = gAtoB foo0
that uses per-field phase maps fieldAtoB
from the IsField
type class. 使用来自
IsField
类型类的每场相位映射fieldAtoB
。
The key step is defining a separate type class AtoB
dedicated to the phase A
-to- B
transition to act as a bridge to the IsField
type class. 关键步骤是定义一个专用于
A
to- B
阶段转换的独立类型AtoB
,作为IsField
类型类的桥梁。 This AtoB
type class will be used in conjuction with the generics-sop
machinery to constrain/match the concrete phase A
and B
types field by field and dispatch to the appropriate fieldAtoB
phase mapping function. 该
AtoB
类型类将与generics-sop
机器结合使用,以逐字段约束/匹配具体的A
相和B
类型,并调度到适当的fieldAtoB
相位映射函数。 Here's the class: 这是班级:
class AtoB aty bty where
fieldAtoB' :: aty -> bty
Fortunately, instances can be automatically derived for Field
s, though it requires the (mostly harmless) UndecidableInstances
extension: 幸运的是,可以为
Field
s自动派生实例,但它需要(大多数无害的) UndecidableInstances
扩展:
instance (IsField sel, Field 'A sel ~ aty, Field 'B sel ~ bty)
=> AtoB aty bty where
fieldAtoB' = fieldAtoB
and we can define an instance for non- Field
s: 我们可以为非
Field
s定义一个实例:
instance {-# OVERLAPPING #-} AtoB ty ty where
fieldAtoB' = id
Note one limitation here -- if you define a Field
with equal concrete types in different phases, this overlapping instance with fieldAtoB' = id
will be used and fieldAtoB
will be ignored. 请注意这里的一个限制 - 如果在不同阶段定义具有相同具体类型的
Field
,将使用具有fieldAtoB' = id
重叠实例,并且将忽略fieldAtoB
。
Now, for a particular selector Bar
whose underlying types should be BarA
and BarB
in the respective phases, we can define the following IsField
instance: 现在,对于一个特定的选择器
Bar
它的底层类型应该是各个阶段的BarA
和BarB
,我们可以定义以下IsField
实例:
-- Bar field
data BarA = BarA () deriving (Show) -- Field 'A 'Bar
data BarB = BarB () deriving (Show) -- Field 'B 'Bar
instance IsField 'Bar where
type Field 'A 'Bar = BarA -- defines the per-phase field types for 'Bar
type Field 'B 'Bar = BarB
fieldAtoB (BarA ()) = (BarB ()) -- defines the field phase map
We can provide a similar definition for Baz
: 我们可以为
Baz
提供类似的定义:
-- Baz field
data BazA = BazA () deriving (Show)
data BazB = BazB () deriving (Show)
instance IsField 'Baz where
type Field 'A 'Baz = BazA
type Field 'B 'Baz = BazB
fieldAtoB (BazA ()) = (BazB ())
Now, we can define the generic gAtoB
transformation like so: 现在,我们可以像这样定义泛型
gAtoB
转换:
gAtoB :: (Generic (rcrd 'A), Code (rcrd 'A) ~ xssA,
Generic (rcrd 'B), Code (rcrd 'B) ~ xssB,
AllZip2 AtoB xssA xssB)
=> rcrd 'A -> rcrd 'B
gAtoB = to . gAtoBS . from
where
gAtoBS :: (AllZip2 AtoB xssA xssB) => SOP I xssA -> SOP I xssB
gAtoBS (SOP (Z xs)) = SOP (Z (gAtoBP xs))
gAtoBS (SOP (S _)) = error "not implemented"
gAtoBP :: (AllZip AtoB xsA xsB) => NP I xsA -> NP I xsB
gAtoBP Nil = Nil
gAtoBP (I x :* xs) = I (fieldAtoB' x) :* gAtoBP xs
There might be a way to do this with generics-sop
combinators instead of this explicit definition, but I couldn't figure it out. 可能有一种方法可以使用
generics-sop
组合器而不是这个明确的定义,但我无法弄明白。
Anyway, gAtoB
works on Foo
records, as per the definition of foo1
above, but it also works on Quux
records: 无论如何,
gAtoB
上面foo1
的定义, gAtoB
可以处理Foo
记录,但它也适用于Quux
记录:
data Quux p = Quux { bar2 :: Field p 'Bar
, num2 :: Int
} deriving (GHC.Generic)
deriving instance Show (Quux 'A)
deriving instance Show (Quux 'B)
instance Generic (Quux p)
quux0 :: Quux 'A
quux0 = Quux (BarA ()) 2
quux1 :: Quux 'B
quux1 = gAtoB quux0
main :: IO ()
main = do
print foo0
print foo1
print quux0
print quux1
Note that I've used selectors with a Selector
data kind, but you could rewrite this to use selectors of type (a :: Phase -> *)
, as I've done in the example at the end. 请注意,我使用了具有
Selector
数据类型的Selector
,但您可以重写它以使用类型(a :: Phase -> *)
选择器,就像我在最后的示例中所做的那样。
Now, you needed this to happen over the IO
monad. 现在,你需要在
IO
monad上实现这一点。 Here's a modified version that does that: 这是一个修改过的版本:
{-# LANGUAGE DataKinds, DeriveGeneric, FlexibleContexts,
FlexibleInstances, KindSignatures, MultiParamTypeClasses,
StandaloneDeriving, TypeFamilies, TypeFamilyDependencies,
TypeSynonymInstances, UndecidableInstances #-}
{-# OPTIONS_GHC -Wall #-}
import qualified GHC.Generics as GHC
import Generics.SOP
import Control.Applicative
data Phase = A | B
data Selector = Bar | Baz
class IsField (sel :: Selector) where
type Field (p :: Phase) sel = r | r -> sel
fieldAtoB :: Field 'A sel -> IO (Field 'B sel)
data Foo p = Foo { bar :: Field p 'Bar
, baz :: Field p 'Baz
, num :: Int
} deriving (GHC.Generic)
deriving instance Show (Foo 'A)
deriving instance Show (Foo 'B)
instance Generic (Foo p)
foo0 :: Foo 'A
foo0 = Foo (BarA ()) (BazA ()) 1
foo1 :: IO (Foo 'B)
foo1 = gAtoB foo0
-- fieldAtoB :: Field 'A sel -> Field 'B sel
class AtoB aty bty where
fieldAtoB' :: aty -> IO bty
instance (IsField sel, Field 'A sel ~ aty, Field 'B sel ~ bty) => AtoB aty bty where
fieldAtoB' = fieldAtoB
instance {-# OVERLAPPING #-} AtoB ty ty where
fieldAtoB' = return
-- Bar field
data BarA = BarA () deriving (Show) -- Field 'A 'Bar
data BarB = BarB () deriving (Show) -- Field 'B 'Bar
instance IsField 'Bar where -- defines the per-phase field types for 'Bar
type Field 'A 'Bar = BarA
type Field 'B 'Bar = BarB
fieldAtoB (BarA ()) = return (BarB ()) -- defines the field phase map
-- Baz field
data BazA = BazA () deriving (Show)
data BazB = BazB () deriving (Show)
instance IsField 'Baz where
type Field 'A 'Baz = BazA
type Field 'B 'Baz = BazB
fieldAtoB (BazA ()) = return (BazB ())
gAtoB :: (Generic (rcrd 'A), Code (rcrd 'A) ~ xssA,
Generic (rcrd 'B), Code (rcrd 'B) ~ xssB,
AllZip2 AtoB xssA xssB)
=> rcrd 'A -> IO (rcrd 'B)
gAtoB r = to <$> (gAtoBS (from r))
where
gAtoBS :: (AllZip2 AtoB xssA xssB) => SOP I xssA -> IO (SOP I xssB)
gAtoBS (SOP (Z xs)) = SOP . Z <$> gAtoBP xs
gAtoBS (SOP (S _)) = error "not implemented"
gAtoBP :: (AllZip AtoB xsA xsB) => NP I xsA -> IO (NP I xsB)
gAtoBP Nil = return Nil
gAtoBP (I x :* xs) = I <$> fieldAtoB' x <**> pure (:*) <*> gAtoBP xs
data Quux p = Quux { bar2 :: Field p 'Bar
, num2 :: Int
} deriving (GHC.Generic)
deriving instance Show (Quux 'A)
deriving instance Show (Quux 'B)
instance Generic (Quux p)
quux0 :: Quux 'A
quux0 = Quux (BarA ()) 2
quux1 :: IO (Quux 'B)
quux1 = gAtoB quux0
main :: IO ()
main = do
print foo0
foo1val <- foo1
print foo1val
print quux0
quux1val <- quux1
print quux1val
And here's a version rewritten to hew as closely to your original design as possible. 这是一个重写的版本,尽可能与原始设计密切相关。 Again a key limitation is that a
Config
with equal configuration-time and run-time types will use toRunTime' = return
and not any other definition given in its Config
instance. 同样关键的限制是具有相同配置时和运行时类型的
Config
将使用toRunTime' = return
而不是其Config
实例中给出的任何其他定义。
{-# LANGUAGE DataKinds, DeriveGeneric, FlexibleContexts,
FlexibleInstances, KindSignatures, MultiParamTypeClasses,
StandaloneDeriving, TypeFamilies, TypeFamilyDependencies,
TypeSynonymInstances, UndecidableInstances #-}
{-# OPTIONS_GHC -Wall #-}
import qualified GHC.Generics as GHC
import Generics.SOP
import Control.Applicative
data UsagePhase = ConfigTime | RunTime
class Config (sel :: UsagePhase -> *) where
type Phase (p :: UsagePhase) sel = r | r -> sel
toRunTime :: Phase 'ConfigTime sel -> IO (Phase 'RunTime sel)
class ConfigRun cty rty where
toRunTime' :: cty -> IO rty
instance (Config (sel :: UsagePhase -> *),
Phase 'ConfigTime sel ~ cty,
Phase 'RunTime sel ~ rty) => ConfigRun cty rty where
toRunTime' = toRunTime
instance {-# OVERLAPPING #-} ConfigRun ty ty where
toRunTime' = return
-- DatabaseConfig field
data DatabaseConfig (p :: UsagePhase)
data ConnectInfo = ConnectInfo () deriving (Show)
data ConnectionPool = ConnectionPool () deriving (Show)
instance Config DatabaseConfig where
type Phase 'ConfigTime DatabaseConfig = ConnectInfo
type Phase 'RunTime DatabaseConfig = ConnectionPool
toRunTime (ConnectInfo ()) = return (ConnectionPool ())
-- KinesisConfig field
data KinesisConfig (p :: UsagePhase)
data KinesisInfo = KinesisInfo () deriving (Show)
data KinesisStream = KinesisStream () deriving (Show)
instance Config KinesisConfig where
type Phase 'ConfigTime KinesisConfig = KinesisInfo
type Phase 'RunTime KinesisConfig = KinesisStream
toRunTime (KinesisInfo ()) = return (KinesisStream ())
-- CfgMyHostName field
data CfgMyHostName = CfgMyHostName String deriving (Show)
data UiServerConfig (p :: UsagePhase) = CfgUiServerC
{ userDatabase :: Phase p DatabaseConfig
, cmsDatabase :: Phase p DatabaseConfig
, kinesisStream :: Phase p KinesisConfig
, myHostName :: CfgMyHostName
, myPort :: Int
} deriving (GHC.Generic)
deriving instance Show (UiServerConfig 'ConfigTime)
deriving instance Show (UiServerConfig 'RunTime)
instance Generic (UiServerConfig p)
gToRunTime :: (Generic (rcrd 'ConfigTime), Code (rcrd 'ConfigTime) ~ xssA,
Generic (rcrd 'RunTime), Code (rcrd 'RunTime) ~ xssB,
AllZip2 ConfigRun xssA xssB)
=> rcrd 'ConfigTime -> IO (rcrd 'RunTime)
gToRunTime r = to <$> (gToRunTimeS (from r))
where
gToRunTimeS :: (AllZip2 ConfigRun xssA xssB) => SOP I xssA -> IO (SOP I xssB)
gToRunTimeS (SOP (Z xs)) = SOP . Z <$> gToRunTimeP xs
gToRunTimeS (SOP (S _)) = error "not implemented"
gToRunTimeP :: (AllZip ConfigRun xsA xsB) => NP I xsA -> IO (NP I xsB)
gToRunTimeP Nil = return Nil
gToRunTimeP (I x :* xs) = I <$> toRunTime' x <**> pure (:*) <*> gToRunTimeP xs
cfg0 :: UiServerConfig 'ConfigTime
cfg0 = CfgUiServerC (ConnectInfo ()) (ConnectInfo ()) (KinesisInfo())
(CfgMyHostName "localhost") 10
main :: IO ()
main = do
print cfg0
run0 <- gToRunTime cfg0
print run0
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.