Поддерживает ли Haskell отражение типов, как в других языках, таких как Python?
Это отличается от Haskell. Как вывести информацию о какой-либо функции в Haskell, например «ghci> :info func»
Проверка типов во время выполнения поддерживается в таких языках, как Java или C#, но в Haskell в этом нет необходимости, поскольку это язык со строгой типизацией. Если вам нужно поддерживать больше типов для функции, скажем, вы можете использовать полиморфную проверку типов.
Возможно, вы ищете Typeable
, typeOf
, eqT
и друзей? Или, может быть, просто классы типов? Трудно сказать. Можете ли вы написать какой-нибудь псевдокод, показывающий, как вы будете использовать возможность «программного получения типа» для достижения чего-то?
Если вы не запланировали прослушивание типов с помощью Typeable
и др., тип не существует во время выполнения. Как только проверка типов завершена, типы стираются, и во время выполнения единственным существующим «типом» являются в основном необработанные биты.
Просто для ясности: все специальные команды GHCi, начинающиеся с двоеточия (например, :type
, :info
и т. д.), не являются частью языка Haskell. GHCi поддерживает их, «оборачивая» фактический Haskell и отслеживая дополнительную информацию. Таким образом, ни одна из этих функций не будет доступна в самом Haskell. Если в Haskell есть что-то, что может решить подобную задачу, нам придется искать это в совершенно другом месте.
Haskell спроектирован таким образом, что типы полностью стираются во время компиляции.
Скажем, у нас есть значение вроде Just True
. При компиляции исходного кода Haskell мы знаем, что он имеет тип Maybe Bool
. Но при преобразовании в настоящий машинный код код, который создает это значение, просто выделяет небольшой блок памяти с небольшим числом и указателем на значение True
. Число используется, чтобы сказать, был ли конструктор Nothing
или Just
. Весь код, который обрабатывает значения Maybe
, будет скомпилирован таким образом, что он использует небольшое число, чтобы сказать, должен ли он выбрать ветку для конструктора Nothing
или конструктора Just
; скажем, компилятор выбирает 0
вместо Nothing
и 1
вместо Just
.
Но в этом маленьком блоке памяти нет ничего, что говорило бы ни о том, что это значение типа данных Maybe
, ни о том, что указатель относится к значению типа Bool
. Любой другой конструктор с одним аргументом также будет представлен в памяти как просто небольшое число и указатель, а некоторые из них могут даже использовать то же число 1
, которое использовалось для Just
. Числовые теги нужны только для того, чтобы отличать другие конструкторы от того же типа; нет глобального реестра, присваивающего уникальные номера, чтобы гарантировать, что разные типы не используют одни и те же номера.
Так что на самом деле скомпилированный код Haskell не может просмотреть произвольное значение и сказать вам, к какому типу оно относится. Эта информация просто исчезла.
Haskell поддерживает отражение типов во время выполнения. Но это не доступно по любой цене без подготовки; это независимая система, требующая от вас явной подготовки к получению доступа к информации о типах во время выполнения. По сути, он использует систему классов типов, чтобы попросить компилятор сохранить информацию о типах.
Класс Typeable специально поддерживается компилятором. Это класс типов, которые могут быть представлены и проверены во время выполнения. Класс фактически включает все типы; компилятор автоматически создает экземпляры для вас. Наложение ограничения Typeable
на вашу функцию означает, что компилятор сделает методы Typeable
доступными для вас, сохраняя достаточно информации во время выполнения, чтобы вы могли спросить: «Какой тип этого значения?». Если у вас нет ограничения Typeable
для переменной типа, эта информация не будет доступна во время выполнения, и вы не сможете запросить тип. (Это также означает, что ограничения Typeable
должны быть добавлены к каждой функции вверх по стеку вызовов до точки, где переменная типа была фактически создана с конкретным типом; если ваши вызывающие объекты не выбрали отражение типа во время выполнения, то вы в одностороннем порядке вернуть нельзя)
То, как вы на самом деле используете его, заключается в том, что вы можете использовать typeOf x
для получения значения типа TypeRep a
(где a
— это тип x
). например typeOf True
дает вам TypeRep Bool
, а typeOf (Just 'a')
дает вам TypeRep (Maybe Char)
и т. д. Если вам нужно использовать это, вы, вероятно, не знаете, что такое переменная типа a
на самом деле, но затем вы можете использовать eqTypeRep
, чтобы проверить, равен ли ваш TypeRep
TypeRep
какого-то другого известного типа, и если это так, вы теперь знаете, что x
относится к этому типу и может вызывать для него другие функции, специфичные для этого типа. Вы не можете просто использовать базовые тесты ==
, чтобы проверить, равен ли он известному типу, поскольку это дает вам только True
или False
, которые ничего не доказывают компилятору; это должно выглядеть немного сложнее, но в основном сводится к этой простой идее. Это может выглядеть примерно так:
{-# LANGUAGE GHC2021, GADTs #-}
import Type.Reflection ( Typeable, typeOf, typeRep, eqTypeRep, (:~~:) (HRefl) )
foo :: Typeable a => a -> Integer
foo x = case typeOf x `eqTypeRep` typeRep @Integer of
Just HRefl -> x + 17
Nothing -> 0
Внутри Just HRefl
плеча case
компилятор знает, что x
имеет тип Integer
, поэтому допустимо добавить 17
и вернуть его как результат функции (которая должна быть чем-то типа Integer
). В руке Nothing
компилятор не позволит вам использовать функциональность Integer
для x
или вернуть ее, поэтому вместо этого мы должны вернуть что-то еще, что, как мы знаем, является Integer
.
Остальная функциональность в Type.Reflection позволяет вам выполнять более гибкие проверки (например, вы можете проверить, применяется ли тип значения Maybe
к чему-либо, не заботясь о том, к какому типу оно применяется). Но в конечном счете все сводится к возможности взять значение, тип которого является переменной типа, и сделать конечное число «догадок» о его типе. Если какая-либо из ваших догадок верна, вы можете действовать в соответствии с этой информацией, но всегда будет вероятность того, что это не какой-либо из типов, которые вы специально проверяете, и вам все равно нужно иметь ветвь, где это просто черный ящик. value (хотя вы всегда можете error
отказаться, если вас не волнует, что ваша функция является полной).
Это позволяет обойти обычные ограничения на значения, тип которых является переменной; без Typeable
вы вообще ничего не можете с ним сделать, кроме как передать его чему-то другому, ожидая значения переменной того же типа (например, переданная функция сопоставления или ограниченная функция класса типа, если у вас есть доступные ограничения на переменную).
Одна вещь, которая, возможно, не была очевидна из вышеизложенного, заключается в том, что мы не можем отразить полиморфизм. То есть нет способа получить TypeRep
, который сам отражает тип, содержащий переменные. Если функция принимает [a]
, эта переменная типа a
будет создаваться при каждом вызове определенного типа, и TypeRep
в конечном итоге будет отражать конкретный тип, который использовался в этом конкретном вызове (например, [Integer]
или [Maybe Bool]
или [Char -> Maybe (IO String)]
; он не будет отражать полиморфный тип [a]
.
Большая часть полезности Haskell для программирования на самом деле связана с ограничениями на то, что вы можете делать со значениями, типы которых содержат переменные. Когда вы привыкнете к этому, знание того, что функция не может сделать, основано исключительно на ее типе, очень полезно.
Это полностью выходит за рамки, когда есть Typeable
ограничения. При вызове такой функции вы ничего не можете вывести о том, что она может или не может делать со значениями Typeable
-ограниченного типа, потому что вы понятия не имеете, о каких типах она знает внутри.
В качестве тривиального примера о функции id :: a -> a
часто говорят, что мы можем точно сказать, что она делает, только по ее типу, потому что единственная возможная (полная) реализация функции с типом a -> a
— это возврат ее аргумента без изменений. Но если бы его тип был вместо Typeable a => a -> a
, мы могли бы очень мало рассказать о том, что он делает. Например, это справедливо:
fakeId :: Typeable a => a -> a
fakeId x = case typeOf x `eqTypeRep` typeRep @Bool of
Just HRefl -> not x
Nothing -> x
fakeId
возвращает большинство аргументов без изменений, но если он получает Bool
, он отменяет его. И снаружи невозможно сказать, что он проверяет Bool
и делает с ними что-то другое; с тем же успехом он мог бы иметь огромный список типов с особым поведением. Если мы тестируем то, что он делает, чтобы увидеть, соответствует ли он нашим требованиям, нет никакой гарантии, что мы найдем все типы, для которых он имеет особое поведение, поэтому мы легко можем получить ошибку в нашей окончательной программе.
Так что, хотя в Haskell есть эта система отражения, она не должна быть частью вашего «стандартного» набора инструментов. API, в котором у вас есть большое количество функций с ограничениями Typeable
, почти наверняка является плохим API; нам нужны ограничения, связанные со стиранием типов. 99% того, что вам нужно сделать, можно и нужно делать без размышлений.
Небольшое примечание: typeOf x `eqTypeRep` typeRep @Integer
также можно упростить до eqT @a @Integer
при условии, что мы добавим явный forall a.
в сигнатуру типа. Тем не менее, более короткий вариант делает менее очевидным, что мы используем тип x
.
Что именно вы спрашиваете? Каков вариант использования для этого? Шаблон Haskell имеет овеществление, если вы используете его только для макросов.