Обработка нескольких типов с одинаковым внутренним представлением и минимальным шаблоном?

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

Существует два относительно очевидных подхода к решению этой проблемы.

Один использует класс типа и расширение GeneralizedNewtypeDeriving. Положите достаточную логику в класс типа, чтобы поддерживать общие операции, которые желает использовать прецедент. Создайте тип с желаемым представлением и создайте экземпляр класса типа для этого типа. Затем для каждого варианта использования создайте обертки для него с помощью newtype и выведите общий класс.

Другой - объявить тип с переменной типа phantom, а затем использовать EmptyDataDecls для создания разных типов для каждого другого варианта использования.

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

Каковы преимущества и недостатки каждого подхода? Есть ли техника, которая подходит ближе к тому, что я хочу, обеспечивая безопасность типов без кода шаблона?

Ответ 1

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

Например, в некоторых случаях у вас есть общий тип, конструкторы которого открыты, и вы хотите использовать обертки newtype, чтобы указать более семантически определенный тип. Используя newtype, затем приводит к сайтам вызова, например,

s1 = Specific1 $ General "Bob" 23
s2 = Specific2 $ General "Joe" 19

Если факт, что внутренние представления одинаковы между различными конкретными типами, прозрачен.

Подход типа тега почти всегда сопровождается скрытием конструктора представления,

data General2 a = General2 String Int

и использование интеллектуальных конструкторов, что приводит к определению типа данных и сайтам вызовов, например,

mkSpecific1 "Bob" 23

Отчасти из-за того, что вам нужен синтаксически легкий способ указать, какой тег вы хотите. Если вы не предоставили интеллектуальные конструкторы, тогда код клиента часто собирал аннотации типов для сужения вещей, например,

myValue = General2 String Int :: General2 Specific1

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

internalFun :: General2 a -> General2 a -> Int
internalFun (General2 _ age1) (General2 _ age2) = age1 + age2

Конечно, вы можете использовать newtype со умными конструкторами и внутренним классом для доступа к общему представлению, но я думаю, что ключевая точка принятия решения в этом пространстве проектирования заключается в том, хотите ли вы, чтобы ваши конструкторы представлений были открыты. Если совместное использование представления должно быть прозрачным, а клиентский код должен иметь возможность использовать любой тег, который он пожелает, без дополнительной проверки, то newtype обертки с GeneralizedNewtypeDeriving работают нормально. Но если вы собираетесь использовать интеллектуальные конструкторы для работы с непрозрачными представлениями, тогда я обычно предпочитаю типы phantom.

Ответ 2

Есть еще один простой подход.

data MyGenType = Foo | Bar

op :: MyGenType -> MyGenType
op x = ...

op2 :: MyGenType -> MyGenType -> MyGenType
op2 x y = ...

newtype MySpecialType {unMySpecial :: MyGenType}

inMySpecial f = MySpecialType . f . unMySpecial
inMySpecial2 f x y = ...

somefun = ... inMySpecial op x ...
someOtherFun = ... inMySpecial2 op2 x y ...

С другой стороны,

newtype MySpecial a = MySpecial a
instance Functor MySpecial where...
instance Applicative MySpecial where...

somefun = ... fmap op x ...
someOtherFun = ... liftA2 op2 x y ...

Я думаю, что эти подходы более приятные, если вы хотите использовать свой общий тип "голый" с любой частотой, и только иногда захотите пометить его. Если, с другой стороны, вы обычно хотите использовать его с тегами, то подход типа phantom более непосредственно выражает то, что вы хотите.

Ответ 3

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

Здесь представлены некоторые подводные камни, в зависимости от характера типа и того, какие операции задействованы.

Во-первых, это заставляет много функций быть излишне полиморфными - даже если на практике каждый экземпляр делает то же самое для разных оболочек, предположение открытого мира для классов типов означает, что компилятор должен учитывать возможность других экземпляров, Хотя GHC определенно умнее среднего компилятора, чем больше информации вы можете дать, тем больше он может вам помочь.

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

С другой стороны, если завернутый тип уже остается абстрактным (т.е. он не экспортирует конструкторы), проблема с узким местом не имеет значения, поэтому класс типа может иметь смысл. В противном случае я бы, вероятно, пошел с тегами типа phantom (или, возможно, с идентификатором Functor, описанным в sclv).