Learning Haskell: Hello, Haskell!

posted 2 years ago

Interacting with Haskell

There are two primary ways of working with code:

  • Interactive environment (GHCi)
  • Typing code into a text editor, saving it, and then loading the source code into GHCi

Using the REPL

REPL is an acronym for read-eval-print loop.

REPLs provide an environment for inputting code, evaluating it, and seeing the result.

REPLs first originated with the Lisp Programming Language but are now common to modern programming languages.

The REPL in Haskell is called GHCi.

Getting Started

Install Haskell by going to: https://www.haskell.org/platform/

Once you have Haskell installed, open your terminal and type ghci or stack ghci.

stack ghci includes a lot of modules that will be convenient to use later. It is not necessary to use stack ghci at this point.

You will see something like the following:

GHCi, version 8.8.4: https://www.haskell.org/ghc/  :? for help
Prelude> 

Let's try some basic arithmetic:

Prelude> 10 + 10
20
Prelude> 1 < 50
True
Prelude> 5 ^ 2
25

Congrats, you are now a functional programmer. It may not seem like much, but this is just the beginning of Haskell's capabilities.

To exit GHCi, type in the command :quit or :q.

What is Prelude?

Prelude is a library of standard functions that Haskell provides. Opening GHCi or Stack GHCi automatically uses Prelude.

There are alternative preludes and you can turn the default Prelude off.

You can find more information about Prelude in Haskell's base package at https://www.stackage.org/package/base.

The base package contains basic libraries.

Working with Source Files

Almost all programming will involve editing libraries or applications comprised of directories containing files with Haskell code in them.

The basic workflow is to have the Haskell code in a file, load it into the REPL, and then interact with it there.

Create a file called hello.hs. Then, enter the following code into it:

helloHaskell :: String -> IO ()
helloHaskell name =
  putStrLn ("Hello, " ++ name ++ "!")

:: is a way to write the type signature. You read it as "this has the type". In this instance, helloHaskellhas the type String -> IO ().

I want to focus primarily on syntax in this article. I will talk about types in a later article.

In the directory where you created the hello.hs file, open up ghci and type the following:

Prelude> :load hello.hs 
[1 of 1] Compiling Main             ( hello.hs, interpreted )
Ok, one module loaded.
:Main> helloHaskell "Haskell"
Hello, Haskell!
:Main> 

You can use :l as a shorthand for :load.

After you load hello.hs, the helloHaskell function is now visible in ghci.

You can pass it a string argument like "Haskell" and see the output.

If you load the code from the source file and your GHCi prompt is no longer Prelude>, you can return to it by using the :m command.

*Main> :m
Prelude> 

The :m command is shorthand for :module.

This will unload the file from GHCi so that it will no longer be in scope.

You can set your prompt to whatever you would like by using :set prompt "λ> " for example.

Understanding Expressions

Everything in Haskell is either an expression or declaration.

An expression can be a value, a combination of values, and/or functions applied to values.

Expressions evaluate to a result.

Expressions act as the building blocks of programs.

The following are all expressions:

10
10 + 10
"Aegis"

Each of these can be entered into GHCi and will return a result. The number 10, has no further reduction steps, so it represents itself.

In the case of 10 + 10, GHCi reduces the expression to 20, then outputs the number 20.

Numbers can be nested:

Prelude> (5 + 5) * 2
20
Prelude> ((5 * 3) + 2) - 5
12

Try expanding on this yourself. You can nest as many expressions as you would like.

Expressions are said to be in normal form when they are irreducible.

The normal form of 2 + 2 is 4. This is because it can no longer be reduced.

Reducible expressions are also known as redexes.

Functions

A function is a specific type of expression.

Functions in Haskell are related to functions in mathematics in that they map an input or set of inputs to an output.

Because functions are built out of expressions, they will always evaluate to the same result when given the same values.

In lambda calculus, all functions take one argument and return one result.

In Haskell, when it appears that we are passing multiple arguments into a function, we are actually applying a series of nested functions.

This is known as currying.

Functions allow us to abstract parts of code that we would like to reuse.

Imagine you need to take a handful of expressions and multiply them by 2.

You could do this manually like this:

Prelude> (2 + 2) * 2
8
Prelude> (5 + 1) * 2
12
Prelude> (3 + 8) * 2
22

That seems like a lot of work though. Imagine you had a ton of expressions that you needed to repeat that process for.

Functions are how we factor out the pattern into something we can reuse with different inputs.

The common pattern is the part where we multiply by 2.

The part that varies is the expression before it.

Let's build this function out in the next section.

Defining Functions

Functions all have a name, parameters, an equal sign, and a body.

Defining functions in GHCi and in source code are a bit different.

In GHCi, you must use let:

Prelude> let doubleIt num = num * 2

In source code, it is entered like this:

