Как мне написать: "если typeclass a, то a также является экземпляром b по этому определению".
У меня есть typeclass MyClass
, и в нем есть функция, которая создает String
. Я хочу использовать это для обозначения экземпляра Show
, чтобы я мог передавать типы, реализующие MyClass
в Show
. До сих пор я,
class MyClass a where
someFunc :: a -> a
myShow :: a -> String
instance MyClass a => Show a where
show a = myShow a
который дает ошибку Constraint is no smaller than the instance head.
, я также пробовал,
class MyClass a where
someFunc :: a -> a
myShow :: a -> String
instance Show (MyClass a) where
show a = myShow a
который дает ошибку, Class
MyClass 'используется как тип`.
Как я могу правильно выразить эти отношения в Haskell?
Спасибо.
Я должен добавить, что я хочу проследить это с конкретными экземплярами MyClass
, которые испускают определенные строки на основе их типа. Например,
data Foo = Foo
data Bar = Bar
instance MyClass Foo where
myShow a = "foo"
instance MyClass Bar where
myShow a = "bar"
main = do
print Foo
print Bar
Ответы
Ответ 1
(Edit: оставляя тело для потомков, но прыгаем до конца для реального решения)
В объявлении instance MyClass a => Show a
, рассмотрим ошибку "Ограничение не меньше, чем глава экземпляра". Ограничение - это ограничение типа класса слева от '= > ', в данном случае MyClass a
. "Глава экземпляра" - это все после класса, в котором вы пишете экземпляр, в данном случае a
(справа от Show
). Один из правил вывода в GHC требует, чтобы ограничение содержало меньше конструкторов и переменных, чем голова. Это часть того, что называется Условия Патерсона. Они существуют как гарантия того, что проверка типа завершается.
В этом случае ограничение точно совпадает с головкой, т.е. a
, поэтому это испытание не выполняется. Вы можете удалить проверки состояния Патерсона, включив UndecidableInstances, скорее всего, с помощью {-# LANGUAGE UndecidableInstances #-}
прагмы.
В этом случае вы, по существу, используете свой класс MyClass
как синоним типа typec для класса Show
. Создание синонимов классов, подобных этому, является одним из канонических применений для расширения UndecidableInstances, поэтому вы можете безопасно его использовать здесь.
"Undecidable" означает, что GHC не может доказать, что typechecking завершится. Хотя это звучит опасно, худшее, что может случиться от включения UndecidableInstances, заключается в том, что компилятор будет зацикливаться, в конечном счете заканчивая после исчерпания стека. Если он компилируется, то очевидно, что проверка типов завершена, поэтому проблем нет. Опасное расширение - IncoherentInstances, что так же плохо, как кажется.
Изменить: возникает другая проблема, вызванная этим подходом:
instance MyClass a => Show a where
data MyFoo = MyFoo ... deriving (Show)
instance MyClass MyFoo where
Теперь есть два экземпляра Show для MyFoo
, один из предложения получения и один для экземпляров MyClass. Компилятор не может решить, какой из них использовать, поэтому он выйдет с сообщением об ошибке. Если вы пытаетесь создать экземпляры типов MyClass
, которые вы не контролируете, у которых уже есть экземпляры Show
, вам придется использовать newtypes, чтобы скрыть уже существующие экземпляры Show. Даже типы без экземпляров MyClass
по-прежнему будут конфликтовать, потому что определение instance MyClass => Show a
, потому что определение фактически обеспечивает реализацию для всех возможных a
(проверка контекста приходит позже, а его не участвует в выборе экземпляра)
Чтобы сообщение об ошибке и как UndecidableInstances заставляют его уходить. К сожалению, это очень сложно использовать в реальном коде, по причинам, которые объясняет Эдвард Кметт. Первоначальный импульс заключался в том, чтобы избежать указания ограничения Show
, когда уже существует ограничение MyClass
. Учитывая, что я бы сделал, просто используйте myShow
из MyClass
вместо Show
. Вам не потребуется ограничение Show
вообще.
Ответ 2
Я хочу решительно не согласиться с разбитыми решениями, поставленными до сих пор.
instance MyClass a => Show a where
show a = myShow a
Из-за того, как работает это разрешение экземпляра, это очень опасный экземпляр для работы!
Разрешение экземпляра происходит путем эффективного сопоставления шаблонов в правой части каждого экземпляра =>
, полностью без учета того, что находится слева от =>
.
Когда ни один из этих экземпляров не перекрывается, это красиво. Тем не менее, вы здесь говорите: "Вот правило, которое вы должны использовать для КАЖДОГО Показать экземпляр. Когда его спросят о экземпляре show для любого типа, вам понадобится экземпляр MyClass, так что идите, и вот реализация". - как только компилятор выполнит выбор использования вашего экземпляра (просто благодаря тому, что "a" объединяется со всем), он не имеет возможности отступить и использовать любые другие экземпляры!
Если вы включите {-# LANGUAGE OverlappingInstances, IncoherentInstances #-}
и т.д., чтобы скомпилировать его, вы получите не очень тонкие сбои, когда будете писать модули, которые импортируют модуль, который предоставляет это определение, и должны использовать любой другой экземпляр Show. В конечном счете вы сможете получить этот код для компиляции с достаточным количеством расширений, но он, к сожалению, не будет делать то, что вы думаете, что он должен делать!
Если вы думаете об этом:
instance MyClass a => Show a where
show = myShow
instance HisClass a => Show a where
show = hisShow
который должен выбрать компилятор?
Ваш модуль может определять только один из них, но код конечного пользователя будет импортировать кучу модулей, а не только ваших. Кроме того, если другой модуль определяет
instance Show HisDataTypeThatHasNeverHeardOfMyClass
компилятор будет в пределах своих прав игнорировать его экземпляр и попытаться использовать ваши.
Правильный ответ, к сожалению, состоит в том, чтобы сделать две вещи.
Для каждого отдельного экземпляра MyClass вы можете определить соответствующий экземпляр Show с очень механическим определением
instance MyClass Foo where ...
instance Show Foo where
show = myShow
Это довольно неудачно, но работает хорошо, когда рассматриваются только несколько экземпляров MyClass.
Когда у вас есть большое количество экземпляров, способ избежать дублирования кода (когда класс значительно сложнее, чем показ) заключается в определении.
newtype WrappedMyClass a = WrapMyClass { unwrapMyClass :: a }
instance MyClass a => Show (WrappedMyClass a) where
show (WrapMyClass a) = myShow a
Это обеспечивает новый тип в качестве транспортного средства, например, для отправки. и затем
instance Foo a => Show (WrappedFoo a) where ...
instance Bar a => Show (WrappedBar a) where ...
однозначно, потому что тип "шаблоны" для WrappedFoo a
и WrappedBar a
не пересекается.
В пакете base
существует ряд примеров этой идиомы.
В Control.Applicative есть определения для WrappedMonad
и WrappedArrow
именно по этой причине.
В идеале вы сможете сказать:
instance Monad t => Applicative t where
pure = return
(<*>) = ap
но эффективно то, что говорит этот экземпляр, состоит в том, что каждый Аппликатив должен быть получен путем первого поиска экземпляра для Monad, а затем отправки на него. Таким образом, хотя это было бы намерение сказать, что каждая Монада является аппликативной (кстати, как это подразумевается как =>
), то, что она на самом деле говорит, состоит в том, что каждый Аппликатив является Монадой, поскольку наличие главы экземпляра 't' соответствует любому типу, Во многих отношениях синтаксис для определений "экземпляр" и "класс" обратный.
Ответ 3
Я думаю, что было бы лучше сделать это наоборот:
class Show a => MyClass a where
someFunc :: a -> a
myShow :: MyClass a => a -> String
myShow = show
Ответ 4
Вы можете скомпилировать его, но не с Haskell 98, вам нужно включить некоторые расширения языка:
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}
-- at the top of your file
Гибкие экземпляры позволяют разрешить контекст в объявлении экземпляра. Я не знаю значения UndecidableInstances, но я бы избегал столько, сколько мог.
Ответ 5
Вы можете найти несколько интересных ответов в связанном SO-вопросе: Связывание/объединение классов классов в Haskell
Ответ 6
Как отметил Эд Кметт, это совершенно невозможно для вашего дела. Однако, если у вас есть доступ к классу, для которого вы хотите предоставить экземпляр по умолчанию, вы можете свести шаблон к минимуму с помощью реализации по умолчанию и ограничить тип ввода с помощью необходимой вам по умолчанию подписки:
{-# LANGUAGE DefaultSignatures #-}
class MyClass a where
someFunc :: a -> Int
class MyShow a where
myShow :: a -> String
default myShow :: MyClass a => a -> String
myShow = show . someFunc
instance MyClass Int where
someFunc i = i
instance MyShow Int
main = putStrLn (myShow 5)
Обратите внимание, что единственный реальный шаблон (ну, кроме всего примера) уменьшен до instance MyShow Int
.
См. aeson
ToJSON
для более реалистичного примера.