Type Application
Introduction#
TypeApplications
are an alternative to type annotations when the compiler struggles to infer types for a given expression.
This series of examples will explain the purpose of the TypeApplications
extension and how to use it
Don’t forget to enable the extension by placing {-# LANGUAGE TypeApplications #-}
at the top of your source file.
Avoiding type annotations
We use type annotations to avoid ambiguity. Type applications can be used for the same purpose. For example
x :: Num a => a
x = 5
main :: IO ()
main = print x
This code has an ambiguity error. We know that a
has a Num
instance, and in order to print it we know it needs a Show
instance. This could work if a
was, for example, an Int
, so to fix the error we can add a type annotation
main = print (x :: Int)
Another solution using type applications would look like this
main = print @Int x
To understand what this means we need to look at the type signature of print
.
print :: Show a => a -> IO ()
The function takes one parameter of type a
, but another way to look at it is that it actually takes two parameters. The first one is a type parameter, the second one is a value whose type is the first parameter.
The main difference between value parameters and the type parameters is that the latter ones are implicitly provided to functions when we call them. Who provides them? The type inference algorithm! What TypeApplications
let us do is give those type parameters explicitly. This is especially useful when the type inference can’t determine the correct type.
So to break down the above example
print :: Show a => a -> IO ()
print @Int :: Int -> IO ()
print @Int x :: IO ()
Type applications in other languages
If you’re familiar with languages like Java, C# or C++ and the concept of generics/templates then this comparison might be useful for you.
Say we have a generic function in C#
public static T DoNothing<T>(T in) { return in; }
To call this function with a float
we can do DoNothing(5.0f)
or if we want to be explicit we can say DoNothing<float>(5.0f)
. That part inside of the angle brackets is the type application.
In Haskell it’s the same, except that the type parameters are not only implicit at call sites but also at definition sites.
doNothing :: a -> a
doNothing x = x
This can also be made explicit using either ScopedTypeVariables
, Rank2Types
or RankNTypes
extensions like this.
doNothing :: forall a. a -> a
doNothing x = x
Then at the call site we can again either write doNothing 5.0
or doNothing @Float 5.0
Order of parameters
The problem with type arguments being implicit becomes obvious once we have more than one. Which order do they come in?
const :: a -> b -> a
Does writing const @Int
mean a
is equal to Int
, or is it b
?
In case we explicitly state the type parameters using a forall
like const :: forall a b. a -> b -> a
then the order is as written: a
, then b
.
If we don’t, then the order of variables is from left to right. The first variable to be mentioned is the first type parameter, the second is the second type parameter and so on.
What if we want to specify the second type variable, but not the first? We can use a wildcard for the first variable like this
const @_ @Int
The type of this expression is
const @_ @Int :: a -> Int -> a
Interaction with ambiguous types
Say you’re introducing a class of types that have a size in bytes.
class SizeOf a where
sizeOf :: a -> Int
The problem is that the size should be constant for every value of that type. We don’t actually want the sizeOf
function to depend on a
, but only on it’s type.
Without type applications, the best solution we had was the Proxy
type defined like this
data Proxy a = Proxy
The purpose of this type is to carry type information, but no value information. Then our class could look like this
class SizeOf a where
sizeOf :: Proxy a -> Int
Now you might be wondering, why not drop the first argument altogether? The type of our function would then just be sizeOf :: Int
or, to be more precise because it is a method of a class, sizeOf :: SizeOf a => Int
or to be even more explicit sizeOf :: forall a. SizeOf a => Int
.
The problem is type inference. If I write sizeOf
somewhere, the inference algorithm only knows that I expect an Int
. It has no idea what type I want to substitute for a
. Because of this, the definition gets rejected by the compiler unless you have the {-# LANGUAGE AllowAmbiguousTypes #-}
extension enabled. In that case the definition compiles,it just can’t be used anywhere without an ambiguity error.
Luckily, the introduction of type applications saves the day! Now we can write sizeOf @Int
, explicitly saying that a
is Int
. Type applications allow us to provide a type parameter, even if it doesn’t appear in the actual parameters of the function!