Everything in Haskell originates in a data constructor from some definition of a type.
Because Haskell is an implementation of a pure lambda calculus, Haskell uses type systems.
Type systems in mathematics and logic are designed to impose constraints that enforce correctness.
Well-designed type systems help eliminate some classes of errors as well as concerns such as what the effect of a condition over a non-Boolean value might be.
Consider the Bool
type. It is a set of two inhabitants: True
and False
. Anytime a True
or False
occurs in a Haskell program, the typechecker will automatically know that they are members of the Bool
type.
In addition, whenever the type Bool
is declared in a type signature, the compiler will automatically expect one of those two values. You will get a type error if you try to pass anything other than True
or False
.
Haskell has a static typing system, which means that typechecking occurs at compile time. This is handy because various errors will be caught before you try to execute your program. Runtime errors still exist, but the type system reduces the number and kinds of errors that you may receive.
Good type system implementations can also offer compiler optimizations. This is because the compiler can know certain things about the program based on the types.
Types also may serve as documentation for your program.
Some may prefer dynamic typing (no types) over static typing, but the upfront cost of static typing pays off in the end. Static typing offers safer code that is easier to maintain.
When we query the types of values, we get useful information:
Prelude> :type True
True :: Bool
Prelude> :type "Clayton"
"Clayton" :: [Char]
Prelude> :type 'C'
'C' :: Char
When we query the types of numeric values, we see typeclass information instead of a concrete type, because the compielr doesn't know the specific numeric type until it is either declared or the compiler infers it:
Prelude> :type 5
5 :: Num p => p
We can make it more explicit by declaring the type:
Prelude> let x = 5 :: Integer
Prelude> :t x
x :: Integer
The Function Type
(->)
, is the type constructor for functions in Haskell.
It's similar to Bool
in that it's a type constructor. The difference is that it takes arguments and has no data constructors.
Prelude> :info (->)
data (->) (a :: TYPE q) (b :: TYPE r) -- Defined in ‘GHC.Prim’
infixr -1 ->
Functions are values.
A function can be applied, and the structure of this type demonstrates this. The arrow function is an infix operator that has two parameters and associates to the right. We know this by infixr
.
Let's break down the function fst
:
fst :: (a, b) -> a
-- [1] [2][3]
(a, b)
.(->)
with two parameters. One is (a, b)
and the other is result a
.a
. It's the same a
that is in the tuple (a, b)
.We know that it's the same a
because the output type a
must be the same type as the input type a
. Nothing happens here that could transform the a
into any other value of that type.
Let's break down the function length
:
Prelude> :type length
length :: [a] -> Int
The length function takes one argument that is a list. It returns an Int
result. It does not care what types are inside of that list.
Typeclass Constrained Type Variables
Let's look at the types of some arithmetic functions:
Prelude> :type (+)
(+) :: Num a => a -> a -> a
Prelude> :type (/)
(/) :: Fractional a => a -> a -> a
Additon takes one numeric argument and adds it to a second numeric argument of the same type, then it returns the result.
Division takes one fractional argument, divides it by a second fractional value, and returns a fractional value as a result.
Notice that they are constrained by a typeclass. Each typeclass offers a standard set of fucntions that can be used across several concrete types.
We say that this functions are constrained because we don't know the concrete type of a
. However, we do know that it can only be one of the types that includes the required typeclass instance.
We can do some cool stuff with numbers knowing this:
Prelude> let five = 5
Prelude> :t five
five :: Num p => p
Prelude> let fiveInt = five :: Int
Prelude> let fiveDouble = five :: Double
Prelude> :t fiveInt
fiveInt :: Int
Prelude> :t fiveDouble
fiveDouble :: Double
We changed a Num p => p
to Int
and Double
. This is possible because Int
and Double
have an instance of the Num
typeclass:
Prelude> :info Num
-- Stuff removed
instance Num Int -- Defined in ‘GHC.Num’
instance Num Double -- Defined in ‘GHC.Float’
Since they both have instances of Num
, they can use the operations from Num
like addition or subtraction.
In Haskell, all functions take in one argument and return one result. Haskell has syntactic sugar that construct curried functions by default.
Currying refers to the nesting of multiple functions, each accepting one argument and returning one result. This allows for an illusion of multiple parameter functions.
Let's review the datatype definition of arrows again.
data (->) a b
In order to have a function, you must have one input, the a
, to apply the function to, and you'll get one result back, the b
.
Each arrow in a type signature represents one argument and one result, with the final type being the final result.
Let's break this down by looking at the type signature for addition again because this function requires multiple inputs.
(+) :: Num a => a -> a -> a
| 1 || 2 |[3]
a
must have an instance of Num
. Addition is defined in the Num
typeclass.Remember that (->)
is right associative. Because of this, types are implicitly parenthesized:
f :: a -> a -> a
-- associates to
f :: a -> (a -> a)
Partial Application
What is the practical value of currying?
Partial application explains the value of currying.
addingStuff :: Integer -> Integer -> Integer
addingStuff a b = a + b + 5
addingStuff
appears to take one argument, but if you loading it in GHCi, you can see that it is taking one argument and returning a function that takes one argument and returns one result.
*Currying> :type addingStuff
addingStuff :: Integer -> Integer -> Integer
*Currying> addingTwo = addingStuff 2
*Currying> :type addingTwo
addingTwo :: Integer -> Integer
*Currying> twelve = addingTwo 5
*Currying> twelve
12
Here, twelve is equal to addingStuff 5 2
because addingTwo
is equal to addingStuff 2
.
This lets us reuse addingStuff
and create new functions from it with one of the arguments applied.
Manual currying and uncurrying
Haskell curries by default. However, you can uncurry functions. You can unnest functions and replace the two functions with a tuple of two values (the two values you want to use as arguments).
If you uncurry (+)
, the type changes from Num a => a -> a -> a
to Num a => (a, a) -> a
. This can be useful as it appears more like "takes two arguments, returns one result".
module Currying where
throwAway :: Bool -> Integer
throwAway False = 10
throwAway True = 20
curriedFunc :: Integer -> Bool -> Integer
curriedFunc i b = i + throwAway b
uncurriedFunc :: (Integer, Bool) -> Integer
uncurriedFunc (i, b) = i + throwAway b
anonymousFunc :: Integer -> Bool -> Integer
anonymousFunc = \i b -> i + throwAway b
anonymousNestedFunc :: Integer -> Bool -> Integer
anonymousNestedFunc = \i -> \b -> i + throwAway b
These are all the same function, giving the same results. You can substitute the backslash with lambda if you would like to compare this with the lambda calculus.
Sectioning
Sectioning specifically refers to partial application of infix operators, which has a special syntax and allows you to choose wehther the argument you're partially applying the operator to is the first or second argument.
Prelude> let x = 5
Prelude> let y = (3^)
Prelude> let z = (^3)
Prelude> y x
243
Prelude> z x
125
Polymorph is a word of recent creation. It was invented in the 19th century from the Greek words poly for "many" and morph for "form". The -ic suffix means "made of". So, all together, polymorphic means "made of many forms". This is in constrast with monomorphic or "made of one form".
Polymorphic type variables allow us to implement expressions that can accept arguments and return results of different types without having to write variations on the same expression for each type.
In Haskell, polymorphism is divided into two categories: parametric polymorphism and constrained polymorphism.
Constrained polymorphism is implemented with typeclasses in Haskell.
Parametric polymorphism is broader than constrained polymorphism. Parametric polymorphism refers to type variables, or parameters, that are fully polymorphic.
If you see a lowercase name in a type signature, it is a type variable and polymorphic (like a, t, etc.).
If you see an uppercase name in a type signature, it is a specific, concrete type such as Int
, Bool
, etc.
Let's consider a parametrically polymorphic function: identity.
The id
function comes with the Prelude
and is called the identity function because it is the identity for any value in Haskell. Passing any value to id
will return the same value:
id :: a -> a
For all a, get an argument for some type a, and return a value of the same type a.
Again, it works with any type of data.
Prelude> id 5
5
Prelude> id "Clayton"
"Clayton"
Prelude> id 'C'
'C'
A function is polymorphic when its type signature has variables that can represent more than one type.
Parametric polymorphism is full polymorphism. It is not constrained by a typeclass.
Parametricity is the property we get from having parametric polymorphism. Parametricity means that the behavior of a function with respect to the types of its arguments is uniform.
Haskell does not make us assert a type for every expression or value because it has type inference.
Type inference is an algorithm that determines types of expressions.
Haskell will infer the most generally applicable (polymorphic) type that is still correct.
An example of Haskell inferring types:
Prelude> let greeting str = str ++ " Clayton!"
Prelude> greeting "Hello"
"Hello Clayton!"
Prelude> :t greeting
greeting :: [Char] -> [Char]
The type inference is able to explicitly infer the type signature because of the function (++)
. It has one value to work with already and it knows that it is a String so it can easily infer this.
Replacing the string value with another variable:
Prelude> let greeting str str2 = str ++ str2
Prelude> greeting "Hello " "Clayton!"
"Hello Clayton!"
Prelude> :t greeting
greeting :: [a] -> [a] -> [a]
It was not able to infer the types because it has no information by which to do so. Thus, it used a polymorphic type signature.
It is considered best practice to explicitly state the types.
It can be self documenting and can help the compiler give you information about where your code is going wrong.
Type signatures can also help other programmers trying to use your code read it and figure out what it does.
Learn You a Haskell - http://learnyouahaskell.com/types-and-typeclasses
Haskell Book - https://haskellbook.com/