元组和记录之间的自动转换

7
记录或简单的 ADT 在 Haskell 中与 boxed 元组基本上是等价的。是否有一种方法(理想情况下是一些花哨的扩展或来自 haksell 平台的库),可以在这种类型和元组之间进行转换?
我对 Haskell 还比较陌生,我正在尝试用 Haskell 构建一些报告工具。这涉及到读取/写入 CSV 文件和数据库表格。使用元组的东西相当直接,但使用普通类时涉及到一些样板文件。 在两种方式中,样板似乎几乎相同,但我没有找到一种好的方法只做一次转换(数据 <-> 元组),并使用来自元组到 CSV/表格的本地转换。
更新
目前我收到的所有答案都认为我需要完全通用的东西并且我想要元组。 我不想要元组,我< strong>已经有元组并且我不想要它们,因此需要转换它们。 实际上,我只想将样板减少(减少到 0 :-)),但不一定需要函数名称对每种类型都相同。
例如,我可以通过取消某个构造函数的 currying 轻松将元组转换为任何内容。 问题是我需要 uncurryN,但我找不到任何地方(除了一个模板 haskell 教程)。 反转更难做到。
我不是在寻求解决方案(虽然我收到的所有答案都很棒,因为我不熟悉 Haskell 中的不同元编程方式),但更多的是,如果轮子已经存在(例如这个 uncurryN,可以手动编写 20 次并打包到漂亮的包中),那么我就不喜欢重新发明轮子。
更新2
显然有一个 uncurry 包,但它仍然只解决了一半的问题。

你可能想尝试一下SYB - luqui
我希望避免使用模板哈斯凯尔。SYB 没有使用它吗? - mb14
不,SYB 不使用 TH,你只需要 deriving (Data) - luqui
5个回答

8

您可能想查看GHC.Generics。它基本上将每个ADT编码为乘积((,))和总和(Either)。例如,这是如何使用泛型显示此表示形式的示例:

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

class Tuple p where
  showRepresentation :: p -> String

  default showRepresentation :: (Generic p, GTuple (Rep p)) => p -> String
  showRepresentation = gshowRepresentation . from

class GTuple p where
  gshowRepresentation :: p x -> String

instance Tuple k => GTuple (K1 i k) where
  gshowRepresentation (K1 t) = showRepresentation t

instance GTuple f => GTuple (M1 i c f) where
  gshowRepresentation (M1 f) = gshowRepresentation f

instance (GTuple f, GTuple g) => GTuple (f :*: g) where
  gshowRepresentation (f :*: g) = gshowRepresentation f ++ " * " ++ gshowRepresentation g

-- Some instances for the "primitive" types
instance Tuple Int where showRepresentation = show
instance Tuple Bool where showRepresentation = show
instance Tuple () where showRepresentation = show

--------------------------------------------------------------------------------
data Example = Example Int () Bool deriving Generic
instance Tuple Example

main :: IO ()
main = putStrLn $ showRepresentation $ Example 3 () False
-- prints: 3 * () * False

您可以在GHC.Generics模块中找到更多文档。同时,我发现这篇论文《A Generic Deriving Mechanism for Haskell》也很容易理解(这是我读过的少数几篇论文之一)。


4
lens库中的Control.Lens.IsoControl.Lens.Wrapped模块提供了一些实用工具,使得处理此类转换更加容易。不幸的是,目前针对这种情况的Template Haskell机制仅处理新类型而非记录,因此您需要自己定义实例。例如:
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}

import Control.Lens

data Foo = Foo { baz :: Int, bar :: Int } deriving Show

instance Wrapped Foo where
  type Unwrapped Foo = (Int,Int)
  _Wrapped' = iso (\(Foo baz' bar') -> (baz',bar')) (\(baz',bar') -> Foo baz' bar')

现在我们可以轻松地进行包装和解包:
*Main> (2,3) ^. _Unwrapped' :: Foo
Foo {baz = 2, bar = 3}

*Main> Foo 2 3 ^. _Wrapped'
(2,3)

我们还可以使用一个函数来修改元组中的 Foo
*Main> over _Wrapped' (\(x,y)->(succ x,succ y))  $ Foo 2 5
Foo {baz = 3, bar = 6}

反之:

*Main> under _Wrapped' (\(Foo x y)->(Foo (succ x) (succ y)))  $ (2,5)
(3,6)

我很喜欢这个。不过,你是在暗示Haskell模板支持它用于newtype吗?那会是什么样的呢? - mb14
如果你只有一个 newtype 而不是一个记录,你可以使用 $(makeWrapped ''NameOfTheNewtype) 来自动生成 Wrapped 实例,使用 TH。它也可以用于记录,但目前似乎还没有实现。 - danidiaz
2
makePrisms 用于单构造器类型,实际上生成了我们正在寻找的 Iso - FunctorSalad

1
如果您想要真正的n元组(而不仅仅是一些语义上等价的其他数据),没有模板Haskell将会很麻烦。
例如,如果您想要转换:
data Foo = Foo Int String Int
data Bar = Bar String String Int Int

