Haskell Language
Тип приложения
Поиск…
Вступление
TypeApplications
являются альтернативой аннотациям типа, когда компилятор пытается вывести типы для данного выражения.
Эта серия примеров объяснит цель расширения TypeApplications
и способы его использования
Не забудьте включить расширение, разместив {-# LANGUAGE TypeApplications #-}
в верхней части исходного файла.
Избегание аннотаций типа
Мы используем аннотации типов, чтобы избежать двусмысленности. Типовые приложения могут использоваться для той же цели. Например
x :: Num a => a
x = 5
main :: IO ()
main = print x
Этот код имеет ошибку неоднозначности. Мы знаем, что a
имеет экземпляр Num
, и для его печати мы знаем, что ему нужен экземпляр Show
. Это могло бы работать, если a
был, например, Int
, поэтому, чтобы исправить ошибку, мы можем добавить аннотацию типа
main = print (x :: Int)
Другое решение, использующее приложения типа, будет выглядеть так:
main = print @Int x
Чтобы понять, что это значит, нам нужно посмотреть на подпись типа print
.
print :: Show a => a -> IO ()
Функция принимает один параметр типа a
, но другой способ взглянуть на это состоит в том, что он фактически принимает два параметра. Первый - это параметр типа , второй - значение, тип которого является первым параметром.
Основное различие между параметрами значения и параметрами типа заключается в том, что последние неявно предоставляются функциям, когда мы их называем. Кто их предоставляет? Алгоритм вывода типа! Что позволяют TypeApplications
, это явно TypeApplications
эти параметры. Это особенно полезно, когда вывод типа не может определить правильный тип.
Итак, чтобы разбить приведенный выше пример
print :: Show a => a -> IO ()
print @Int :: Int -> IO ()
print @Int x :: IO ()
Тип приложений на других языках
Если вы знакомы с такими языками, как Java, C # или C ++ и концепцией generics / templates, то это сравнение может быть полезно для вас.
Скажем, у нас есть общая функция в C #
public static T DoNothing<T>(T in) { return in; }
Чтобы вызвать эту функцию с помощью float
мы можем сделать DoNothing(5.0f)
или если мы хотим быть явным, мы можем сказать DoNothing<float>(5.0f)
. Эта часть внутри угловых скобок является типом приложения.
В Haskell это одно и то же, за исключением того, что параметры типа не только неявны на сайтах-узлах, но также и на сайтах определения.
doNothing :: a -> a
doNothing x = x
Это также можно сделать явным, используя ScopedTypeVariables
, Rank2Types
или RankNTypes
подобные этому.
doNothing :: forall a. a -> a
doNothing x = x
Затем на сайте вызова мы снова можем либо написать doNothing 5.0
либо doNothing @Float 5.0
Порядок параметров
Проблема с неявными аргументами типа становится очевидной, если у нас больше нескольких. В какой порядок они входят?
const :: a -> b -> a
Имеет ли запись const @Int
значение a
равно Int
, или это b
? Если мы явно укажем параметры типа, используя forall
например const :: forall a b. a -> b -> a
тогда порядок написан так: a
, тогда b
.
Если мы этого не сделаем, то порядок переменных будет слева направо. Первой переменной, которая будет упомянута, является параметр первого типа, второй - параметр второго типа и т. Д.
Что делать, если мы хотим указать переменную второго типа, но не первую? Мы можем использовать подстановочный знак для первой переменной, подобной этой
const @_ @Int
Тип этого выражения
const @_ @Int :: a -> Int -> a
Взаимодействие с неоднозначными типами
Предположим, вы вводите класс типов, размер которых в байтах.
class SizeOf a where
sizeOf :: a -> Int
Проблема в том, что размер должен быть постоянным для каждого значения этого типа. Мы фактически не хотим, sizeOf
функция sizeOf
зависела от a
, но только от ее типа.
Без приложений типа наилучшим решением было Proxy
тип, определенный таким образом
data Proxy a = Proxy
Цель этого типа - переносить информацию о типе, но без информации о значении. Тогда наш класс мог бы выглядеть так
class SizeOf a where
sizeOf :: Proxy a -> Int
Теперь вам может быть интересно, почему бы вообще не отказаться от первого аргумента? Тогда тип нашей функции будет просто sizeOf :: Int
или, если быть более точным, потому что это метод класса sizeOf :: SizeOf a => Int
или быть еще более явным sizeOf :: forall a. SizeOf a => Int
.
Проблема заключается в выводе типа. Если я буду писать sizeOf
где-то, алгоритм вывода только знает, что я ожидаю Int
. Он не знает, какой тип я хочу заменить a
. Из-за этого определение будет отклонено компилятором, если вы не включили расширение {-# LANGUAGE AllowAmbiguousTypes #-}
. В этом случае определение компилируется, его просто невозможно использовать нигде без ошибки двусмысленности.
К счастью, введение типов приложений экономит день! Теперь мы можем написать sizeOf @Int
, явно говоря, что a
- Int
. Приложения типа позволяют нам предоставлять параметр типа, даже если он не отображается в реальных параметрах функции !