Написание функции с универсально выраженным типом возвращаемого значения - PullRequest
2 голосов
/ 22 июля 2010

Если я напишу

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

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

Ответы [ 4 ]

11 голосов
/ 22 июля 2010

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

bar :: (Num a) => a

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

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

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

Означает, что абонент получает возможность выбрать тип a, снова и снова baz не знает, какой именно тип был выбран.

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

bar :: Int

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

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

3 голосов
/ 22 июля 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 не проверяет тип.

2 голосов
/ 22 июля 2010

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

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

0 голосов
/ 22 июля 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...