Скажем, у меня есть несколько типов:
data Person = Person
{ worksFor :: String
, ...
}
data Company = Company
{ companyId :: String
, address :: String
,...
}
companies :: [Company]
people :: [Person]
worksFor
в Person
ссылается на данный Company
посредством companyId
. Допустим, я завязываю узел и в итоге получаю:
data Person' = Person'
{ worksFor :: Company'
, ...
}
data Company' = Company'
{ ...
}
Очевидно, что несколько Person
могут ссылаться на один и тот же Company
, но в Person'
это совместное использование теряется. Также очевидно, что если я обновлю данный Company'
, это обновление не будет отражено в значениях типа Person'
, которые ссылаются на это Company'
.
Я знаю о IxSet
и тому подобном, но я бы предпочел, чтобы это было просто. Я хотел бы запретить любые обновления записей Person'
и Company'
, сохраняя при этом доступ к их полям.
Одним из решений было бы определить Company'
и Person'
в отдельном модуле с разрешенным NoTraditionalRecordSyntax
(и, таким образом, определить типы данных без синтаксиса записи, например, data Company' = Company' String String ...
), определить функции селектора, имитирующие селекторы полей, сгенерированные GHC, и не экспортировать какие-либо конструкторы, но это кажется как перебор.
Другим вариантом было бы обернуть каждое поле в типы данных ...'
, например. data Gettable a = Gettable { get :: a }
и не экспортировать конструктор Gettable
, но тогда каждому доступу к полю должен предшествовать get
, что довольно некрасиво.
Мне интересно, есть ли другой способ добиться этого, которого я не вижу?
Также обратите внимание, что структура в этой конкретной задаче представляет собой двудольный граф, поэтому может иметь смысл не делать никаких специальных узлов, а вместо этого использовать библиотечную реализацию, такую как Algebra.Graph.Bipartite. Или, проще говоря, вообще не хранить worksFor
как поле для Person
, а вместо этого вывести это из компании, в которой работает этот человек.
Редактировать: как отмечено в комментариях, приведенная ниже рекомендация не предотвращает обновления синтаксиса записи, такие как p { worksFor = "foo" }
.
Самый простой способ запретить обновления полей — не экспортировать их конструкторы. Вы по-прежнему можете записывать их в виде записей и экспортировать имена полей в качестве геттеров, указав имена полей отдельно в списке экспорта.
Скорее всего, вам понадобится какой-то способ построить их вне модуля, в котором они определены, и в этом случае вы можете определить свой собственный конструктор и экспортировать его (может быть так же просто, как mkPerson = Person
).
См., например, эту статью, в которой используется аналогичный подход.
Проблема в том, что даже если вы не экспортируете конструкторы, но уже имеете значение c :: Company'
, вы все равно можете сделать обновление записи c { address = "..." }
.
Ах, я не понял этого! Мой плохой, обновил мой ответ, чтобы отразить это. Кажется, что подход, который вы изложили в вопросе, действительно ваш лучший выбор.
Одним из решений было бы определить
Company'
иPerson'
в отдельном модуле с разрешеннымNoTraditionalRecordSyntax
(и, таким образом, определить типы данных без синтаксиса записи, например,data Company' = Company' String String ...
), определить функции селектора, имитирующие селекторы полей, сгенерированные GHC, и не экспортировать какие-либо конструкторы, но это кажется как перебор.
Это звучит совершенно правильно для меня. Цель синтаксиса записи — объявить, что ваш тип — это просто скучный конкретный тип кортежа с любыми значениями, разрешенными для всех его полей. Вы не хотите этого; вам нужен абстрактный тип с контролем доступа. Поэтому не используйте запись, не экспортируйте конструктор и не определяйте функции селектора вручную. Немного хлопотно, но иногда это цена правильного API.
В случае, если вы хотите сохранить часть синтаксиса записи, доступную только для чтения, но запретить обновления, вы можете попытаться использовать синонимы шаблонов.
К сожалению, для этого требуется некоторый шаблон. Нам нужно определить модуль и быть осторожным при экспорте только общедоступного интерфейса.
{-# LANGUAGE PatternSynonyms #-}
module PatSynRecord
( R(R) -- the type and the pattern synonym
, a -- the patsyn fields (it would be nice if we didn't have to list them)
, b
, c
, test -- an example
) where
-- The private, internal representation
data R = PriR
{ priA :: Int
, priB :: String
, priC :: Bool
}
deriving Show
-- Optional type signature
pattern R :: Int -> String -> Bool -> R
pattern R { a, b, c } <- PriR {priA=a, priB=b, priC=c}
{-# COMPLETE R #-}
-- In this module we can use the internal representation
test :: R
test = PriR { priA=1, priB = "dsaasd", priC=True }
Приведенный выше пацын является однонаправленным, поэтому пользователи этого модуля могут использовать его только для чтения данных из записи. Это разрешено:
a test + 4
case test of R{a=x} -> x+4
Это обновление не
test{ a=45 }
Это решение не идеально, так как поля должны повторяться в PriR
приватном конструкторе, в R
публичном пацыне (дважды!), и в списке экспорта модулей. С положительной стороны, использование такого модуля позволяет экспортировать только нужные смарт-конструкторы, эффективно ограничивая значения типа R
, которые могут быть созданы.
Не экспортируйте конструкторы и не пишите геттеры вручную.