Home Hello Haskell, language introduction and cheat sheet
Post
Cancel

Hello Haskell, language introduction and cheat sheet

Haskell is a statically typed pure functional programming language. This is more of a cheat sheet for basic haskell syntax and constructs. I will not describe in any details what is pure functional programming or what is static typing. Scala, Kotlin and now Java are a mixed paradigm programming languages, which are mixture of OOP with Functional programming. If you have used Scala or a modern version of Java or Kotlin, you should find familiar constructs. The only thing you will not find , is classes , objects or any other OOP constructs. You will find something called Type Classes that has nothing to do with classes in OOP, you can think of it as type categories or the closest OOP thing to it is interfaces.

This is also not a tutorial, which means, the sections are not ordered.

Getting Started

The easiest way to get started with Haskell is to install Haskell Platform which includes a compiler, a build system, analysis and profiling tools, most commonly used packages among others.

After installing Haskell Platform, you can start the Glasgow Haskell Compiler in interactive mode by running ghci. Once run it will great you with Prelude> prompt. Prelude is a library of functions that are preloaded for you to use.

The Interactive Mode error messages are clear and very useful. For example, I tried 1 != 2 to check for inequality. I got a detailed error message, not only telling me that != doesn’t exist “in scope”, but also suggesting using /= for inequality.

1
2
3
4
5
<interactive>:8:3: error:
    • Variable not in scope: (!=) :: Integer -> Integer -> t
    • Perhaps you meant one of these:
        ‘>=’ (imported from Prelude), ‘==’ (imported from Prelude),
        ‘/=’ (imported from Prelude)

Useful commands in Interactive mode

  • :quit or :q to exit
  • :? for help
  • :set prompt "ghci> " Change the default prompt from Prelude to ghci
  • :set -Wall Show all warnings. Useful for non-exhaustive pattern matching
  • :! to run bash command
    • :! clear to clear screen under Linux
  • :l filename.hs to load a haskell file into the active session.
  • :info or :i gives information about a given “name”, if it is an operator for example it may yield its precedence and associativity.
  • To write a block of code we surround it by :{ and :}
  • :browse The list of type signatures and functions loaded from the current module. We can also use :browse ModuleName for a specific module
  • :sprint that is s-print. It prints a value without forcing its evaluation.

Basic Syntax

Naming Conventions

  • Function names are camelCased
  • Types are PascalCased

Bits and pieces

  • Indentation: Code which is part of some expression should be indented further than the beginning of that expression. Indentation Summary
  • comments start with --
  • String is treated like a list of characters, thus we can h:"ello world"
  • To give a value an explicit type we use ::, e.g. 5 :: Int
    • Assigning a value to variable with explicit type would be something like a = 5 :: Int

Basic IO

  • We have print, putStrLn and putStr to print to the console.
    • putStr :: String -> IO () displays strings to console. That is, It only takes arguments of type String
    • putStrLn :: String -> IO () Similar to putStr with a new line at the end
    • print :: Show a => a -> IO (), similar to putStrLn but works with other types. It prints the String representation of any type, given that the type implements Show (Think of Java’s toString)
  • IO () means an input/output action (side effect) that has no meaningful return type denoted by unit ()

Functions

  • functions can be called without parenthesis or commas even with multiple arguments. For example max 1 2 would yield 2 as expected.
  • Prefix functions can be turned into infix functions by wrapping them with a backtick. So we can do 1 max 2
  • Infix functions can be turned into prefix functions by wrapping them with parenthesis (+) 1 2
  • You can do max 1 2 + 3 which is same as (max 1 2) + 3. Please, use some form of the latter, don’t depend on precedence rules, for the sake of mere mortals like myself, and yourself few weeks down the road.
  • to define a function doubleMe x = x + x.
  • $ operator is the lowest in precedence. It allows its right side to be evaluated before the left.
  • The -> is the type constructor for functions in Haskell. It takes arguments and has no data constructor. functions are values
  • Functions take one argument and return one result.
    • Multi-argument functions are a series of curried functions
    • We partially apply the function to the first argument, returning another function that is partially applied to the second and so forth.
    • Uncurrying a function would be to turn the curried application into using a tuple for arguments. That is turning a -> a -> a to (a, a) -> a
    • currying a function is opposite of uncurrying. That is turning (a, a) -> a to a -> a -> a
    • We can use curry and uncurry to do exactly that to functions. Either will return a new function that is curried or uncurried accordingly.
    • When we apply a function to some value, we say that the function parameter is bound to its argument

Lambdas (Anonymous Functions)

  • Lambdas are useful for example if we are passing a predicate to a filter or a function to a map for example
  • \ x -> x * 2 is a lambda, that can be used for example as map (\ x -> x * 2) lst.
  • \ is basically λ as both symbols look similar, the former is easier to type

