Skip to content

Instantly share code, notes, and snippets.

@jeyj0
Created September 7, 2020 17:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeyj0/a2fcd8a94931a7f3e9febccd1e6d8dec to your computer and use it in GitHub Desktop.
Save jeyj0/a2fcd8a94931a7f3e9febccd1e6d8dec to your computer and use it in GitHub Desktop.
Haskell Crash Course

Haskell Crash-Course

Hello, Haskell!

putStrLn

putStrLn "Hello, Haskell!"

with a main function

main = putStrLn "Hello, Haskell!"
main

type of the main function

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.

Functions

With one argument

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.

With two arguments

concat :: String -> String -> String
concat a b = a ++ b
concat "Hello" "Concat"

Functions are just values

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.

Infix functions

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.

Creating a custom infix function

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

Using any function as an infix function

You can use any function as an infix function by surrounding its name with two `:

add = (+)
4 `add`

Pattern matching

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.

(as case .. of)

isEmptyList :: [a] -> Bool
isEmptyList list =
  case list of
    [] ->
      True
    _ ->
      False

Some Stuff

No null

Use the Maybe monad instead (I’ll show you in a bit)

What about undefined?

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"

Tuples and Records

Tuples

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.

The empty tuple

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

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

Variables

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.

(in a do-block)

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

If-Then-Else

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

Loops

Use recursion instead

Monads

The Maybe monad

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.

The IO monad

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.

do and “getting values out of monads”

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.

other stuff

Modules / Imports

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

The $ function

Ignore the fact that this is a function. This is just a way to avoid writing as many parenthesis. If you would have code like this:
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.

Order of arguments

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:

Piping

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.

What’s the type of (.)?

(.) :: (b -> c) -> (a -> b) -> a -> c

Lambdas

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

Formatting

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"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment