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/
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)!
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
We’ll talk about types in much more detail later. For now, let’s experiment a little.
> :type True
> :t True
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 Num
ber
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 Int
s or two Float
s
or two Double
s 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)
The String
type is represented as a list of
Char
acters, 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’
> :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'))))))
> <UP>
> :<TAB> -- lots of commands
> :set <TAB> -- lots of options and flags
> :set prompt "> "
> 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
> 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.
This may be handy once in a while:
> :{
| let
| add x y =
| x + y
| add' (x, y) =
| x + y
| :}
So far, we’ve used the interactive shell. Now let’s create a
standalone Haskell source file called Introduction.hs
.
Load a source file from the shell.
> :load Introduction.hs
> :l Introduction.hs
> :l Introduction
> :reload
> :r
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
).
> :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
-- single line comment
{- multi
line
comment -}
The top of this syntax reference summarizes some of what we have seen so far (and previews some of what we have not).
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
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.
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.
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
> import System.Environment
> :t getEnv
> getEnv "user"
> getEnv "USER" -- akin to: % echo $USER
> getEnv "PASSWORD"
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.
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
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
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"]
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.
Make sure your development environment and submission workflow are in order: see PA 0 and submit before the deadline.