Section of an infix operator

  • Special syntax for partial application of infix operators
  • For example ^
    • (2^) left section is equivalent to \x -> 2 ^ x
    • (^2) right section is equivalent to \x -> x ^ 2
  • This becomes interesting: containedInList = (elem list), which now tests if an element belongs to a list, which saves us an intermediate step to switch arguments explicitly. Note we changed the prefix function elem to infix by surrounding it by backticks
    1
    2
    3
    4
    
    rightSection = (++ " end")
    leftSection = ("start " ++)
    rightSection "hello"  -- Results in: "hello end"
    leftSection "hello"   -- Results in "start hello"
    

Composition

  • we use . higher order function to compose two functions
  • (.) :: (b -> c) -> (a -> b) -> a -> c
    1
    2
    3
    4
    
    f1 x = "(f1 " ++ x ++ ")"
    f2 x = "(f2 " ++ x ++ ")"
    f = f1 . f2
    f "x"   -- Result in "(f1 (f2 x))"
    
  • In other words (f1 . f2) x = f1(f2(x))

Useful functions

  • show converts a some type to a String
  • read converts a string into some other type
  • flip reverses the parameters of two argument functions.
  • catMaybes :: [Maybe a] -> [a] flattens a list of Maybes

Pattern Matching

  • Pattern matching can be done simply by declaring the function definition then the list of possible matches.
    1
    2
    3
    
    factorial :: (Integral a) => a -> a  
    factorial 0 = 1  
    factorial n = n * factorial (n - 1)  
    

    We can pattern match against Tuples as well

    1
    2
    
    multiple :: (Integral a) => (a, a) -> a
    multiple (a1, a2) = a1 * a2
    

    That is opposed to multiple a =(fst a) * (snd a) Pattern matching against lists would be

  • [] for empty list
  • (a1:a2:[]) or [a1, a2] first two elements of two elements list
  • (a1:a2:_) first two elements for a long list, ignoring the rest of the list
  • (a:as) which is head:tail
  • all@(a:as) Will get us all the elements of a list in addition to a and as

If we want to match any, we use _. This is particularly useful if we want an exhaustive matching as opposite to partial matching.

