Как элегантно избегать «Переменной типа неоднозначности» при использовании классов типов Haskell - PullRequest
0 голосов
/ 19 ноября 2018

Я хочу написать простую структуру, которая работает с существующими объектами.Идея состоит в том, чтобы иметь класс типа Entity и предоставлять общие операции сохранения, такие как

storeEntity    :: (Entity a) => a -> IO () 
retrieveEntity :: (Entity a) => Integer -> IO a
publishEntity  :: (Entity a) => a -> IO () 

Фактические типы данных являются экземпляром этого класса типа Entity.

Даже если операции сохранения являются общими иВам не нужна информация о конкретных типах данных. Вы должны предоставить аннотацию типа на сайте вызова, чтобы GHC был доволен, например:

main = do
    let user1 = User 1 "Thomas" "Meier" "tm@meier.com"
    storeEntity user1
    user2 <- retrieveEntity 1 :: IO User -- how to avoid this type annotation?
    publishEntity user2

Есть ли способ избежать такого типа сайта вызовааннотации?

Я знаю, что эти аннотации мне не нужны, если компилятор может определить фактический тип из контекста использования.Так, например, следующий код работает нормально:

main = do
    let user1 = User 1 "Thomas" "Meier" "tm@meier.com"
    storeEntity user1
    user2 <- retrieveEntity 1
    if user1 == user2
        then publishEntity user2
        else fail "retrieve of data failed"

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

main = do
    let user1 = User 1 "Heinz" "Meier" "hm@meier.com"
    storeEntity user1
    -- unfortunately the next line does not compile
    retrieveEntity 1 >>= publishEntity

    -- but with a type annotation it works:
    (retrieveEntity 1 :: IO User) >>= publishEntity

Но наличие аннотации типа здесь нарушаетэлегантность полиморфизма ...

Для полноты картины я включил полный исходный код:

{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
module Example where
import GHC.Generics
import Data.Aeson

-- | Entity type class
class (ToJSON e, FromJSON e, Eq e, Show e) => Entity e where 
    getId :: e -> Integer

-- | a user entity    
data User = User {
      userId    :: Integer
    , firstName :: String
    , lastName  :: String
    , email     :: String
} deriving (Show, Eq, Generic, ToJSON, FromJSON)

instance Entity User where
    getId = userId 


-- | load persistent entity of type a and identified by id
retrieveEntity :: (Entity a) => Integer -> IO a
retrieveEntity id = do
    -- compute file path based on id
    let jsonFileName = getPath id
    -- parse entity from JSON file
    eitherEntity <- eitherDecodeFileStrict jsonFileName
    case eitherEntity of
        Left msg -> fail msg
        Right e  -> return e

-- | store persistent entity of type a to a json file
storeEntity :: (Entity a) => a -> IO ()
storeEntity entity = do
    -- compute file path based on entity id
    let jsonFileName = getPath (getId entity)
    -- serialize entity as JSON and write to file
    encodeFile jsonFileName entity

-- | compute path of data file based on id
getPath :: Integer -> String
getPath id = ".stack-work/" ++ show id ++ ".json"

publishEntity :: (Entity a) => a -> IO ()   
publishEntity = print

main = do
    let user1 = User 1 "Thomas" "Meier" "tm@meier.com"
    storeEntity user1
    user2 <- retrieveEntity 1 :: IO User
    print user2

1 Ответ

0 голосов
/ 20 ноября 2018

Вы можете связать типы storeEntity и retrieveEntity, добавив тег уровня типа к идентификатору вашей организации Integer. Я думаю, что ваш дизайн API также имеет небольшую погрешность, которая не критична, но я все равно исправлю ее. А именно: User s не должны хранить свой идентификатор. Вместо этого имейте единственную обертку типа верхнего уровня для идентифицированных вещей. Это позволяет вам написать код раз и навсегда для идентификаторов, например: функция, которая принимает сущность, у которой еще нет идентификатора (как бы вы представляли ее с помощью определения User?), и выделяет для нее новый идентификатор - не возвращаясь и не изменяя свой класс Entity и все его реализации. Также отдельно хранить имя и фамилию неправильно . Итак:

import Data.Tagged

data User = User
    { name :: String
    , email :: String
    } deriving (Eq, Ord, Read, Show)

type Identifier a = Tagged a Integer
data Identified a = Identified
    { ident :: Identifier a
    , val :: a
    } deriving (Eq, Ord, Read, Show)

Здесь мой Identified User соответствует вашему User, а мой User не имеет аналога в вашей версии. Класс Entity может выглядеть так:

class Entity a where
    store :: Identified a -> IO ()
    retrieve :: Identifier a -> IO a
    publish :: a -> IO () -- or maybe Identified a -> IO ()?

instance Entity User -- stub

В качестве примера приведенного выше принципа «напиши раз и навсегда» вы можете найти удобным для retrieve фактическое связывание возвращаемой сущности с ее идентификатором. Теперь это можно сделать единообразно для всех сущностей:

retrieveIDd :: Entity a => Identifier a -> IO (Identified a)
retrieveIDd id = Identified id <$> retrieve id

Теперь мы можем написать действие, связывающее вместе типы его хранилища и извлечения действий:

storeRetrievePublish :: Entity a => Identified a -> IO ()
storeRetrievePublish e = do
    store e
    e' <- retrieve (ident e)
    publish e'

Здесь ident e имеет достаточно богатую информацию о типе, поэтому мы знаем, что e' должно быть a, даже если у нас нет явной сигнатуры типа для него. (Подпись на storeRetrievePublish также необязательна; здесь указана подпись GHC.) Последние штрихи:

main :: IO ()
main = storeRetrievePublish (Identified 1 (User "Thomas Meier" "tm@meier.com"))

Если вы не хотите явно определять storeRetrievePublish, вы можете обойтись следующим:

main :: IO ()
main = do
    let user = Identified 1 (User "Thomas Meier" "tm@meier.com")
    store user
    user' <- retrieve (ident user)
    publish user'

... но вы не можете развернуть определения гораздо дальше: если вы уменьшите ident user до 1, вы потеряете связь между тегом type идентификатора, используемого для store и для retrieve и вернитесь к неоднозначной ситуации.

...