Does Haskell support type reflection as in other languages such as Python?
This is different from Haskell How to print info of some function in haskell like "ghci> :info func"
Just to be clear, all of the special commands in GHCi that begin with a colon (like :type
, :info
, etc) are not part of the Haskell language. GHCi supports them by "wrapping around" actual Haskell, and keeping track of extra information. So none of those features are going to be accessible in Haskell itself. If there's something in Haskell that can meet a similar goal, we'll need to look for it in a totally different place.
Haskell is designed to have types completely erased during compilation.
Say we have a value like Just True
. When compiling Haskell source code, we know that is of type Maybe Bool
. But when turned into actual machine code, the code that produces that value will just allocate a little block of memory, with a small number and a pointer to the True
value. The number is used to tell whether the constructor was Nothing
or Just
. All the code that handles Maybe
values will have been compiled so that it uses the small number to tell whether it should take the branch for the Nothing
constructor or the Just
constructor; say the compiler chooses 0
for Nothing
and 1
for Just
.
But there's nothing in that little block of memory that says that this is a value from the Maybe
data type, nor that the pointer is to a value of type Bool
. Any other single-argument constructor will also be represented in memory as basically just a small number and a pointer, and some of them might even use the same number 1
that was being used for Just
. The number tags only need to distinguish other constructors from the same type; there's no global registry assigning unique numbers to ensure that different types don't use the same numbers.
So it actually isn't possible for compiled Haskell code to look at an arbitrary value and tell you what type it is. That information is just gone.
Haskell does support runtime reflection of types. But it's not available for just any value with no preparation; it's an opt-in system requiring you to explicitly prepare for wanting access to type information at runtime. Effectively it uses the type class system to ask the compiler to preserve information about types.
The Typeable
class is specially supported by the compiler. It is the class of types that can be represented and inspected at runtime. The class actually includes every type; the compiler automatically makes the instances for you. Putting a Typeable
constraint on your function means that the compiler will make the Typeable
methods available to you, keeping enough information available at runtime that you can ask "what is the type of this value?". If you don't have a Typeable
constraint on a type variable, then that information isn't kept available at runtime and you can't ask for the type. (This also means that the Typeable
constraints need to be added to every function up the call stack until the point where the type variable was actually instantiated with a concrete type; if your callers haven't opted-in to runtime type reflection, then you can't unilaterally get it back)
The way you actually use it is that you can use typeOf x
to produce a value of type TypeRep a
(where a
is the type of x
). e.g. typeOf True
gives you a TypeRep Bool
, and typeOf (Just 'a')
gives you a TypeRep (Maybe Char)
, etc. If you need to use this you probably don't know what the type variable a
actually is, but you can then use eqTypeRep
to test whether your TypeRep
is equal to the TypeRep
of some other known type, and if it is you now know that x
is of that type and can call other functions on it that are specific to that type. You can't simply use basic ==
tests to test if it's equal to a known type since that only gives you True
or False
, which don't prove anything to the compiler; it needs to look a little more complicated, but basically boils down to this simple idea. It could look something like this:
{-# 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
Inside the Just HRefl
arm of the case
, the compiler knows that x
is of type Integer
, so it's valid to add 17
and to return that as the result of the function (which must be something of type Integer
). In the Nothing
arm the compiler will not allow you to use Integer
functionality on x
or return it, so we have to return something else we know is an Integer
instead.
The rest of the functionality in the Type.Reflection
allows you to do some more flexible inspections (e.g. you could test whether a value's type is Maybe
applied to something, without caring about what type it's applied to). But ultimately it boils down to this ability to take a value whose type is a type variable and make a finite number of "guesses" about its type. If any of your guesses is correct you can act on that information, but there will always be the possibility that it isn't any of the types you're specifically checking for and you still have to have a branch where it's just a black box value (though you can always error
out if you don't care about your function being total).
This circumvents the normal restrictions on values whose type is a variable; without Typeable
you can't do anything at all with it other than pass it on to something else expecting a value of the same type variable (such as a matching function that's been passed in, or a type class constrained function if you have available constraints on the variable).
One thing that might not have been obvious from the above is that we can't reflect polymorphism. That is, there's no way to get a TypeRep
that itself reflect a type containing variables. If a function takes a [a]
that a
type variable will be instantiated on each call to some particular type, and the TypeRep
will end up reflecting the particular type that was used in this particular call (like [Integer]
or [Maybe Bool]
or [Char -> Maybe (IO String)]
; it will not reflect the polymorphic type [a]
.
A large amount of Haskell's usefulness for programming actually comes from the restrictions on what you can do with values whose types contain variables. When you get used to it, knowing what a function can't do based purely on its type is very powerful.
That goes completely out the window when there are Typeable
constraints. When calling such a function, you can't deduce anything about what it can or can't do with values of the Typeable
-constrained type, because you have no idea what types it knows about internally.
As a trivial example, it's often said of the function id :: a -> a
that we can tell exactly what it does just from its type, because the only possible (total) implementation of a function with type a -> a
is to return its argument unchanged. But if its type were instead Typeable a => a -> a
, we could tell very little about what it does. For example, this is valid:
fakeId :: Typeable a => a -> a
fakeId x = case typeOf x `eqTypeRep` typeRep @Bool of
Just HRefl -> not x
Nothing -> x
fakeId
returns most arguments unchanged, but if it receives a Bool
it negates it. And there's no way to tell from the outside that it's checking for Bool
s and doing something different with them; it could just as well have a huge list of types with special behaviour. If we're testing what it does to see if it meets our requirements, there's no guarantee that we'll find all the types for which it has special behaviour, so we could easily end up with a bug in our final program.
So while Haskell has this reflection system, it really should not be part of your "standard" toolkit. An API where you have large numbers of functions with Typeable
constraints is almost certainly a bad API; we want the restrictions that come with type erasure. 99% of the things you need to do can and should be done without reflection.