转换为

type FooTuple = (Int, String, Int)
type BarTuple = (String, String, Int, Int)

由于数据类型的字段不同,GHC.GenericsSYB都会存在问题,因为结果类型需要有所不同。即使两者都被称为“元组”,(Int,String,Int)(String,String,Int,Int)是完全独立的类型,并且没有方便的方法以通用方式处理n元组。以下是一种使用GHC.Generics实现上述目标的方法:

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE DeriveGeneric #-}

-- Generic instance to turn generic g x into some n-tuple whose exact
-- type depends on g.
class GTuple g where
    type NTuple g

    gtoTuple :: g x -> NTuple g

-- Unwarp generic metadata
instance GTuple f => GTuple (M1 i c f) where
    type NTuple (M1 i c f) = NTuple f

    gtoTuple = gtoTuple . unM1

-- Turn individual fields into a Single type which we need to build up
-- the final tuples.
newtype Single x = Single x

instance GTuple (K1 i k) where
    type NTuple (K1 i k) = Single k

    gtoTuple (K1 x) = Single x

-- To combine multiple fields, we need a new Combine type-class.
-- It can take singular elements or tuples and combine them into
-- a larger tuple.
--
class Combine a b where
    type Combination a b
    combine :: a -> b -> Combination a b

-- It's not very convenient because it needs a lot of instances for different
-- combinations of things we can combine.

instance Combine (Single a) (Single b) where
    type Combination (Single a) (Single b) = (a, b)
    combine (Single a) (Single b) = (a, b)

instance Combine (Single a) (b, c) where
    type Combination (Single a) (b, c) = (a, b, c)
    combine (Single a) (b, c) = (a, b, c)

instance Combine (a,b) (c,d) where
    type Combination (a,b) (c,d) = (a,b,c,d)
    combine (a,b) (c,d) = (a,b,c,d)

-- Now we can write the generic instance for constructors with multiple
-- fields.

instance (Combine (NTuple a) (NTuple b), GTuple a, GTuple b) => GTuple (a :*: b) where
    type NTuple (a :*: b) = Combination (NTuple a) (NTuple b)

    gtoTuple (a :*: b) = combine (gtoTuple a) (gtoTuple b)


-- And finally the main function that triggers the tuple conversion.
toTuple :: (Generic a, GTuple (Rep a)) => a -> NTuple (Rep a)
toTuple = gtoTuple . from

-- Now we can test that our instances work like they should:
data Foo = Foo Int String Int deriving (Generic)
data Bar = Bar String String Int Int deriving (Generic)

fooTuple = toTuple $ Foo 1 "foo" 2
barTuple = toTuple $ Bar "bar" "asdf" 3 4

上面的方法是可行的,但需要大量工作(我无法快速弄清楚是否可以在不使用UndecidableInstances的情况下完成)。
现在,你真正想做的可能只是跳过元组,直接使用泛型转换为CSV。我假设你正在使用csv-conduit,并希望生成ToRecord类型类的实例。
以下是一个示例:
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DeriveGeneric #-}

import GHC.Generics
import Data.ByteString (ByteString)
import Data.CSV.Conduit.Conversion

class GRecord g where
    gToRecord :: g x -> [ByteString]

instance GRecord f => GRecord (M1 i c f) where
    gToRecord = gToRecord . unM1

instance ToField k => GRecord (K1 i k) where
    gToRecord (K1 x) = [toField x]

instance (GRecord a, GRecord b) => GRecord (a :*: b) where
    gToRecord (a :*: b) = gToRecord a ++ gToRecord b

genericToRecord :: (Generic a, GRecord (Rep a)) => a -> Record
genericToRecord = record . gToRecord . from

现在,您可以轻松地为自定义类型创建实例。
data Foo = Foo Int String Int deriving (Generic)
data Bar = Bar String String Int Int deriving (Generic)

instance ToRecord Foo where
    toRecord = genericToRecord

instance ToRecord Bar where
    toRecord = genericToRecord

作为对您更新问题的回应:您可能会对tuple包(特别是Curry)感兴趣,该包含了元组的uncurryNcurryN的实现,适用于最多15个元素的元组。

如果我使用simple-mysql-quasi,就不能跳过元组。尽管如我的更新中所指定的那样,我不一定需要一个类,可能可以用GTuple2、GTuple3等来解决。 - mb14
添加了一个链接到 tuple 包,你可能会发现它很有用。 - shang

0

generic-lens 中的 _Ctor 函数可以将任何记录值转换为元组,反之亦然,而无需使用模板哈斯克尔。


0
在一些情况下,你可以使用unsafeCoerce。函数的名称应该是非常明显的警告,需要非常小心。特别是行为取决于编译器甚至编译器版本。
data Bar = Bar Text Text

tupleToBar :: (Text, Text) -> Bar
tupleToBar = unsafeCoerce


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接