putStrLn "Hello, Haskell!"
main = putStrLn "Hello, Haskell!"
main
Type annotations in haskell are done like this:
main :: IO ()
We can verify this is true by using :t
on main
in ghci:
:t main
However, the brackets aren’t there to define the arguments of the main function. Instead, they define what’s “inside” the IO
monad. Don’t worry about that yet, though. You simply have to understand that IO ()
is one value, or a function without arguments.
Defining a function with an argument is simple though:
print :: String -> IO ()
print s = putStrLn s
print "Hi, there!"
This code first defines the type of the print
function, which means it takes a String
as its first argument, then defines the output type, which is the IO
monad without a value again.
concat :: String -> String -> String
concat a b = a ++ b
concat "Hello" "Concat"
add x y = x + y
add2 = add -- t: add2 == t: add
add 1 2
In this case, add 5
returns a function that takes one less argument. It’s the same function as add
, only with the first argument already set to 5
.
The symbol +
is also just a function. However, it’s an infix function, which means it can be placed between its two arguments. You can use it like a normal function, if you surround it with parenthesis:
(+) 10 20
Any function which has a name that only consists of symbols is an infix function. Therefore (++)
is also an infix function.
Note that I referenced the two +
signs-function (string concatenation) with parenthesis around the signs. That’s how infix functions are usually referenced when writing about them.
Let’s create our own infix function, simply by copying the addition-function (+)
into the new name (/|\)
, just because we can:
(/|\) x y = (+) x y
1 /|\ 9
Of course I could have omitted the x
and y
, however I want to show you something else. Notice how we have to reference (/|\)
with the parenthesis when we declare it? Well, that’s because we’re not using it as an infix function there. We could however just as well do this:
x /|\ y = (+) x y
13 /|\ 17
You can use any function as an infix function by surrounding its name with two `
:
add = (+)
4 `add`
Functions can have more than one definition. Instead of doing conditionals on the content of arguments, you can instead pattern match on them and do different things based on them. It’s basically like the case .. of
syntax without the case .. of
.
isEmptyList :: [a] -> Bool
isEmptyList [] = True
isEmptyList _list = False
There’s much more advanced things you can do, but instead of listing it all here, let’s just talk about that when we could use it, or just duckduckgo it.
isEmptyList :: [a] -> Bool
isEmptyList list =
case list of
[] ->
True
_ ->
False
Use the Maybe
monad instead (I’ll show you in a bit)
Haskell does actually have undefined
. However, it’s not a value that can be passed around. It is a value that satisfies any types, however once it is actually used it’ll throw an exception. It’s useful to mark something as a TODO and have it compile:
complexFunctionIllWriteLater :: String -> String
complexFunctionIllWriteLater = undefined
complexFunctionIllWriteLater "Hi"
We can return more than one value from a haskell function by putting them in a tuple. A tuple is an ordered fixed-size collection of values of which each can have a different type.
Tuples use parenthesis:
someFn :: String -> Bool -> (String, Bool)
someFn s b = (s, b)
This function takes a String
and a Bool
and returns them wrapped in a tuple.
You can access elements from a tuple using the fst
function (which returns the first element), the snd
function (which returns the second element) or pattern matching.
There’s also the empty tuple ()
which is used to say “this is never anything”. It’s what the main
function returns inside of the IO
monad.
Records are like tuples in that they hold a fixed amount of values each with separate types, only that each has a name as well. You can sort-of see them as objects.
To define a new record type, use the data
keyword:
:{
data MyRecord = MyRecord
{ name :: String
, isCool :: Bool
}
:}
You can then create a new value like this:
:{
john :: MyRecord
john = MyRecord
{ isCool = True
, name = "John"
}
:}
Note that name
and isCool
are functions registered in the module’s scope to access values from the record, so you can’t have two records with fields with the same name in one module.
johnsName :: String
johnsName = name john
If you want it to look a bit closer to john.name
, you can use the (&)
function - I would recommend getting used to name john
though, unless there’s another reason to use (&)
.
johnsName' :: String
johnsName' = john & name
Haskell is purely functional. This means there’s no statements. Variable declarations are usually side-effects. So how do you handle variables?
Well, first of: you need far less variables than you would in other languages - the only reason to really use them is when indentation becomes unbearable (at which point you usually need a function anyways though) or when you’d repeat yourself.
Variables are nothing else than functions with no arguments:
myString :: String
myString = "This is some text."
You can get scoped variables too, though - there’s two ways to define them:
main :: IO ()
main =
let
string1 :: String
string1 = "I am the first string."
in
putStrLn (string1 ++ " " ++ string2)
where
string2 :: String
string2 = "I am the second string."
There’s no runtime difference between the two - choose the one that makes the code more readable.
While in a do
-block, variable definitions look like this:
main :: IO ()
main = do
let stringWithoutTypeDeclaration = "Hello"
let stringWithType :: String
stringWithType = "I have a type."
putStrLn (stringWithoutTypeDeclaration ++ stringWithType)
This looks a lot like imperative programming. Remember though that haskell is lazy-evaluated, meaning that the values of the variables are only determined once they are required/used (in this case in the last line of the snipped).
Since everything is an expression in haskell, all if
require else
. It looks like this:
result =
if predicate then
expressionIfPredicateIsTrue
else if p2 then
expressionIfPredicateIsFalse
else
ex3
-- note: this formatting requires the DoAndIfAndElse language-extension
-- in some situations. Since I like it, I have that lang.-ext. active
Use recursion instead
This monad is used whenever a value might be there, but isn’t necessarily (basically when other languages would sometimes have a null
, sometimes the actual value).
Don’t be afraid of the term “monad”.
To simplify things, imagine a monad like a box. You can put things in boxes in the same way you can put things in monads. However, monads are a bit more selective about it.
The Maybe
monad is defined as:
data Maybe a
= Just a
| Nothing
Ignore the a
for now.
The data
is a way to define a new type in haskell. There’s also type
and newtype
, but I’ll mention those later.
So, we’re defining a new type called Maybe
. We ignore the a
, then there a =
to say “here comes the definition”, followed by Just a
and Nothing
, separated with |
.
This means this is a type called Maybe
that is either Just
, or Nothing
. That’s it.
Back in the box-analogy, this means we either have a Just
-box, or a Nothing
-box.
Now for the a
. a
is a type argument. When we use the Maybe
monad, we can define what type of thing the Just
-box should let in. For example, it could be a String
:
x :: Maybe String
x
would now either be a Just String
, or Nothing
. This is the equivalent of a Java variable of type String
- which could also be null. In Haskell however, this is defined in the type-system, which means it can be checked at compile time! That means there’s no possibility for something like a NullPointerException
.
To access the value in the Maybe
, there’s a special syntax: case .. of
.
printIfValue :: Maybe String -> IO ()
printIfValue Nothing = putStrLn "..."
printIfValue (Just value) = putStrLn ("The value..." ++ value)
printIfValue maybeValue =
case maybeValue of
Nothing ->
putStrLn "There's nothing in maybeValue"
Just value ->
putStrLn ("The value of 'value' is" ++ value)
There’s other ways too, however I won’t mention them here./
This is a form of pattern matching. If maybeValue
is Nothing
, ~”There’s nothing in maybeValue”~ will be printed. However, if maybeValue
is Just String
, the second branch will be executed, with the value carried in the String
now in the variable value
.
Whenever you want to do something with the outside world, you will have to use the IO
monad. Once you have a value “in” the IO
monad, you won’t be able to get it “out”. However, you can work inside the IO
monad as well. This way the typesystem can ensure what function do something to the outside world and which can’t.
That’s also the reason the main
function has the type signature:
main :: IO ()
The ()
is the empty type, which means no value. By being wrapped in the IO
monad, the main function can actually do something. If it wasn’t wrapped in it, it would also not be able to call another function that requires IO
.
However, it is no problem to call a function that does not require IO
in an IO
function.
Sometimes you want to “get the value out of the monad”. That’s however not really possible. Instead, what you do is you give the monad a function which is should apply to it’s value, and you will get back the transformed value inside the same monad type.
That would look like this:
main :: IO ()
main = getArgs >>= doSomethingWithArgs
This code reads like “get me the arguments passed to this program, then pass them to doSomethingWithArgs”.
For such a simple case this reads well and looks very clean and concise. For longer functions this can get ugly, especially when the function is defined inline as a lambda.
An alternative is using do
:
main :: IO ()
main = do
args <- getArgs
return (doSomethingWithArgs args)
This is the exact same thing, with a bit of syntactic sugar.
It reads more like imperative languages though. Here, a new variable called args
is introduced, into which the “carried” [String]
from the IO [String]
return value of getArgs
is put. Now we have direct access to that value and can use it as we would use any other old [String]
.
Note that the last line of a do
block is what is returned from it. do
can also only be used inside a monad. If you want to return a non-monadic value from a do
-block, you have to return
(pure
works too) it:
myFn = do
args <- getArgs
return args
Note that you can actually put something like statements in a do
-block. They’re still expressions, but you can choose to ignore the result:
main :: IO ()
main = do
_ <- myIOFunction
return ()
Here, myIOFunction could return a IO String
and we simply ignore the value and return the empty tuple instead.
-- this belongs at the top of Main.hs
module Main where
import System.Exit -- imports all of System.Exit in global namespace
import qualified Chrono.TimeStamp as CTS -- imports into CTS namespace
import System.Environment (getArgs) -- only imports getArgs into global ns
x = f1 (f2 arg1 (f3 arg2 arg3 (f4)))
You can easily replace every starting parenthesis that ends at the very end (in the example above all of them) with a $
and ignore the ending parenthesis:
x = f1 $ f2 arg1 $ f3 arg2 arg3 $ f4
This is much easier to type, and after a while it’s also much easier to read.
You can’t replace a parenthesis like this:
y = f1 (a + b) c
The parser wouldn’t know to place the closing parenthesis after the b
. Instead, it would interpret a $
like this:
-- this:
-- y = f1 $ a + b c
-- is parsed as
y = f1 (a + b c)
That code likely wouldn’t compile though, so that’s good. The error in this case sadly isn’t very easy to read though, so if I have a very weird type error in a place where I’ve used $
, I tend to just replace it with plain old parenthesis.
The order of arguments in haskell is usually pretty different from what what you’d expect (at least I did). This has a very good reason though.
Let’s look at the map
function as an example:
map :: (a -> b) -> [a] -> [b]
What the function does is it takes a transformer function as a first argument. This function takes a value of type a
and turns it into a value of type b
. As a second argument it takes a list of values of type a
and then finally returns a list of values of type b
.
It’s the same thing as for example JavaScript’s map: it takes a list of values and transforms them in some way. However, in JS you’d see:
myValues.map((v) => v + 1)
Intuitively, I’d have expected haskell’s map
to look like this, because of it:
map :: [a] -> (a -> b) -> [b]
-- sidenote:
-- this is the type of the ~for~ function, which is used less often
The first form has a few advantages though:
Using the (&)
function you can pipe a value through multiple functions to reach the final value.
So instead of doing:
transformThroughFiveSteps value =
lastStep $ fourthStep $ thirdStep $ secondStep $ firstStep value
You can do this, which reads in the order in which functions are applied:
transformThroughFiveSteps value =
value
& firstStep
& secondStep
& thirdStep
& fourthStep
& lastStep
In both of these cases, firstStep
to lastStep
are functions which take the value they are working on as a last argument. If map
were in there, it’d fit right in:
anotherTransform value =
value
& doThis
& (map transformFn)
& lastStep
If map
’s arguments were the other way around, you’d have to use a lambda to do this.
In the above cases, there’d have been another solution still. We can define a pipeline of functions using the (.)
function. It also requires the last argument to be the one to transform though, but doesn’t read in-order either:
anotherTransform v = (lastStep . (map transformFn) . doThis) v
We can leave out the value
argument, because the pipeline will just return a function that takes one argument, which will be of the correct type.
(.) :: (b -> c) -> (a -> b) -> a -> c
Lambdas are anonymous functions. In haskell, they look like this:
myFn = \arg1 arg2 arg3 -> if arg1 then arg2 else arg3
-- is the same as
myFn arg1 arg2 arg3 = if arg1 then arg2 else arg3
You’ll often see lists (among other things) declared like this:
stringList =
[ "First entry"
, "Second entry"
, "Third entry"
]
This is for easy editing. except for when you change the first item in the list, this also has the advantage of much nicer (git-)diffs.
There’s also a form for type
declarations:
type MyType
= FirstOption
| SecondOption String
| ThirdOption Bool
type YellowState
= Blinking
| BeforeRed
| BeforeGreen
type TrafficLight
= Green
| Yellow YellowState
| Red String
type Yellow = "blinking" | "beforered" | "beforegreen"
type TrafficLight = "green" | Yellow | "red"