Могу ли я напечатать в Haskell тип полиморфной функции, как если бы я передал ей объект конкретного типа?

Dec 12 2020

Вот функция, полиморфная трех типов:

:t (.)
(.) :: (b -> c) -> (a -> b) -> a -> c

а вот неполиморфная функция:

:t Data.Char.digitToInt
Data.Char.digitToInt :: Char -> Int

Если применить первое ко второму, мы получим функцию, полиморфную в 1 типе:

:t (.) Data.Char.digitToInt
(.) Data.Char.digitToInt :: (a -> Char) -> a -> Int

что означает, что он (.)был «создан» (я не уверен, что это правильный термин; как программист на C ++ я бы назвал его так) с помощью b === Charи c === Int, поэтому подпись, к (.)которой применяется, digitToIntследующая

(Char -> Int) -> (a -> Char) -> a -> Int

Мой вопрос: есть ли способ , чтобы эта подпись напечатанной на экране, учитывая (.), digitToIntи «информация» , что я хочу , чтобы применить первые к последнему?

Кому интересно, этот вопрос ранее был закрыт как дубликат этого .

Ответы

7 MikeSpivey Dec 13 2020 at 06:32

Другие ответы требуют помощи функций, которые были определены с искусственно ограниченными типами, таких как asTypeOfфункция в ответе HTNW. В этом нет необходимости, как показывает следующее взаимодействие:

Prelude> let asAppliedTo f x = const f (f x)

Prelude> :t head `asAppliedTo` "x"
head `asAppliedTo` "x" :: [Char] -> Char

Prelude> :t (.) `asAppliedTo` Data.Char.digitToInt
(.) `asAppliedTo` Data.Char.digitToInt
  :: (Char -> Int) -> (a -> Char) -> a -> Int

Это использует отсутствие полиморфизма в лямбда-связывании, которое неявно присутствует в определении asAppliedTo. Оба вхождения fв его теле должны иметь один и тот же тип, и это тип его результата. constИспользуемая здесь функция также имеет естественный тип a -> b -> a:

const x y = x
12 HTNW Dec 12 2020 at 09:09

В углу экрана спрятана небольшая изящная функция Prelude:

Prelude.asTypeOf :: a -> a -> a
asTypeOf x _ = x

Он задокументирован как «принуждение первого аргумента к тому же типу, что и второй». Мы можем использовать это, чтобы задать тип (.)первого аргумента:

-- (.) = \x -> (.) x = \x -> (.) $ x `asTypeOf` Data.Char.digitToInt -- eta expansion followed by definition of asTypeOf -- the RHS is just (.), but restricted to arguments with the same type as digitToInt -- "what is the type of (.) when the first argument is (of the same type as) digitToInt?" ghci> :t \x -> (.) $ x `asTypeOf` Data.Char.digitToInt
\x -> (.) $ x `asTypeOf` Data.Char.digitToInt
  :: (Char -> Int) -> (a -> Char) -> a -> Int

Конечно, это работает для любого количества аргументов, которое вам нужно.

ghci> :t \x y -> (x `asTypeOf` Data.Char.digitToInt) . (y `asTypeOf` head)
\x y -> (x `asTypeOf` Data.Char.digitToInt) . (y `asTypeOf` head)
  :: (Char -> Int) -> ([Char] -> Char) -> [Char] -> Int

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

8 K.A.Buhr Dec 12 2020 at 09:57

Я думаю, что ответ @ HTNW, вероятно, покрывает это, но для полноты, вот как inContextработает решение в деталях.

Сигнатура типа функции:

inContext :: a -> (a -> b) -> a

означает, что, если у вас есть вещь, которую вы хотите напечатать, и «контекст», в котором она используется (выражаемая как лямбда, которая принимает ее в качестве аргумента), скажем, с типами:

thing :: a1
context :: a2 -> b

Вы можете принудительно объединить a1(общий тип thing) с a2(ограничениями контекста), просто построив выражение:

thing `inContext` context

Обычно унифицированный тип thing :: aтеряется, но сигнатура типа inContextподразумевает, что тип всего результирующего выражения также будет унифицирован с желаемым типом a, и GHCi с радостью сообщит вам тип этого выражения.

Итак, выражение:

(.) `inContext` \hole -> hole digitToInt

в конечном итоге получает назначенный тип, (.)который имел бы в указанном контексте. Вы можете ввести это в заблуждение так:

(.) `inContext` \(.) -> (.) digitToInt

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

На самом деле не имеет значения, как inContextопределяется, если вы просто спрашиваете GHCi о его типе, так inContext = undefinedчто это сработало бы. Но, просто взглянув на сигнатуру типа, достаточно легко дать inContextрабочее определение:

inContext :: a -> (a -> b) -> a
inContext a _ = a

Оказывается, это всего лишь определение const, поэтому inContext = constтоже работает.

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

Так, например:

λ> :t (fromJust, fmap length) `inContext` \(a,b) -> a . b
(fromJust, fmap length) `inContext` \(a,b) -> a . b
  :: Foldable t => (Maybe Int -> Int, Maybe (t a) -> Maybe Int)

сообщает вам, что в выражении fromJust . fmap lengthтипы были специализированы для:

fromJust :: Maybe Int -> Int
fmap length :: Foldable t => Maybe (t a) -> Maybe Int
6 FyodorSoikin Dec 12 2020 at 03:35

Вы можете сделать это с помощью TypeApplicationsрасширения, которое позволяет вам явно указать, какие типы вы хотите использовать для создания экземпляров параметров типа:

λ :set -XTypeApplications                                 
λ :t (.) @Char @Int
(.) @Char @Int :: (Char -> Int) -> (a -> Char) -> a -> Int

Обратите внимание, что аргументы должны быть в точном порядке.

Для функций, имеющих сигнатуру «обычного» типа foo :: a -> b, порядок определяется порядком, в котором параметры типа впервые появляются в сигнатуре.

Для функций, которые используют ExplicitForalllike foo :: forall b a. a -> b, порядок определяется тем, в чем он находится forall.


Если вы хотите определить тип, основанный на применении (.)к digitToChar(а не просто зная, какие типы заполнять), я почти уверен, что вы не можете этого сделать в GHCi, но я настоятельно рекомендую поддержку Haskell IDE.

Например, вот как это выглядит для меня в VSCode (вот расширение ):

5 chi Dec 12 2020 at 16:39

Это небольшая вариация ответа HTNW.

Предположим, у нас есть любое потенциально большое выражение, включающее полиморфный идентификатор. poly

 .... poly ....

и нам интересно, как в этот момент был создан полиморфный тип.

Это можно сделать, используя две особенности GHC: asTypeOf(как указано HTNW) и типизированные дыры , а именно:

 .... (poly `asTypeOf` _) ....

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

Вот пример в GHCi:

> ((.) `asTypeOf` _) Data.Char.digitToInt
<interactive>:11:17: error:
    * Found hole: _ :: (Char -> Int) -> (a -> Char) -> a -> Int