Кажется, я не могу определить переменные типа, смешанные с классами

Я в значительной степени понимаю 3/4 остальную часть языка, но каждый раз, когда я опускаю ноги в использование классов значимым образом в своем коде, я становлюсь надолго укорененным.

Почему этот чрезвычайно простой код не работает?

data Room n = Room n n deriving Show

class HasArea a where
  width :: (Num n) => a -> n

instance (Num n) => HasArea (Room n) where
  width (Room w h) = w

Таким образом, ширина комнаты обозначается ints или, возможно, float, я не хочу ограничивать ее в этот момент. И класс, и экземпляр ограничивают n тип Nums, но он все еще не нравится, и я получаю эту ошибку:

Couldn't match expected type `n1' against inferred type `n'
  `n1' is a rigid type variable bound by
       the type signature for `width' at Dungeon.hs:11:16
  `n' is a rigid type variable bound by
      the instance declaration at Dungeon.hs:13:14
In the expression: w
In the definition of `width': width (Room w h) = w
In the instance declaration for `HasArea (Room n)'

Итак, он говорит мне, что типы не совпадают, но он не говорит мне, какие типы он считает, что это действительно полезно. Как замечание, есть ли простой способ отладить ошибку, подобную этой? Единственный способ, которым я это знаю, - случайное изменение материала, пока оно не сработает.

Ответ 1

Ошибка, которую вы получаете, говорит вам, что, по его мнению, тип должен быть; к сожалению, оба типа обозначаются переменными типа, что затрудняет их просмотр. В первой строке указано, что вы указали тип выражения n, но он хотел дать ему тип n1. Чтобы выяснить, что это такое, посмотрите на следующие несколько строк:

`n1' is a rigid type variable bound by
     the type signature for `width' at Dungeon.hs:11:16

Это говорит о том, что n1 - это переменная типа, значение которой известно и, следовательно, не может меняться ( "жестко" ). Поскольку он связан сигнатурой типа для width, вы знаете, что он связан линией width :: (Num n) => a -> n. Там еще n в области видимости, поэтому этот n переименован в n1 (width :: (Num n1) => a -> n1). Далее, мы имеем

`n' is a rigid type variable bound by
    the instance declaration at Dungeon.hs:13:14

Это говорит вам, что Haskell нашел тип n из строки instance (Num n) => HasArea (Room n) where. Проблема, о которой сообщается, заключается в том, что n, который является типом GHC, вычисленным для width (Room w h) = w, не совпадает с n1, который является ожидаемым типом.

Причина, по которой у вас возникает эта проблема, заключается в том, что ваше определение width менее полиморфно, чем ожидалось. Типичная подпись width равна (HasArea a, Num n1) => a -> n1, что означает, что для каждого типа, являющегося экземпляром HasArea, вы можете представить его ширину с любым номером вообще. Однако в определении вашего экземпляра строка width (Room w h) = w означает, что width имеет тип Num n => Room n -> n. Обратите внимание, что это недостаточно полиморфно: while Room n является экземпляром HasArea, для этого требуется width иметь тип (Num n, Num n1) => Room n -> n1. Это неспособность унифицировать конкретный n с общим n1, который вызывает вашу ошибку типа.

Есть несколько способов исправить это. Один подход (и, вероятно, лучший подход), который вы можете увидеть в sepp2k answer, состоит в том, чтобы сделать HasArea тип переменной типа * -> *; это означает, что вместо a, являющегося самим типом, такие вещи, как a Int или a n, являются типами. Maybe и [] являются примерами типов с видом * -> *. (Обычные типы типа Int или Maybe Double имеют вид *.) Это, вероятно, лучший выбор.

Если у вас есть некоторые типы типов *, у которых есть область (например, data Space = Space (Maybe Character), где width всегда 1), однако это не сработает. Другой способ (который требует некоторых расширений для Haskell98/Haskell2010) состоит в том, чтобы сделать HasArea класс с несколькими параметрами:

{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances #-}

data Room n = Room n n deriving Show

class Num n => HasArea a n where
  width :: a -> n

instance Num n => HasArea (Room n) n where
  width (Room w h) = w

Теперь вы передаете тип ширины в качестве параметра самому классу типа, поэтому width имеет тип (HasArea a n, Num n) => a -> n. Возможно, это связано с тем, что вы можете объявить instance HasArea Foo Int и instance HasArea Foo Double, что может быть проблематичным. Если это так, то для решения этой проблемы вы можете использовать функциональные зависимости или типы семейств. Функциональные зависимости позволяют указать, что заданный один тип, другие типы определяются однозначно, как если бы у вас была обычная функция. Используя те, вы получаете код

{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, FunctionalDependencies #-}

data Room n = Room n n deriving Show

class Num n => HasArea a n | a -> n where
  width :: a -> n

instance Num n => HasArea (Room n) n where
  width (Room w h) = w

Бит | a -> n сообщает GHC, что если он может вывести a, то он также может вывести n, так как для каждого a существует только один n. Это предотвращает описанные выше экземпляры.

Типы семейств более разные:

{-# LANGUAGE MultiParamTypeClasses, FlexibleContexts, TypeFamilies #-}

data Room n = Room n n deriving Show

class Num (Area a) => HasArea a where
  type Area a :: *
  width :: a -> Area a

instance Num n => HasArea (Room n) where
  type Area (Room n) = n
  width (Room w h) = w

Это говорит о том, что помимо функции width класс HasArea также имеет тип Area (или функцию типа, если вы хотите об этом думать). Для каждого HasArea a вы указываете тип Area a (который, благодаря ограничению суперкласса, должен быть экземпляром Num), а затем использовать этот тип как ваш тип номера.

Как отлаживать такие ошибки? Честно говоря, мой лучший совет - "Практика, практика, практика". Со временем вы будете больше использовать для выяснения (а) того, что говорят ошибки, и (б) что, вероятно, пошло не так. Случайное изменение материала - это один из способов сделать это. Однако самый большой совет, который я могу дать, - обратить внимание на строки Couldn't match expected type `Foo' against inferred type `Bar'. Они сообщают вам, что вычислил компилятор (Bar) и ожидаемый (Foo) для этого типа, и если вы можете точно определить, какие именно эти типы, это поможет вам выяснить, где ошибка.

Ответ 2

class HasArea a where
  width :: (Num n) => a -> n

Тип (Num n) => a -> n означает, что для любого типа n, который является экземпляром Num, width должен иметь возможность вернуть значение этого типа. Поэтому для любого значения v типа T, где T является экземпляром HasArea, следующий код должен быть действительным:

let x :: Integer = width v
    y :: Double = width v
in
    whatever

Однако это не относится к вашим экземплярам Room. Например, для Room Integer y :: Dobule = width v недействительно.

Чтобы сделать ваш пример работы, вы можете сделать что-то вроде этого:

data Room n = Room n n deriving Show

class HasArea a where
  width :: (Num n) => a n -> n

instance HasArea Room where
  width (Room w h) = w

Здесь мы не говорим, что Room Integer, Room Float и т.д. являются экземплярами HasArea, но вместо этого Room является экземпляром HasArea. И тип width таков, что он генерирует значение типа n при задании значения типа a n, поэтому, если вы введете Room Integer, вы вернете Integer. Таким образом, тип подходит.