Introduction

Install the latest stable version of Haskell (currently 9.2.5 or higher, or 9.4.3 or higher) on your machine. It should also be available on linux.cs.uchicago.edu. When installation finishes, hopefully you see something like this:

All done!

To start a simple repl, run:
  ghci

To start a new haskell project in the current directory, run:
  cabal init --interactive

To install other GHC versions and tools, run:
  ghcup tui

If you are new to Haskell, check out https://www.haskell.org/ghcup/steps/

REPL (Read-Eval-Print Loop)

Launch the interactive Haskell shell, GHCi, from the UNIX command prompt.

% ghci
GHCi, version 9.2.5: https://www.haskell.org/ghc/  :? for help
ghci>

If e can be parsed and assigned a valid type t (READ), then e is evaluated (EVAL). If it terminates, the resulting value v is converted to a string and displayed (PRINT). Repeat (LOOP)!

Numbers, Booleans, Strings, Characters

Try evaluating expressions e involving integers, floating-point numbers, booleans, strings, and characters. Some of the expressions below exhibit type errors and are thus not evaluated.

ghci> 4
ghci> :set prompt "> "
> 4
> 4.0
> 4 + 4
> 4 + 4.0
> 'a'
> "a"
> "a" + "b"
> "a" ++ "b"
> "a" ++ 'b'
> True
> true

Types

We’ll talk about types in much more detail later. For now, let’s experiment a little.

> :type True
> :t True

Numeric Types

First, let’s ask for “default” types:

> :t +d 4
> :t +d (+)

Next, ask for more general types:

> :t 4

We’ll dig into this in detail later. For now, read “Num a => a” as “Int and Float and Double and every other numeric type you can think of”. That is, “for all types a that are Number types, 4 has type a.”

We can “convert” (i.e. “instantiate”) 4 to a particular numeric type with an explicit type ascription (a.k.a. type annotation), using the syntax e :: t, where e is an expression and t is a type.

> :t (4 :: Int)
(4 :: Int) :: Int
> :t 4 :: Int
4 :: Int :: Int
> :t 4 :: Integer
4 :: Integer :: Integer
> :t 4 :: Float
4 :: Float :: Float
> :t 4 :: Double
4 :: Double :: Double
> :t 4.0 :: Double
4.0 :: Double :: Double
> :t 4.0 :: Int

Notice that operators like (+) can work on many different kinds of numbers.

> 4 + 5
> (+) 4 5
> :t (+)
(+) :: Num a => a -> a -> a

You can read the type for (+) as saying “(+) takes two Ints or two Floats or two Doubles or two values of any other numeric type and returns another number of the same type as the arguments”. Haskell automatically infers the specific types based on how the expressions are used. But we can also explicitly tell Haskell which types we want using type ascription.

> :t (4 :: Int) + (5 :: Int)
> :t (4 + 5) :: Int
> :t (4 + 5) :: Float
> :t (4 :: Float) + 5
> :t (4 :: Float) + (5 :: Double)
> :t (4 :: Float) + (5 :: Int)

Strings as Lists of Characters

The String type is represented as a list of Characters, written [Char]. (It may be easier to read if you pretend it says List Char.)

> :t 'a'
> :t "a"
> :t "a" :: [Char]
> :t (++) :: String -> String -> String

By default, String is defined an alias for the type [Char]. (Ignore the String :: * line for now.)

> :info String
type String :: *
type String = [Char]
    -- Defined in ‘GHC.Base’

Tuples

> :t ('a', True)
> :t ('a', True, 1)
> :t ('a', (True, 1))

The zero-element tuple is called unit; its type, the zero-element tuple type, is called the unit type:

> :t ()

What types do you think the following expressions have?

> :t ('a')
> :t (((((('a'))))))

History and Tab Completion

> <UP>
> :<TAB>               -- lots of commands
> :set <TAB>           -- lots of options and flags
> :set prompt "> "

Let-bindings

> let x = 1
> x + 1
> let y = 2
> x + y

If you’re familar with mutable variables in other languages, this is not the same thing. These variable bindings never change…

