Моделирование данных домена в 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 является довольно новым (но довольно захватывающим) для меня.
Ответы
Ответ 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
), чтобы получить идентификатор объекта.