Написание функции с универсальным типом возвращаемого значения

если я напишу

foo :: (Num a) => a
foo = 42

GHC с радостью примет это, но если я напишу

bar :: (Num a) => a
bar = (42 :: Int)

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

Я сталкиваюсь с той же ситуацией, пытаясь написать функцию, которая, сводясь к сути проблемы, выглядит примерно так:

-- Note, Frob is an instance of class Frobbable
getFrobbable :: (Frobbable a) => Frob -> a
getFrobbable x = x

Можно ли написать такую ​​функцию? Как я могу сделать результат совместимым с подписью типа?


person Wyzard    schedule 22.07.2010    source источник


Ответы (4)


Вы рассматриваете ограничения класса типов так, как если бы они были ограничениями подтипа. Это обычное дело, но на самом деле они совпадают только в контравариантном случае, то есть когда речь идет о аргументах функции, а не о результатах . Подпись:

bar :: (Num a) => a

Означает, что вызывающий объект может выбрать тип a при условии, что он является экземпляром Num. Итак, здесь вызывающий абонент может выбрать вызов bar :: Int или bar :: Double, все они должны работать. Таким образом, bar :: (Num a) => a должен иметь возможность создавать числа любого типа, bar не знает, какой именно тип был выбран.

Контравариантный случай точно такой же, просто в данном случае он соответствует интуиции ООП-программистов. Например.

baz :: (Num a) => a -> Bool

Означает, что вызывающий снова выбирает тип a, и снова baz не знает, какой конкретный тип был выбран.

Если вам нужно, чтобы вызываемый выбрал тип результата, просто измените сигнатуру функции, чтобы отразить это знание. Например.:

bar :: Int

Или, в вашем случае getFrobbable, getFrobbable :: Frob -> Frob (что делает функцию тривиальной). Везде, где встречается ограничение (Frobbable a), ему будет соответствовать Frob, поэтому вместо этого просто скажите Frob.

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

person luqui    schedule 22.07.2010
comment
Причина, по которой я не могу изменить сигнатуру getFrobbable для возврата Frob, заключается в том, что в моем реальном коде функция является частью другого класса — назовем ее FrobGettable. Цель состоит в том, чтобы разные экземпляры FrobGettable могли определить getFrobbable для возврата разных экземпляров Frobbable. Хотя, похоже, это не очень хороший дизайн. - person Wyzard; 22.07.2010
comment
Я собираюсь предположить, что все методы class Frobbable a имеют следующую форму: foo :: ... -> a -> ..., где ...s не содержат a. Если это так (как это обычно бывает в такого рода шаблонах), то замените класс Frobbable на простой тип данных: data Frobbable = Frobbable { foo :: ..., bar :: ... }, где ... — это сигнатура соответствующего метода с удаленным аргументом a. Затем ваш метод класса возвращает простой Frobbable. Это метод, описанный в ссылке на ошибку. - person luqui; 22.07.2010

Один из способов получить эффект, на который, похоже, нацелен ваш образец.

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

-- Note, Frob is an instance of class Frobbable
getFrobbable :: (Frobbable a) => Frob -> a
getFrobbable x = x

По сути, это операция литья. Он принимает Frob и просто забывает, что это такое, сохраняя только знание о том, что у вас есть экземпляр Frobbable.

Для этого в Haskell есть идиома. Для этого требуется расширение ExistentialQuantification в GHC. Вот пример кода:

{-# LANGUAGE ExistentialQuantification #-}
module Foo where

class Frobbable a where
  getInt :: a -> Int

data Frob = Frob Int

instance Frobbable Frob where
  getInt (Frob i) = i

data FrobbableWrapper = forall a . Frobbable a => FW a

instance Frobbable FrobbableWrapper where
  getInt (FW i) = getInt i

Ключевой частью является структура данных FrobbableWrapper. С его помощью вы можете написать следующую версию вашей функции приведения getFrobbable:

getFrobbable :: Frobbable a => a -> FrobbableWrapper
getFrobbable x = FW x

Эта идиома полезна, если вы хотите иметь гетерогенный список, элементы которого имеют общий класс типов, даже если они могут не иметь общего типа. Например, в то время как Frobbable a => [a] не позволит вам смешивать разные экземпляры Frobbable, список [FrobbableWrapper] определенно позволит.

Почему код, который вы разместили, не разрешен

Теперь, почему вы не можете написать свою операцию приведения как есть? Все дело в том, что можно было бы сделать, если бы вашей исходной функции getFrobbable было разрешено ввести проверку.

Уравнение getFrobbable x = x действительно следует рассматривать как уравнение. x никак не модифицируется; таким образом, ни его тип. Это делается по следующей причине:

Давайте сравним getFrobbable с другим объектом. Рассмотреть возможность

anonymousFrobbable :: Frobbable a => a
anonymousFrobbable = undefined

(Код, включающий undefined, является отличным источником неловкого поведения, когда вы действительно хотите подтолкнуть свою интуицию.)

Теперь предположим, что кто-то приходит и вводит определение данных и функцию наподобие

data Frob2 = Frob2 Int Int

instance Frobbable Frob2 where
  getInt (Frob2 x y) = y

useFrobbable :: Frob2 -> [Int]
useFrobbable fb2 = []

Если мы перейдем к ghci, мы сможем сделать следующее:

*Foo> useFrobbable anonymousFrobbable 
[]

Нет проблем: подпись anonymousFrobbable означает "Вы выбираете экземпляр Frobbable, а я притворяюсь, что я из этого типа".

Теперь, если мы попытаемся использовать вашу версию getFrobbable, вызов типа

useFrobbable (getFrobbable someFrob)

приведет к следующему:

  1. someFrob должен иметь тип Frob, так как он передается getFrobbable.
  2. (getFrobbable someFrob) должен иметь тип Frob2, так как он передается useFrobbable
  3. Но по уравнению getFrobbable someFrob = someFrob мы знаем, что getFrobbable someFrob и someFrob имеют один и тот же тип.

Таким образом, система делает вывод, что Frob и Frob2 относятся к одному и тому же типу, даже если это не так. Следовательно, это рассуждение является необоснованным, что в конечном итоге является типом рационального обоснования того, почему опубликованная вами версия getFrobbable не проверяет тип.

person intoverflow    schedule 22.07.2010

Также стоит отметить, что литерал 42 на самом деле означает fromInteger (42 :: Integer), который на самом деле имеет тип (Num a) => a. См. Отчет Haskell о числовых литералах.

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

person yatima2975    schedule 22.07.2010

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

data Frob = Frob Int Float

class FrobGettable a where
    getFrobbable :: Frob -> a

instance FrobGettable Int where
    getFrobbable (Frob x _) = x

instance FrobGettable Float where
    getFrobbable (Frob _ y) = y

instance FrobGettable Char where
    getFrobbable _ = '!'

frob = Frob 10 3.14

test1 :: Float
test1 = getFrobbable frob * 1.1

test2 :: Int
test2 = getFrobbable frob `div` 4

test3 = "Hi" ++ [getFrobbable frob]

Мы можем использовать GHCi, чтобы увидеть, что у нас есть,

*Main> :t getFrobbable
getFrobbable :: (FrobGettable a) => Frob -> a
person Anthony    schedule 22.07.2010