Автоматическое преобразование между кортежами и записью

Запись или простой ADT в haskell в значительной степени эквивалентны кортежам в упаковке. Есть ли способ (в идеале какие-нибудь причудливые расширения или библиотека с платформы haksell), который позволяет выполнять преобразование между таким типом и кортежами?

Я (довольно) новичок в Haskell, и я пытаюсь создать инструмент отчетности в Haskell. Это включает в себя чтение/запись CSV-файлов и таблиц базы данных. С кортежами все довольно просто, но при использовании простого класса требуется немного шаблонного шаблона. Швы шаблона почти идентичны в обоих случаях, но я не нашел хорошего способа сделать это только один раз, за ​​исключением, возможно, выполнения преобразования (данные ‹-> кортеж) и использования собственного преобразования из кортежа в CSV/таблица.

Обновлять

Весь ответ, который я получил до сих пор, предполагает, что мне нужно что-то совершенно общее, и я хочу кортеж. Мне не нужен кортеж, у меня есть кортеж, и мне они не нужны, поэтому их нужно конвертировать. На самом деле я просто хочу уменьшить котельную пластину (до 0 :-)), но мне не обязательно, чтобы функции имели одинаковое имя для всех типов.

Например, я могу легко преобразовать кортеж во что угодно, отменив каррирование одного из его конструкторов. Проблема в том, что мне нужен uncurryN, который я нигде не могу найти (кроме учебника по шаблону haskell). Обратное сделать сложнее.

Я не прошу решения (хотя все ответы, которые я получил, великолепны, потому что я не знаком с другим способом метапрограммирования в Haskell), а скорее, потому что я не люблю изобретать велосипед, если колесо уже существовало (например этот uncurryN, мог быть написан вручную до 20 и упакован в красивую упаковку)

Обновлено2

По-видимому, пакет uncurry существует, но он решает половину проблемы.


person mb14    schedule 27.04.2014    source источник
comment
Вы можете попробовать SYB.   -  person luqui    schedule 27.04.2014
comment
Я хотел бы избежать использования шаблона haskell. Разве SYB не использует его?   -  person mb14    schedule 27.04.2014
comment
Нет, SYB не использует TH, все, что вам нужно, это deriving (Data)   -  person luqui    schedule 27.04.2014


Ответы (4)


Вы можете посмотреть на GHC.Generics. В основном он кодирует каждый АТД как произведение ((,)) и сумму (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, вполне читабельным (это был один из несколько статей, которые я читал).

person bennofs    schedule 27.04.2014

Библиотека lens в модулях Control.Lens.Iso и Control.Lens.Wrapped содержит несколько утилит, упрощающих работу с такими преобразованиями. К сожалению, на данный момент шаблон Haskell machinery для таких случаев не обрабатывает записи, а только новые типы, поэтому вам придется определять экземпляры самостоятельно. Например:

{-# 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)
person danidiaz    schedule 27.04.2014
comment
Мне это очень нравится. Однако вы предлагаете, чтобы шаблон Haskell поддерживал его для нового типа? Что бы это было ? - person mb14; 27.04.2014
comment
Если у вас просто newtype вместо записи, вы можете использовать $(makeWrapped ''NameOfTheNewtype) для автоматического создания экземпляра Wrapped с помощью TH. Это могло бы работать и для записей, но, похоже, на данный момент это не реализовано. - person danidiaz; 27.04.2014
comment
makePrisms для типа с одним конструктором фактически генерирует искомое Iso. - person FunctorSalad; 28.04.2014

Если вам нужны настоящие n-кортежи (а не просто какие-то другие данные, которые семантически эквивалентны), это будет громоздко без Template 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.Generics, так и SYB будут проблематичными, потому что тип результата должен отличаться в зависимости от полей типа данных. Несмотря на то, что оба они называются «кортежами», (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), который содержит реализации для uncurryN и curryN для кортежи до 15 элементов.

person shang    schedule 28.04.2014
comment
Я не могу пропустить кортеж, если, например, использую simple-mysql-quasi. Хотя, как указано в моем обновлении, мне не нужен обязательно один класс, я, вероятно, мог бы обойтись с GTuple2, GTuple3... и т.д. - person mb14; 28.04.2014
comment
Добавлена ​​ссылка на пакет tuple, который может быть вам полезен. - person shang; 28.04.2014

В некоторых случаях вы можете использовать unsafeCoerce. . Название функции должно быть довольно четким предупреждением, чтобы быть очень осторожным. В частности, поведение зависит от компилятора и даже версии компилятора.

data Bar = Bar Text Text

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

person Joshua Chia    schedule 27.08.2020