We can use constructors to deconstruct (think unapply in Scala` a value)

1
2
3
4
data Foo = Bar | Baz Int
f :: Foo -> Int
f Bar     = 1
f (Baz x) = x - 1

Conditionals

If expression

1
2
3
4
greaterThan10 x = 
    if x > 10
    then "greater than 10" 
    else "nope"

Which we can also write in one line as

1
greaterThan10 x = if x > 10 then "greater than 10" else "nope"

Case expression

1
2
3
4
5
6
f :: Int -> String
f x = case x of 
  1 -> "One"
  2 -> "Two"
  3 -> "Three"
  _ -> "Not one, two or three"

Guard

1
2
3
4
water temperature
  | temperature <= 0      = "solid"
  | temperature >= 100    = "gas"
  | otherwise             = "liquid"

guard block is the code evaluating to Bool between | and =

Collections

List

  • We can only have lists of the same type.
  • [1,2,3] creates a list, however it is a syntactic sugar for 1:2:3:[], where [] denotes an empty list
  • Similar to Scala head, tail, last and init work as expected
    • head first element of the list
    • tail all but the first element in the list
    • last the last element in the list
    • init all but the last element in the list
  • null checks if a list is empty. I think that [] is similar to Scala’s nil
  • Common list functions: length, reverse, take, drop, maximum, minimum, sum, product, splitAt
  • Common list higher order functions: takeWhile, dropWhile, map, filter
  • elem similar to contains in Scala. 2 elem [1,2,3] would yield True
  • replecate 2 3 creates a list by replicating 3 two times.
  • zip combines two lists into a single list of tuples
  • zipWith applies a function to each corresponding elements of the two input list and return a list of results
1
2
3
4
numbers = [1,2,3,4]
numbers ++ numbers -- concatination
0:numbers -- cons operator: adds an element in front of a list
numbers !! 0 -- Get the first element in the list (at index 0)

Range

  • [1..3] defines a range from 1 to 3 all included.
  • [1,4..10] defines a range with a step of 3.
  • [4,3..1] We have to define a step to go in descending order
  • ['a'..'f'] it works for characters too

Infinite

One cool thing lazy evaluation gives us, is infinite lists

  • [1..] Yes, that counts up… forever, and it actually runs with no errors… but you will need to ctrl+c to snap out of it
  • repeat 'a' An infinite list of a’s
  • cycle [1,2] similar to repeat but for a list
  • Use something like take to get a few numbers of an infinite sequence

Comprehension

List comprehension has the format [operation | list, filters].

  • The list x <- y where y is a list
  • The operation, where a function is applied to x
  • The filter, which is a predicate that we run against x
    1
    2
    
    [x | x <- [1,2], x > 1]
    duplicatePositiveNumbers xs = [x * 2 | x <- xs, x > 0]
    
  • We can have multiple predicates [x | x <- [1..10], even x, x > 5, x < 9]
  • We can have multiple lists [show x ++ "-" ++ show y | x <- [1..3], y <- [4]] which would result in ["1-4","2-4","3-4"]
  • We can have multiple lists and mutiple predicates [show x ++ "-" ++ show y | x <- [1..10], y <- [4], x > 5, even x, x <9]
  • We can discard x itself if we don’t care about it sum[1 |_ <- "hello"] which would return the length of “hello”
  • List comprehensions can be nested [sum y | y <- [ [1 | _ <- xs]| xs <- ["hello", "world!"]]], which would yield [5,6] the lengths of the two strings.

Tuples

  • Tuples are surrounded by parenthesis (1,2)
  • The type of a tuple includes the number of elements and their types
  • Paris have two useful functions to extract the first and second elements fst and snd respectively
  • zip of two lists, result in a list of tuples
    • The new list will have the length of the shortest of the original lists
    • zip [1..] [4, 5, 6] results in [(1,4),(2,5),(3,6)] even though the first list is infinite

Types

  • Use :type or :t in the interactive shell to inspect the type of something
    • For example :t 'a' results in 'a' :: Char
    • :: means has the type or of type
  • Int is bounded by ±2147483647, while Integer is not. The former is more efficient
  • Float, Double, Bool and Char behave as expected. [Char] is the representation of strings
  • Rational Represents rational numbers, such as 5/7. 4/2 will be stored as 2/1
  • Fixed Fixed decimal point number. 1.00, has two points, etc.
  • :t max results in max :: Ord a => a -> a -> a. It first defines the types of arguments Ord a then denotes taking two arguments and one return a -> a -> a
  • :t head results in head :: [a] -> a. Note the absence of type declaration. This is a variable type, similar to generics in a way.
  • => Anything before that symbol in type definitions is called class constraint

Useful types

  • Maybe a = Nothing | Just a is used when a value may or may not exist. Similar to nullable types in Kotlen and Optional in Scala

Declaring a new type

  • data Bool = False | True is the Bool type declaration, called data declaration
    • Bool is a type Constructor
    • False or True are two value constructors
    • | indicates a sum type. That is Bool, has either of the two values.
    • In English:
      • Declare type Bool
      • Declare two values True and False
      • Assign for Type Bool the two values True and False. In a way that an identifier of type Bool can either be True or False.

Data constructor

For data Foo = Bar Int String, Bar is data constructor with the signature Bar :: Int -> String -> Foo. It is a function (constructor) that takes two argument and returns data.

type vs. data vs. newtype

  • data Foo = Foo Int Foo and Int are totally different.
  • newtype Foo = Foo Int Foo and Int are different at compile time but same at runtime.
  • type Foo = Int Foo and Int are same at compile time and runtime.

Type Classes

Common type classes

  • Eq, Ord for equality and ordering
  • Show, Read for show and read functions respectively
  • Enum For representing enumerators
    • Types in this class include (), Bool, Char, Ordering, Int, Integer, Float and Double.
    • succ and pred are functions that use this type
  • Bounded for types that have min and max bounds. We can do maxBound :: Int, and there is also minBound
  • Num, which in turn includes Integral and Floating
    • fromIntegral function turns Integer like values to floating point like values. Its type is fromIntegral :: (Num b, Integral a) => a -> b

Polymorphism

  • There are three kinds of types concrete, constrained polymorphic and parametric polymorphic
  • type classes are a form of constrained or ad-hoc polymorphism
  • head :: [a] -> a is parametrically polymorphic function. It specifies how the types of inputs and outputs are in relation to one another, without specifying exactly what those types are.
  • In the signature, the lower cased letters such as a and b are called type variables. Those variables can be constrained by a type class maxBound :: Bounded a => a or not head :: [a] -> a

Creating Type Class instances

  • We can automatically derive Eq, Ord, Enum, Bounded, Read, and Show using the keyword deriving with some constraints.
  • To define a type class ,we make instance for a given type and use the keyword instance for example
    1
    2
    3
    
    data TypeConstructor = DataConstructor
    instance Eq TypeConstructor where
      DataConstructor == DataConstructor = True
    
  • This is more or less the same as data TypeConstructor = DataConstructor deriving Eq
  • :i Real would give us class (Num a, Ord a) => Real which means, for something to have an instance of Real, it must first have instances of Num and Ord. I omitted the rest of the result of :i Real, which indicates what other functions Real require

Creating our own Type Classes

1
2
3
4
5
6
7
8
9
class CanPlayFootball a where
    yes :: a -> Bool

instance CanPlayFootball Int where
    yes 10 = True
    yes _ = False
    
a = yes (5  :: Int) -- Will be false
b = yes (10 :: Int) -- Will be true

Other considerations around type classes

  • type defaulting is a way for the compiler to solve ambiguous type problems. In Prelude for example there is default Num Integer, which defaults to a specific concrete type for the Num type class.
  • An orphan instance is when an instance is defined for a typeclass and a data type but not in the same module in either the typeclass of the data type. If we don’t own the data type we should newtype it.

Resources

This post is licensed under CC BY 4.0 by the author.
Contents

Creating pipelines using channels in Go

Hello Python: Syntax Cheat Sheet

Comments powered by Disqus.