> let z = 0
> let z = 1                   -- "shadows" previous definition
> let z = z + 1               -- recursive definition
> z                           -- "forces" evaluation (diverges)
> Ctrl-C                      -- to interrupt
> Ctrl-D                      -- to quit GHCi

Multiple bindings in a single let.

> let a = 1; b = 2            -- multiple bindings
> let a = 1; b = 2;           -- optional last semi-colon
> let {a = 1; b = 2;}         -- optional braces

Top-level vs. local let-bindings.

let x = 1                     -- "global" binding
let y = 2 in y + y            -- "local" binding
y                             -- not in scope

Shadowing.

let y = 6
let y = 5 in y + y            -- previous binding "shadowed" (locally)
y

let {a = let b = 3 in b; b = 2;}   -- definitions at same level are recursive;
(a, b)                             -- inner definitions shadow outer ones

Simple Function Definitions

> let add3 x = x + 3
> add3 1
> add3 1.1
> add3 0xDEADBEEF

> let add x y = x + y
> add 3 4
> add 3
... No instance for Show ...  -- more on this later

> let add' (x, y) = x + y     -- variables can include "prime" character

> add' 1 2                    -- type error
> add (1, 2)                  -- type error

The the add function takes two arguments, whereas the add' function takes a single argument (a 2-element tuple, a.k.a. a pair).

All functions in Haskell take exactly one argument and produce exactly one value in return. But what about the “multi-argument” functions above?

> let add4 = add 4               -- "currying" or partial application
> add4 10

We’ll talk much more about functions in due course.

Multi-Line REPL Input

This may be handy once in a while:

> :{
| let
|   add x y =
|     x + y
|   add' (x, y) =
|     x + y
| :}

Source Files

So far, we’ve used the interactive shell. Now let’s create a standalone Haskell source file called Introduction.hs.

Loading Files

Load a source file from the shell.

> :load Introduction.hs
> :l Introduction.hs
> :l Introduction
> :reload
> :r

Top-Level Definitions

Unlike in the shell, top-level definitions in Haskell source files do not start with let. Instead, they are written like equations.

minutesPerDay = 60 * 24

Definitions can refer to subsequent definitions later in the file. Helper definitions can help improve readability and maintainability.

minutesPerDay = minutesPerHour * hoursPerDay
minutesPerHour = 60
hoursPerDay = 24

All definitions are at the “top-level” and are mutually recursive.

Local variables can help improve readability and maintainability, by emphasizing the scope of the definitions. For example, instead of the previous three top-level definitions, we can use two locally defined bindings:

minutesPerDay =
  let minutesPerHour = 60 in
  let hoursPerDay = 24 in
  minutesPerHour * hoursPerDay

Or better yet:

minutesPerDay =
  let
    minutesPerHour = 60
    hoursPerDay = 24
  in
  minutesPerHour * hoursPerDay

Alternatively, where-clauses can be used instead of let-bindings.

minutesPerDay = minutesPerHour * hoursPerDay
  where minutesPerHour = 60
        hoursPerDay = 24

Like with let-bindings, minutesPerHour and hoursPerDay are not accessible outside this definition.

For both let-bindings and where-clauses, you can choose indendation depth but all variables being defined must start at the same column. So, there are many possible formatting styles. Here’s another one.

minutesPerDay      = minutesPerHour * hoursPerDay where
    minutesPerHour = 60
    hoursPerDay    = 24

Where-clauses can be understood as syntactic sugar for let-bindings.

e_2 where x = e_1 = let x = e_1 in e_2

Depending on the context, you may prefer a “top to bottom” organization (via where) or “bottom to top” style (via let).

Booleans and Guarded Definitions

> :t True
> :t 1 == 2
> :t 1 < 2

If-then-else expressions.

absoluteValue n =
  if n >= 0
    then n
    else -1 * n

Alternatively:

absoluteValue n
  | n >= 0 = n
  | n <  0 = -1 * n

We can use otherwise as the last, “catch-all” guard. (What value do you think otherwise equals?) And we can negate n more concisely.

absoluteValue n
  | n >= 0    = n
  | otherwise = -n

As we will understand better later, conditionals and guards are syntactic sugar for a more primitive pattern-matching form:

absoluteValue n =
  case n >= 0 of
    True  -> n
    False -> -n

Comments

-- single line comment

{- multi
   line
   comment -}

Syntax Reference

The top of this syntax reference summarizes some of what we have seen so far (and previews some of what we have not).

Hello? World?

Okay, we’ve said hello to the Haskell world. But where was printing “Hello, world!”? Let’s do it!

Everything so far has been “pure” (no side effects). To interact with the world in Haskell, we need to “run” “actions” (effects).

From the interpreter:

> putStrLn "hello!"

From a file Hello.hs:

hello :: IO ()
hello = putStrLn "hello!"

In interpreter:

> :l Hello.hs
> :run hello
> hello

From UNIX command-line:

% ghc Hello.hs -o hello

Fails, because main function is not defined. Note that the module must be called Main. In Hello.hs:

module Main where

main :: IO ()
main = putStrLn "Hello, world!"

From UNIX command-line:

% ghc Hello.hs -o hello
./hello

I/O Types

In other languages, I/O primitives typically have signatures like the following:

printLine :: String -> ()
readLine  :: () -> String

But these types also describe functions that do not perform any I/O.

IO Actions in Haskell

In Haskell, a value of type IO t is “an IO action that, when run, produces a t”. IO is like a scarlet letter that says “I/O inside!”. There is no (acceptable) way to take off that IO label.

putStrLn :: String -> IO ()

Dummy unit value is the result of printing to the screen.

How to read a string from standard input?

getLine :: IO String

Whoa, it’s not a function. It’s an action that, when run, returns a String.

> getLine

main = do
  s <- getLine                   -- binds result of action
  putStrLn ("[[" ++ s ++ "]]")

Note: You may have seen an operator called ($), which pops up early and often in Haskell code. We’ll talk more about how it works later. For now, when you see an infix use of $, think about it by replacing it with an open parenthesis ( and then adding a close parenthesis ) all the way at the rightmost end of the expression. For example, the last line above becomes:

  putStrLn $ "[[" ++ s ++ "]]"

This operator becomes profitable ($$$) when reading and writing nested function calls.

Handy functions for working with newlines:

words :: String -> [String]
unwords :: [String] -> String

Any functions that (transitively) perform I/O will have types that label it as such! So, it’s good practice to factor the impure and pure code as much as possible. This will take practice.

Do-notation

The do construct takes a sequence of actions (one or more) and converts them into a single action.

> do putStrLn "hello"

> do putStrLn "hello"; putStrLn "world"

> do { putStrLn "hello"; putStrLn "world" }

> :{
do
  putStrLn "hello"
  putStrLn "world"
:}

Kind of looks like imperative code. We’ll see that this syntax is, in fact, much more general!

A do-block takes the form do {stmt_1; …; stmt_n; e}. See the syntax reference for the three forms that each stmt may take.

Running an action without binding its results is particularly useful for actions of type IO ().

do
  () <- putStrLn "yo"
  _  <- putStrLn "yo"
  putStrLn "yo"

The return function wraps a pure value inside an IO value:

> :t return :: a -> IO a

Reading Environment Variables

> import System.Environment
> :t getEnv
> getEnv "user"
> getEnv "USER"        -- akin to: % echo $USER
> getEnv "PASSWORD"

Example: Login Loop

Login.hs:

import System.Environment
import System.IO

main :: IO ()
main = do
  hSetBuffering stdin LineBuffering
  putStrLn "What's your name?"
  s <- getLine
  user <- getEnv "USER"
  if s == user then
    putStrLn $ "Well done, " ++ user ++ "!"
  else do
    putStrLn "Wrong, try again.\n"
    main

Notice that expressions of type IO t can be recursive, like expressions of any other types. And setting LineBuffering allows characters to be deleted from standard input.

Read and Show

We will learn about the Read and Show type classes later. For now, a preview:

> :t show                         -- a.k.a. "toString"
show :: Show a => a -> String

> :t read                         -- a.k.a. "fromString"
read :: Read a => String -> a

> show True
> read "True"
> read "True" :: Bool

> :t read "True" :: Int           -- well-typed but...
> read "True" :: Int

> read "hi" :: String
> read "\"hi\"" :: String

Example: Looping and Reading Numbers

main :: IO ()
main =
  do
    hSetBuffering stdin LineBuffering
    putStr "Tell me a nice number: "
    s <- getLine
    let i = read s :: Int
    putStrLn ("Yes, " ++ show i ++ " is a nice number.")
    main

This is okay, but read crashes when the string s cannot be parsed as an Int. Let’s avoid error cases by checking all isDigit s, where:

all :: (a -> Bool) -> [a] -> Bool
isDigit :: Char -> Bool

Note: The actual type for all is more general than shown above.

import Data.Char

main :: IO ()
main = do
  putStr "Tell me a nice number: "
  s <- getLine
  if all isDigit s then
    let i = read s :: Int in
    putStrLn ("Yes, " ++ show i ++ " is a nice number.")
  else
    putStrLn "Hmm, that doesn't seem like a number."
  main

A bit better. Now let’s factor some of the pure code out of the IO do-block. Because the string can only maybe be interpreted as an Int, we use an algebraic datatype Maybe Int to denote the fact that either there may be an Int or there may not be. We will soon talk in detail about how ADTs are defined, and how their values are constructed and deconstructed via pattern matching.

readMaybeInt :: String -> Maybe Int
readMaybeInt s
  | all isDigit s = Just (read s)
  | otherwise     = Nothing

main :: IO ()
main = do
  putStr "Tell me a nice number: "
  s <- getLine
  case readMaybeInt s of
    Just i  -> putStrLn ("Yes, " ++ show i ++ " is a nice number.")
    Nothing -> putStrLn "Hmm, that doesn't seem like a number."
  main

Much nicer, but can we factor even more out of the do-block?

response :: String -> String
response s =
  case readMaybeInt s of
    Just i  -> "Yes, " ++ show i ++ " is a nice number."
    Nothing -> "Hmm, that doesn't seem like a number."

main = do
  putStr "Tell me a nice number: "
  s <- getLine
  putStrLn (response s)
  main

Let’s finish by pulling the looping behavior out as well.

loop :: String -> (String -> String) -> IO ()
loop prompt f = do
  putStr prompt
  s <- getLine
  putStrLn (f s)
  loop prompt f

main :: IO ()
main = loop "Tell me a nice number: " response

The pure and impure code is now nicely factored and reusable.

Now let’s go back to readMaybeInt and handle the empty string and negative numbers.

readMaybeInt :: String -> Maybe Int
readMaybeInt s =
  case s of
    [] ->
      Nothing
    '-':s' ->
      case readMaybeInt s' of
        Just i  -> Just (-1 * i)
        Nothing -> Nothing
    _ ->
      if all isDigit s then
        Just (read s)
      else
        Nothing

Note: We’ll consider pattern-matching on strings more later. But for now: What does the following produce, and why?

> "" == []

If we were going further and supporting more features (leading and trailing whitespace, fractions, etc.) we would probably choose to use regular expressions rather than manipulating individual characters. We would also use nice parsing techniques that we’ll learn about later in the course.

Final version: Loop.hs

Reading Command-Line Arguments

GetArgs.hs:

import System.Environment

main :: main IO
main = do
  args <- getArgs
  putStrLn (show args)

Compile and run:

% ghc GetArgs.hs
% ./GetArgs one two three
["one","two","three"]

Library Documentation

The documentation for Haskell libraries will be very useful throughout the course. From the main Haskell page, click “Documentation” and then “Haddocks for Libraries included with GHC”. Prelude is a good place to start browsing. Additional resources are listed at Haskell Documentation.

Hello, World!

Make sure your development environment and submission workflow are in order: see PA 0 and submit before the deadline.