By Leo Rudberg, for the UPL Video Lecture Series
First, let's make sure that you have ghc
and ghci
installed:
$ ghc --version && ghci
# ghci should boot up...
If that doesn't work, you need to download the Haskell platform.
Your REPL should be ready to roll. Let's do the classic "Hello world":
Prelude> putStrLn "Hello world!"
Enter the following into your ghci
REPL and observe the results
1 + 2
1 - 2
(*) 2 3 -- operators are just functions (see below)
(/ 16) 2 -- partial application
7 `mod` 4
(mod 4) 5
True || False
False && True
(not ((> 4) 3)) && (3 >= 3)
'c' -- a char
['s', 't', 'r', 'i', 'n', 'g'] == "string"
["this is a list", "of", "THINGS!"]
[] -- an empty list
-- NOTE: all elements of a list should be the same type
-- lists can be ranges
[1..5]
['a'..'z']
-- they can also be infinite!
[1..]
-- to get an element from a list
3 == [1..5] !! 2
-- tuples are fixed-sizes groupings
("like", "pairs")
('x', 'y', 'z')
(1, 3, ["foo", "bar"], True, ("UPL", 2))
(1, 2) -- not the same as [1, 2]
-- basic syntax
let doubler = (\ n -> n * 2)
let adder = (\ a b -> a + b) -- two arguments
Lambdas are anonymous functions, nothing special! If you've used scripting languages like Ruby, Python, or JavaScript, you've probably used a lambda, but called it a block or closure. They can be used as arguments to other functions like map
.
-- two dashes is a comment
{-
multi
line
comment
-}
if 4 < 5 then ":)" else ":("
if 5 >= 6 then ":(" else if 3 < 2 then ">:(" else "8|"
(4 + (2 * 3)) == 4 + $ 2 * 3 -- only works in ghc (NOT ghci)
mod 3 4 == 3 `mod` 4 -- infix operators
-- FUNCTION DECLARATIONS
-- the following are equivalent
let add x y = x + y -- in ghci
add x y = (+) x y -- in ghc
add = (+)
add = (\ x y -> x + y) -- a lambda expression (see below)
(add 4) 5 == add 4 5 -- no parentheses or commas!
All functions in Haskell are functions of only one argument.
When you call a function with "all arguments", it returns a value. If you call a function with less than all of its parameters, then you get a curried function.
So, the following are equivalent:
let tripler n = n * 3
-- equivalent to
let tripler = (3 *)
-- equivalent to
let tripler = (*) 3
A useful example
take 2 (map tripler [1..]) == [3, 6]
Let's look at the left hand side:
(map tripler [1..])
This maps all positive integers (1 to Infinity) using tripler
. In other words, for every element n
in the infinite list [1..]
, tripler
is applied to it to get 3 * n
. Thus (map tripler [1..])
gives us [3 * 1, 3 * 2, 3 * 3, ...]
.
We can work with infinite lists in Haskell because it is lazy -- it only gives you what you need when you need it, and never earlier! Read more about Haskell's laziness here.
take 2 (map tripler [1..])
The take 2
part of this retrieves the first two elements from this infinite list. In this case, we get [3, 6]
.
See this article for more on ghci
syntax.
Note: do
is a special sugar syntax over monads. Think of it as a way of forcing Haskell to do IO actions in a specific order. Remember that performing I/O in your functions will make them impure, and impurity makes your life less easy. In Haskell, you want to minimize I/O: your main
function should, optimally, be the only function performing I/O. All other functions should be running pure, mathematical computation over that unwrapped ("un-I/O'd") input.
{- the thing directly below these comments is a type signature
it helps make your code clearer for compilers and humans alike!
this type signature says:
"foo is a function that takes a String and returns an Integer"
-}
foo :: String -> Integer
foo s = (length s) * 1000
-- main is a function that takes no (code) arguments and performs IO
main :: IO ()
main =
do
let result = foo "do something"
putStrLn (show result) -- show is like "toString"
return ()
import System.Environment (getArgs)
-- three string arguments, one integer return value
bar :: String -> String -> String -> Integer
bar a1 a2 an = (length (a1 ++ a2)) * (length $ maximum [a1, a2, an])
baz :: [String] -> Integer
baz args = 100 - (count args)
main :: IO ()
main =
do
args <- getArgs -- "<-" means "perform impure function, and fetch result"
let [arg1, arg2, argN] = args -- yay pattern matching! here, we're forcing 3 args
let combinedResult = (bar arg1 arg2 argN) * (baz args)
putStrLn (show combinedResult)
return ()
quux :: String -> Bool
quux str = 'q' `elem` str
loop = do
putStrLn "Input:"
input <- getLine
let result = quux input
putStrLn (show result)
loop
main = do
loop
During the talk, Mike Tolly created this great explanation of Haskell types:
-- Haskell datatypes are made up of "constructors",
-- different ways to construct or make that datatype.
-- Here is a type with 2 constructors:
data Bool = False | True
-- (this definition is built-in, you don't need to put it in your code.)
-- This definition makes
-- 1. a type, called Bool
-- 2. a value, False, of type Bool, written: False :: Bool
-- 3. a value, True, of type Bool, written: True :: Bool
-- Constructors are just values (or functions), with some extra features.
-- Both type names and constructors start with capital letters.
-- (Normal variables and "type variables" (shown later) start with lowercase letters.)
-- "Pattern matching" is like a super-powered "switch" statement.
-- Each line of the match specifies *constructors* to match on.
-- Let's define the logical AND function for Bool.
myand :: Bool -> Bool -> Bool -- takes 2 Bools as args and returns a Bool
myand x y = case x of -- pattern match on the "x" argument
True -> y -- If x is True, then the result is y.
False -> False -- If x is False, then the result is False.
-- Pattern matching can be written in two styles.
-- The first style with the "case" keyword is above.
-- Here's the second way, which lets you match on all arguments together.
myand2 :: Bool -> Bool -> Bool
myand2 True y = y
-- In the above line, True is a pattern for the first argument,
-- and y is a pattern for the second. Both patterns must match
-- for the line to be chosen.
-- The "y" is a pattern that matches no matter what, and *binds*
-- that second argument to the name "y".
myand2 False _ = False
-- Now, False is the pattern for the first argument, and the underscore
-- is the pattern for the second. The underscore is like the "y"
-- pattern, but it doesn't bind the value -- it ignores it.
-- The power of pattern matching is that the compiler
-- can check that you handled all constructors of the value you match on.
-- Here's a definition that will cause a warning:
brokenfn :: Bool -> Int
brokenfn True = 5
-- We didn't handle the False case, so supplying False will
-- throw an exception. Make sure you pass -Wall to ghc or ghci
-- to see the warnings.
-- Now, here's another type that's not just a list of values.
data IntOrNull = AnInt Int | Null
-- Here, we define an IntOrNull type, with 2 constructors:
-- 1. AnInt is a *function* that takes an Int, and returns an IntOrNull.
-- (Int is a built-in type.)
-- The type of AnInt can be written: AnInt :: Int -> IntOrNull
-- 2. Null is a value, just like True or False, but of type IntOrNull.
-- Here's how to match on it.
theIntOrZero :: IntOrNull -> Int
theIntOrZero (AnInt i) = i -- This matches the AnInt constructor,
-- and binds "i" to the Int within.
-- That binding is then active only on the
-- right-hand side of the match line.
theIntOrZero Null = 0
-- A more general version of this is already built-in to Haskell:
data Maybe a = Just a | Nothing
-- The "a" here is a "type variable". This is very much like the
-- generics system in Java/C#/etc.
-- Maybe can be called a "type function" -- it is parametrized
-- by another type. So "Maybe" is not a type, but "Maybe Int" is.
-- For the type "Maybe Int", the "Just" constructor takes an Int.
-- So Just has this type: Just :: a -> Maybe a
-- Nothing is still just a value: Nothing :: Maybe a
-- Here's a function that already exists in the "Data.Maybe" module.
-- You can put "import Data.Maybe" at the top of your file to get it.
fromMaybe :: a -> Maybe a -> a
-- This type means: for some (any) type, which we'll call "a",
-- this function can take an "a", a "Maybe a", and produce an "a".
-- So, for example, you can give it Int and Maybe Int and get Int.
fromMaybe _ (Just x) = x -- Mind the parens!
-- If our "Maybe a" has a real "a" in it, return that.
fromMaybe x Nothing = x
-- Otherwise, return the first argument as a default.
main :: IO ()
main = print (myand2 False True)