Как я могу продвигать повторное использование кода способом, аналогичным mixins / модификаторам методов / признакам на других языках? - PullRequest
9 голосов
/ 30 августа 2011

Я работаю над кодом, который взаимодействует со схемой базы данных, которая моделирует постоянный граф. Прежде чем перейти к деталям моего конкретного вопроса, я подумал, что это может помочь обеспечить некоторую мотивацию. Моя схема вокруг книг, людей и ролей авторов. Книга имеет много авторских ролей, где в каждой роли есть человек. Однако вместо того, чтобы разрешать прямые запросы UPDATE для объектов книги, вы должны создать новую книгу и внести изменения в новую версию.

Теперь вернемся к земле Хаскелла. В настоящее время я работаю с несколькими типами классов, но, что важно, у меня есть HasRoles и Entity:

class HasRoles a where
    -- Get all roles for a specific 'a'
    getRoles :: a -> IO [Role]

class Entity a where
    -- Update an entity with a new entity. Return the new entity.
    update :: a -> a -> IO a

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

instance Entity Book where
    update orig newV = insertVersion V >>= copyBookRoles orig

Это хорошо, но есть кое-что, что меня беспокоит, и это отсутствие какой-либо гарантии инварианта, что если что-то будет Entity и HasRoles, то вставка новой версии скопирует над существующими ролями. Я подумал о 2 вариантах:

Используйте больше типов

Одним из «решений» является введение RequiresMoreWork a b. Исходя из вышесказанного, insertVersion теперь возвращает HasRoles w => RequiresMoreWork w Book. update хочет Book, поэтому, чтобы получить значение RequiresMoreWork, мы могли бы позвонить workComplete :: RequiresMoreWork () Book -> Book.

Однако реальная проблема заключается в том, что наиболее важной частью головоломки является сигнатура типа insertVersion. Если это не совпадает с инвариантами (например, в нем не упоминается необходимость HasRoles), то все снова разваливается, и мы возвращаемся к нарушению инварианта.

Докажите это с помощью QuickCheck

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


Я немного озадачен этим. В Лиспе я бы использовал модификаторы методов, в Perl я бы использовал роли, но есть ли что-нибудь, что я могу использовать в Haskell?

Ответы [ 2 ]

4 голосов
/ 30 августа 2011

Имея дело с конкретным, я бы сделал роли частью типа, а не класса

данные Свернутый a = Свернутый a [Роль]

экземпляр Entity a => Entity (Rolled a) где update (Rolled a rs) = Rolled (update a) rs

В более общем смысле, вы можете просто создать пары экземпляров Entity

Я не дошел до Haskell Zen, но я думаю, что вы должны работать с монадой Writer или State (или их версиями-трансформерами)

3 голосов
/ 07 сентября 2011

Я думаю о том, как мне ответить на это:

Это хорошо, но есть кое-что, что меня беспокоит, и это недостаток любой гарантии инварианта, что если что-то является сущностью и HasRoles, то вставка новой версии скопирует поверх существующей роли.

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

С другой стороны, это означает, что вы будете воспроизводить шаблон copyRoles для каждого из ваших типов, и вы наверняка можете забыть включить его, так что это законная проблема.

Когда вам требуется динамическая диспетчеризация такого типа, одним из вариантов является использование GADT для охвата контекста класса:

class Persisted a where
    update :: a -> a -> IO a

data Entity a where
    EntityWithRoles :: (Persisted a, HasRoles a) => a -> Entity a
    EntityNoRoles   :: (Persisted a) => a -> Entity a

instance Persisted (Entity a) where
    insert (EntityWithRoles orig) (EntityWithRoles newE) = do
      newRoled <- copyRoles orig newE
      EntityWithRoles <$> update orig newRoled
    insert (EntityNoRoles orig) (EntityNoRoles newE) = do
      EntityNoRoles <$> update orig newE

Однако, с учетом описанной вами структуры, вместо использования метода класса update, вы можете использовать метод save, где update будет нормальной функцией

class Persisted a where
    save :: a -> IO ()

-- data Entity as above

update :: Entity a -> (a -> a) -> IO (Entity a)
update (EntityNoRoles orig) f = let newE = f orig in save newE >> return (EntityNoRoles newE)
update (EntityWithRoles orig) f = do
  newRoled <- copyRoles orig (f orig)
  save newRoled
  return (EntityWithRoles newRoled)

Я бы ожидал, что с некоторым вариантом этого будет гораздо проще работать.

Основное различие между классами типов и классами ООП заключается в том, что методы классов типов не предоставляют никаких средств повторного использования кода. Чтобы повторно использовать код, вам нужно извлечь общие черты из методов класса типов в функции, как я сделал с update во втором примере. Альтернатива, которую я использовал в первом примере, состоит в том, чтобы преобразовать все в какой-то общий тип (Entity) и затем работать только с этим типом. Я ожидаю, что второй пример с автономной функцией update будет проще в долгосрочной перспективе.

Есть еще один вариант, который стоит изучить. Вы можете сделать HasRoles суперклассом Entity и потребовать, чтобы все ваши типы имели HasRoles экземпляров с фиктивными функциями (например, getRoles _ = return []). Если большинство ваших сущностей в любом случае будут иметь роли, с ними довольно удобно работать, и это абсолютно безопасно, хотя и несколько не элегантно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...