doubleIt num = num * 2

Let's examine each part:

doubleIt num = num * 2
   [1]   [2][3]   [4]
  1. This is the name of the function. It can be whatever you would like. Just make sure that it starts with a lowercase letter.
  2. This is the parameter of the function. The parameters of a function correspond to the head of a lambda and bind variables that appear in the body expression.
  3. The = is used to define values and functions.
  4. This is the body of the function. Here the expression num * 2 is the body of the function. If you have an expression like doubleIt 5, num is bound to 5.

Trying the doubleIt function out:

Prelude> doubleIt 5
10

Try using different values for num. You can use integer values or even mathematical expressions.

Try changing the function itself in the source code. Then, reload it using the :r or :reload command.

Evaluation

Evaluating an expression is reducing terms until the expression reaches its simplest form.

Haskell uses a non-strict evaluation strategy which defers evaluation of terms until they're forced by other terms referring to them.

Like in the lambda calculus, application is evaluation: applying a function to an argument allows evaluation or reduction.

Refer back to the doubleIt function:

doubleIt 4
-- [doubleIt num = num * 2; num := 4]
4 * 2
8

Here, we apply doubleIt to the value 4 and then reduce the expression to the final result 8.

The expression doubleIt 4 is in normal form when it reaches 8 because it has no remaining reducible expressions.

Infix Operators

Functions in Haskell default to prefix syntax meaning that the function being applied is at the beginning of the expression rather than the middle.

We saw prefix syntax with our doubleIt function.

While this is the default syntax for functions, not all functions are prefix.

For example, the arithmetic operators we've been using, are functions but appear by default in an infix position.

All operators are functions; not all functions are operators.

You can use infix operators in a prefix fashion by wrapping them in parentheses:

Prelude> (+) 200 200
400
Prelude> (*) 2 5
10

If a function name is alphanumeric, it is a prefix function by default.

Not all prefix functions can be made infix.

If the name is a symbol, it is infix by default.

Associativity and Precedence

GHCi has a command called :info which provides information about an operator or a function.

Let's use this command on operators to get a better idea of them:

Prelude> :info (*)
class Num a where
  ...
  (*) :: a -> a -> a
  ...
    -- Defined in ‘GHC.Num’
infixl 7  *
  [1] [2][3]
  1. infixl means that it's an infix operator; the l means that it's left associative.
  2. 7 is the precedence, higher is applied first, the scale is 0-9.
  3. The infix function name: in this case, multiplication.

Left associativity:

This:

2 * 3 * 4

Evaluated as:

(2 * 3) * 4

Left takes precedence.

Here's a right-associative infix operator:

Prelude> :info (^)
(^) :: (Num a, Integral b) => a -> b -> a   -- Defined in ‘GHC.Real’
infixr 8  ^
  [1] [2][3]
  1. infixrmeans that it's an infix operator; ther` means that it's right associative
  2. 8 is the precedence
  3. The infix function name: in this case, exponentiation

Right associativity:

This:

2 ^ 3 ^ 4

Evaluated as:

2 ^ (3 ^ 4)

Declaring Values

Let's make a new file called values.hs

module Values where

x = 23 * 7 + y

theResult = x * 3

y = 6

Declaring values is as easy as that.

Arithmetic Functions in Haskell

Operator Name Purpose/application
+ plus addition
- minus subtraction
* asterisk multiplication
/ slash fractional division
div divide integral division, rounding down
mod modulo like rem, but after modular division
quot quotient integral division, rounds towards zero
rem remainder remainder after division

Integral division refers to division of integers.

You usually want to use div for integral division, due to the way div and quot round.

Negative Numbers

To work with negative numbers, wrap them in parenthesis:

Prelude> 10000 + (-10)
9990

Parenthesization

The $ operator is used frequently in Haskell.

The definition of $:

f $ a = f a

This operator is a convenience for when you want to express something with fewer pairs of parenthesis:

This:

Prelude> (2^) (2 + 2)
16

Same as:

Prelude> (2^) $ 2 + 2
16

If you remove $ and the parenthesis:

Prelude> (2^) 2 + 2
6

Everything to the right of $ will be evaluated first and can be used to delay function application.

You can stack up multiple uses of $ in the same expression.

Comparing Let and Where

Let introduces an expression, so it can be used wherever you can have an expression.

Where is a declaration and is bound to a surrounding syntactic construct.

where example:

module Learn where

printer n = print plusTwo
  where
    plusTwo = n + 2
*Learn> printer 1
3

let example:

module Learn where

printer n =
  let plusTwo = n + 2
   in print plusTwo
*Learn> printer 1
3

Article on let vs. where: https://wiki.haskell.org/Let_vs._Where

Sources

Functions - https://www.haskell.org/tutorial/functions.html

Haskell Book - https://haskellbook.com/