Back to Basics: Expressions and Types, Distilled
This week we begin our “Haskell Distilled” series. These articles will look at important Haskell concepts and break them down into the most fundamental parts. This week we start with the essence of what defines Haskell as a language.
Everything is an Expression
In Haskell, almost everything we write is an expression. This goes from the simplest primitive elements to the most complicated functions. Each of the following is an expression:
True
False
4
9.8
[6, 2]
\a -> a + 5
‘a’
“Hello”
This is, in part, what defines Haskell as a functional language. We can compare this against a more procedural language like java:
public int add5ToInput(int x) {
int result = x + 5;
return 5;
}
The method we’ve defined here isn’t a expression in the same way a function is in Haskell. We can’t substitute the definition of this method in for an invocation of it. In Haskell, we can do exactly this with functions! Each of the lines in this method are not expressions themselves either. Rather, each of them is a command to execute.
Even Haskell code that appears to be procedural in nature is really an expression!
main = do
putStrLn “Hello”
putStrLn “World”
In this case, main
is an expression of type IO ()
, not a list of procedures. Of course, the IO monad functions in a procedural manner sometimes. But still, the substitution property holds up.
Every Expression has a Type
Once we understand expressions, the next building block is types. All expressions have types. For instance, we can write out potential types for each of the expressions we had above:
True :: Bool
False :: Bool
4 :: Int
9.8 :: Float
[6, 2] :: [Int]
\a -> a + 5 :: Int -> Int
‘a’ :: Char
“Hello” :: String
Types tell us what operations we can perform with the data we have. They ensure we don’t try to perform operations with non-sensical data. For instance we can only use the add operation (+)
on expressions that are of the same type and that are numeric:
(+) :: (Num a) -> a -> a -> a
By contrast, in javascript, where we can "add" arbitrary variables (think [] + {}
) which can have non-sensical results.
Functions are Still Just Expressions
As we saw in the Java example above, methods are a little cumbersome. There is a distinction between a method definition and the commands we use for normal code. But in Haskell, we define our more complicated expressions as functions. And these are still expressions just like our most primitive definitions! Functions have types as well, based on the parameters they take. So we could have functions that take one or more parameters:
add5 :: Int -> Int
add5 x = x + 5
sumAndProduct :: Int -> Int -> Int
sumAndProduct x y = (x + y) * (x + y)
In our Java example, the statement return result
is fundamentally different from the method definition of add5ToInput
. But above, sumAndProduct
isn’t a different concept from the simpler expression 5
.
We use functions by “applying” them to arguments like so:
sumAndProduct 7 8 :: Int
We can also partially apply functions. This means we fix one of the arguments, but leave the rest of it open-ended.
sumAndProduct 7 :: Int -> Int
The result of this expression is a new function that takes only a single argument. It’s result will be the sumAndProduct
function with the first argument fixed as 7
. We've peeled off one of the arguments from the function type.
Conclusion
In the coming week’s we’ll continue breaking down simple concepts. We’ll examine how some of Haskell’s other syntactic constructs work within this system of expressions and types. If you’re new to Haskell, you should check out our Getting Started Checklist so you can start using these concepts!