Search code examples
haskelltypesreflection

Haskell support for Type Reflection


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"


Solution

  • 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.

    Type erasure

    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.

    Reflection

    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).

    Polymorphism

    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 word of warning

    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 Bools 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.