简单ADT到数组的通用转换

时间:2019-02-17 23:23:02

标签: haskell

给出一个求和类型:

type PlayerId = String
data Location = Deck | Hand PlayerId

我该如何编写这两个函数(我不在乎采用哪种通用方法……可以帮助我找出更合适的奖励积分):

myF :: Generic a => a -> [String]
myF :: Data a => a -> [String]

-- roughly equivalent to
myF :: Location -> [String]
myF x = case x of
          Deck     -> ["deck"]
          Hand pid -> ["hand", show pid]

(对于任何“无效”类型,例如,该参数不可Show,请返回[]error。)

上下文:我想为它们通用地定义Data.Aeson.ToJSON实例,但有许多类似的枚举类型,尽管上面给出了myF我知道如何做其余的事情。尽管大多数情况下,我只是这样做是为了学习有关通用编程的更多信息。

尝试:

使用Generic

λ> unM1 $ from Deck
(L1 (M1 {unM1 = U1}))

λ> :t (undefined :: Rep Location p)
(undefined :: Rep Location p)
  :: D1
       ('MetaData "Location" "Test" "main" 'False)
       (
         C1 ('MetaCons "Deck" 'PrefixI 'False) U1
         :+:
         C1
           ('MetaCons "Hand" 'PrefixI 'False)
           (S1
              ('MetaSel
                 'Nothing 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy)
              (Rec0 String)))

由于:+:被定义为L1 | R1,因此我可能可以“合并”以上两个结果。我不确定我会这样做的一种好方法..也许在前者上进行模式匹配,并使用它“降级”到后者-但我不确定如何在类型定义和真实代码。

使用Data

AFAICT Data是泛型的另一种方法。您可以使用 Generic Data,对吗?

我认为我需要使用gmap*函数之一,但是我不知道如何将类型与我的问题联系起来。尝试了一些探索性的“将随机参数插入各种方法”,但没有发现任何有趣的地方。

更新!我试图简化我的示例,但是我做得太多了。在我的实际代码中,PlayerId是围绕字符串的新类型。在这种情况下,以下“有效”(以小写形式表示构造函数名称):

mkQ :: (Typeable a, Typeable b) => r -> (b -> r) -> a -> r
(r `mkQ` q) a = case cast a of
                  Just b -> q b
                  Nothing -> r

myF :: Data a => a -> [String]
myF input =
  [showConstr . toConstr $ input]
  ++ gmapQ (\x -> ("" `mkQ` f) x) input

f :: PlayerId -> String
f (PlayerId x) = x

这里的见解是构造函数和参数需要区别对待。另一个问题是上述代码需要了解PlayerId。以下内容无效:

f :: Show a => a -> String
f = show

...,因为它与gmapQ的类型签名不匹配。我想我理解为什么会这样:gmapQ的工作方式是使用cast,而f的定义还不够具体,无法为其提供实际的类型。我不确定是否可以解决此问题,或者是否限制使用Data。 (不过,即使不是理想情况,这仍然可能是可行的:我可以想象这样一种情况:我有myF由某些fs参数化了,这些参数特定于类型中的特定参数。)

感觉也不对,因为我从original SYB paper复制了mkQ函数...我以为我应该能够使用Data.Data提供的函数来做到这一点。

2 个答案:

答案 0 :(得分:2)

使用泛型时,您不需要合并两种类型的信息。您所需要做的就是通过实例处理每种可能的类型。

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE TypeOperators #-}
import GHC.Generics

type PlayerId = String
data Location = Deck | Hand PlayerId deriving Generic

instance MyClass Location

class MyClass a where
  myF :: a -> [String]
  default myF :: (Generic a, MyClass1 (Rep a)) => a -> [String]
  myF = defaultMyF

defaultMyF :: (Generic a, MyClass1 (Rep a)) => a -> [String]
defaultMyF a = myF1 $ from a

Rep a具有种类* -> *,所以我们不能直接为MyClassU1V1等实现M1。相反,我们需要另一个类其中myF的类型为:: a b -> [String]

class MyClass1 a where
  myF1 :: a b -> [String]

instance MyClass1 V1 where
  myF1 _ = []

instance MyClass1 U1 where
  myF1 _ = []

instance MyClass1 (K1 i String) where
  myF1 (K1 a) = [a]

instance (MyClass1 f, Constructor t) => MyClass1 (C1 t f) where
  myF1 c@(M1 a) = (conName c) : myF1 a

instance (MyClass1 f) => MyClass1 (D1 t f) where
  myF1 (M1 a) = myF1 a


instance (MyClass1 f) => MyClass1 (S1 t f) where
  myF1 (M1 a) = myF1 a

instance (MyClass1 a, MyClass1 b) => MyClass1 (a :+: b) where
  myF1 (L1 x) = myF1 x
  myF1 (R1 x) = myF1 x

instance (MyClass1 a, MyClass1 b) => MyClass1 (a :*: b) where
  myF1 (a :*: b) = myF1 a ++ myF1 b

现在您可以对其进行测试:

main :: IO ()
main = do
  putStrLn $ show $ myF Deck
  putStrLn $ show $ myF $ Hand "1234"

答案 1 :(得分:2)

这是使用generics-sop的解决方案。

{-# LANGUAGE DeriveAnyClass, DeriveGeneric, FlexibleContexts, ScopedTypeVariables, TypeApplications #-}

import Data.Char
import Generics.SOP
import qualified GHC.Generics as GHC

type PlayerId = String
data Location = Deck | Hand PlayerId
  deriving (GHC.Generic, Generic, HasDatatypeInfo)

该库使用自己的通用表示形式,该通用表示形式可以从GHC Generic类自动导出,也可以通过Template Haskell导出。我们使用前一种方法,这意味着我们必须通过GHC.Generic扩展来导出DeriveGeneric,然后通过Generic扩展来获取SOP的HasDatatypeInfoDeriveAnyClass类。 / p>

我们现在分两步进行。第一个只是获取值的构造函数的名称为小写字符串(因为这是您在示例中使用的名称)。此函数的变体确实应该在库中,但是不幸的是,它不是,所以我们必须自己定义它:

lcConstructor :: forall a . (Generic a, HasDatatypeInfo a) => a -> String
lcConstructor x =
  hcollapse
    (hzipWith
      (\ c _ -> K (map toLower (constructorName c)))
      (constructorInfo (datatypeInfo (Proxy @a)))
      (unSOP (from x))
    )

本质上,constructorInfo (datatypeInfo (Proxy @a))构造一个表,其中包含类型a的所有构造函数信息。然后,对hzipWith的调用从表中选择正确的组件(与所讨论的值x对应的组件)。此外,我们从构造函数信息中提取名称,并将其转换为小写字符。

我们可以测试这部分:

GHCi> lcConstructor Deck
"deck"
GHCi> lcConstructor (Hand "42")
"hand"

剩下的工作是获取所有构造函数参数的字符串表示形式,并将它们附加到构造函数名称之后:

myF :: (Generic a, HasDatatypeInfo a, All2 Show (Code a)) => a -> [String]
myF a =
  (lcConstructor a :) . hcollapse . hcmap (Proxy @Show) (mapIK show) . from $ a

在这里,from将值转换为其表示形式,然后hcmap使用show将构造函数的所有参数转换为字符串,然后hcollapse将结果提取为字符串列表,并在(lcConstructor :)前面加上构造函数的名称。

GHCi> myF Deck
["deck"]
GHCi> myF (Hand "42")
["hand", "\"42\""]