Добавление maybeness к типу данных Haskell (с синтаксисом записи) - PullRequest
0 голосов
/ 27 декабря 2018

Глядя на ответ на этот вопрос:

https://stackoverflow.com/a/34164251/1052117

Я вижу, что он определяет тип данных, который используется для анализа объекта JSON.

data Address = Address
    { house  :: Integer
    , street :: String
    , city   :: String
    , state  :: Maybe String
    , zip    :: String -- here I change the original, zip codes are strings, they have leading zeros.
    } deriving (Show, Eq)

$(deriveJSON defaultOptions ''Address)

Это полезно, но мне интересно: как я могу изменить тип данных Address, чтобы все поля json могли обнуляться?В частности, я вижу поле «Возможно» перед состоянием, но я представляю большую структуру данных, в которой было бы утомительно изменять все поля на поля «Возможно».Например, хотя я / мог / переписал выше как:

data Address = Address
    { house  :: Maybe Integer
    , street :: Maybe String
    , city   :: Maybe String
    , state  :: Maybe String
    , zip    :: Maybe String
    } deriving (Show, Eq)

Какую функцию я мог бы применить к типу данных Address / в коде /, чтобы достичь того же результата, не переписывая весь код и не вставляя вручнуюMaybes?

Ответы [ 2 ]

0 голосов
/ 29 декабря 2018

достичь того же результата, не переписывая весь код и не вставляя Maybes вручную

Чтобы избежать навязчивых изменений в типе записи, мы можем работать с другим типом, производным от которого является от рекордного, анализируя его структуру, что требует относительно продвинутого общего программирования и программирования на уровне типов.В этом ответе используется пакет generics-sop .

Некоторые требуемые прагмы и импорт:

{-# LANGUAGE DataKinds, TypeFamilies, FlexibleInstances, UndecidableInstances, 
             ScopedTypeVariables, TypeApplications, TypeOperators, 
             DeriveGeneric, StandaloneDeriving, MultiParamTypeClasses,
             FunctionalDependencies, AllowAmbiguousTypes, FlexibleContexts #-}
import           Data.Kind (Type)
import           Data.Type.Equality (type (==))
import           GHC.TypeLits
import qualified GHC.Generics as GHC
import           Generics.SOP -- from package "generics-sop"
import qualified Generics.SOP.Type.Metadata as M

Этот новый тип представляет n-арный продукт значений полей, полученных из записи, каждое из которых заключено в функтор f.Список уровня типа ns поля Имена хранится в виде переменной типа фантома :

newtype Wrapped f (ns :: [Symbol]) (xs :: [Type]) = Wrapped { unwrap :: NP f xs }

deriving instance All (Generics.SOP.Compose Show f) xs => Show (Wrapped f ns xs)

type family FieldNamesOf (a :: M.DatatypeInfo) :: [Symbol] where
    FieldNamesOf ('M.ADT moduleName datatypeName '[ 'M.Record constructorName fields ]) = 
        ExtractFieldNames fields

type family ExtractFieldNames (a :: [M.FieldInfo]) :: [Symbol] where
    ExtractFieldNames '[] = '[]
    ExtractFieldNames (('M.FieldInfo n) ': xs) = n ': ExtractFieldNames xs

fromRecord :: forall r ns xs.  (IsProductType r xs, 
                                HasDatatypeInfo r, 
                                FieldNamesOf (DatatypeInfoOf r) ~ ns)
           => r 
           -> Wrapped I ns xs 
fromRecord r = let (SOP (Z np)) = from r in Wrapped np

toRecord :: forall r ns xs.  (IsProductType r xs, 
                              HasDatatypeInfo r, 
                              FieldNamesOf (DatatypeInfoOf r) ~ ns)
         => Wrapped I ns xs 
         -> r
toRecord (Wrapped np) = to (SOP (Z np))

Если нам не нужно сохранять полеВо всем мире новый тип становится излишним, и лучше работать напрямую с n-арным продуктом NP , манипулируя им с помощью богатого набора функций , предоставляемых generics-sop.

Но если мы хотим сохранить возможность выбора полей по имени, то нам нужно определить функцию для нового типа, поддерживаемую парой классов типов:

getWrappedField :: forall n f ns xs x. HasField ns n xs x => Wrapped f ns xs -> f x
getWrappedField (Wrapped np) = getHasField @ns @n np  

class HasField (ns :: [Symbol]) (n :: Symbol) 
               (xs :: [Type])   (x :: Type)   | ns n xs -> x where 
    getHasField :: NP f xs -> f x 

instance ((e == n) ~ flag, HasField' flag (e : ns) n xs x) => HasField (e : ns) n xs x where
    getHasField = getHasField' @flag @(e : ns) @n

class HasField' (flag :: Bool) 
                (ns :: [Symbol]) (n :: Symbol) 
                (xs :: [Type]) (x :: Type)     | ns n xs -> x where 
    getHasField' :: NP f xs -> f x 

instance HasField' True (n : ns) n (x : xs) x where
    getHasField' (v :* _) = v

instance HasField ns n xs x => HasField' False (nz : ns) n (xz : xs) x where
    getHasField' (_ :* rest) = getHasField @ns @n rest

Учитывая эту запись примера, котораяполучает необходимый с поддержкой классов типов :

data Person = Person { name :: String, age :: Int } deriving (Show, GHC.Generic)
instance Generic Person
instance HasDatatypeInfo Person

Мы можем построить его обобщенное представление (где все поля изначально заключены в тождествоfunctor I ), а затем получить одно из полей, например:

ghci> getWrappedField @"age" (fromRecord (Person "Jimmy" 25))
I 25

Имя поля передается в виде уровня типа Symbol, используя тип применение .

0 голосов
/ 27 декабря 2018

Как обсуждалось в комментариях, использование функтора-функтора сработало бы с очень небольшими изменениями исходного типа данных.

Если вы начинаете с

data Address = Address
    { house  :: Integer
    , street :: String
    , city   :: String
    , state  :: Maybe String
    , zip    :: String
    } deriving (Show, Eq)

, то это эквивалентно

import Data.Functor.Identity

data AddressF f = Address
  { house  :: f Integer 
  , street :: f String
  , city   :: f String
  , state  :: Maybe String
  , zip    :: f String 
  } deriving (Show, Eq)

type Address = AddressF Identity

, а затем вы можете получить второй, написав

type Address' = AddressF Maybe

Чтобы вернуться к исходному определению, вы можете написать

toOriginal (AddressF (Identity house) (Identity street) (Identity city) mbState (Identity zip)) = Address house street city mbState zip
...