Могу ли я напечатать в Haskell тип полиморфной функции, как если бы я передал ей объект конкретного типа?
Вот функция, полиморфная трех типов:
: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
и «информация» , что я хочу , чтобы применить первые к последнему?
Кому интересно, этот вопрос ранее был закрыт как дубликат этого .
Ответы
Другие ответы требуют помощи функций, которые были определены с искусственно ограниченными типами, таких как 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
В углу экрана спрятана небольшая изящная функция 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 в комментариях - использование функции с более строгой сигнатурой, чем ее реализация, для управления выводом типа - за исключением того, что нам не нужно ничего определять самостоятельно, за счет того, что мы не можем просто скопируйте рассматриваемое выражение под лямбду.
Я думаю, что ответ @ 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
Вы можете сделать это с помощью TypeApplications
расширения, которое позволяет вам явно указать, какие типы вы хотите использовать для создания экземпляров параметров типа:
λ :set -XTypeApplications
λ :t (.) @Char @Int
(.) @Char @Int :: (Char -> Int) -> (a -> Char) -> a -> Int
Обратите внимание, что аргументы должны быть в точном порядке.
Для функций, имеющих сигнатуру «обычного» типа foo :: a -> b
, порядок определяется порядком, в котором параметры типа впервые появляются в сигнатуре.
Для функций, которые используют ExplicitForall
like foo :: forall b a. a -> b
, порядок определяется тем, в чем он находится forall
.
Если вы хотите определить тип, основанный на применении (.)
к digitToChar
(а не просто зная, какие типы заполнять), я почти уверен, что вы не можете этого сделать в GHCi, но я настоятельно рекомендую поддержку Haskell IDE.
Например, вот как это выглядит для меня в VSCode (вот расширение ):

Это небольшая вариация ответа 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