简体   繁体   English

如何派生涉及类型族的泛型遍历

[英]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. 这是一个似乎有效的解决方案。

Generic Phase Mapping without a Monad 没有Monad的通用相位映射

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它的底层类型应该是各个阶段的BarABarB ,我们可以定义以下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 -> *)选择器,就像我在最后的示例中所做的那样。

Generic Phase Traversal over a Monad Monad上的通用阶段遍历

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

Adapted to Your Problem 适应您的问题

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM