Подтвердить что ты не робот

Моделирование данных домена в Haskell

Я работаю над созданием более крупного веб-приложения с использованием Haskell. Это чисто для моего образования и интереса.

Я начинаю писать свои объекты домена/значения. Одним из примеров является Пользователь. Вот что я до сих пор придумал

module Model (User) where

class Audited a where
    creationDate :: a -> Integer
    lastUpdatedDate :: a -> Integer
    creationUser :: a -> User
    lastUpdatedUser :: a -> User

class Identified a where
    id :: a -> Integer

data User = User { userId :: Integer
                 , userEmail :: String
                 , userCreationDate :: Integer
                 , userLastUpdatedDate :: Integer
                 , userCreationUser :: User
                 , userLastUpdatedUser :: User
                 }

instance Identified User where
    id u = userId u 

instance Audited User where
    creationDate u = userCreationDate
    lastUpdatedDate u = userLastUpdatedDate
    creationUser u = userCreationUser
    lastUpdatedUser u = userLastUpdatedUser

В моем приложении будет примерно 20 типов, подобных описанному выше. Когда я говорю "как вышеупомянутый тип", я имею в виду, что они будут иметь идентификатор, аудиторскую информацию и некоторую информацию о типе (например, электронную почту в случае с пользователем).

То, что я не могу обойти, - это тот факт, что каждое из моих полей (например, User.userEmail) создает новую функцию fieldName :: Type -> FieldType. С 20 различными типами пространство имен кажется, что оно будет довольно полным довольно быстро. Кроме того, мне не нравится называть поле User ID userId. Я бы назвал его id. Есть ли способ обойти это?

Может быть, я должен упомянуть, что я родом из императивного мира, поэтому этот материал FP является довольно новым (но довольно захватывающим) для меня.

4b9b3361

Ответ 1

Да, пространство имен может быть болезненным в Haskell. Я обычно в конечном итоге затягиваю свои абстракции, пока не так много имен. Это также позволяет больше повторного использования. Для вашего, я бы сделал тип данных, а не класс для информации аудита:

data Audit = Audit {
    creationDate :: Integer,
    lastUpdatedDate :: Integer,
    creationUser :: User,
    lastUpdatedUser :: User
}

И затем сопоставьте это с данными типа:

data User = User { 
    userAudit :: Audit,
    userId :: Integer,
    userEmail :: String
}

Вы можете использовать эти классы, если хотите:

class Audited a where
    audit :: a -> Audit

class Identified a where
    ident :: a -> Integer

Однако, по мере развития вашего дизайна, будьте открыты для возможности растворения этих типов стекол в воздухе. Объектноподобные классы типов - те, в которых каждый метод принимает один параметр типа a, имеют способ упрощения.

Другой способ приблизиться к этому может заключаться в классификации ваших объектов с параметрическим типом:

data Object a = Object {
    objId    :: Integer,
    objAudit :: Audit,
    objData  :: a
}

Проверьте, Object - Functor!

instance Functor Object where
    fmap f (Object id audit dta) = Object id audit (f dta)

Я был бы более склонен делать это таким образом, основываясь на моей дизайнерской догадки. Трудно сказать, какой путь лучше, не зная больше о ваших планах. И посмотрите, необходимость в том, чтобы эти стили распадались.: -)

Ответ 2

Это известная проблема с записями Haskell. Были высказаны некоторые предложения (особенно TDNR), чтобы смягчить эффекты, но решения пока не появились.

Если вы не возражаете, чтобы каждый из ваших объектов данных находился в отдельном модуле, вы можете использовать пространства имен для различения функций:

import qualified Model.User as U
import qualified Model.Privileges as P

someUserId user = U.id user
somePrivId priv = P.id priv

Что касается использования id вместо userId; возможно, если вы по умолчанию скрываете id, который импортируется из Prelude. В качестве первого оператора импорта используйте следующее:

import Prelude hiding (id)

и теперь обычная функция id не будет доступна. Если вам это нужно по какой-либо причине, вы можете получить к нему доступ с полным именем, т.е. Prelude.id.

Подумайте, прежде чем создавать имя, которое сталкивается с Prelude. Это часто может ввести в заблуждение программиста, и с ним немного неудобно работать. Возможно, вам лучше использовать короткое, общее имя, например oId.

Ответ 3

Один простой вариант - вместо того, чтобы изо всех сил набирать классы типов, сделать все ваши типы разновидностями одного типа алгебраических данных:

data DomainObject = User {
                      objectID :: Int,
                      objectCreationDate :: Date
                      ...
                    }
                  | SomethingElse {
                      objectID :: Int,
                      objectCreationDate :: Date,
                      somethingProperty :: Foo
                      ...
                    }
                  | AnotherThing {
                      objectID :: Int,
                      objectCreationDate :: Date,
                      anotherThingProperty :: Bar
                      ...
                    }

Это неуклюже, потому что требует наличия всех ваших структур данных в одном файле, но, по крайней мере, позволяет вам использовать ту же функцию (objectID), чтобы получить идентификатор объекта.