Haskell Notes, page 2
General Haskell Notes
- 11/03/2024 Reimplementation of unix core utilities in Haskell. HN thread with comments by repo owner Austin.
- Austin has some posts on his blog discussing his work.
- Austin is answering questions in the HN thread.
- 12/02/2024 Discussion about 8 months of OCaml after 8 years of Haskell in production blog post and related HN thread.
Haskell mooc
Lecture 1
From 1.3.3 Uses of Haskell
- Examples of where Haskell is used can be found on The Haskell Wiki and this 2022 blog post
1.6 Expressions and Types
- Expressions and Types are core to Haskell. Unlike Python, Java, C, etc., there are no statements.
- An expresssion must have a value and a type.
- From 1.6.1 Syntax of Expressions. Note how
function()
calls in C/Java/… assume input arguments. Whereas in Haskell, one simply uses spaces to separate a function and it’s following list of parameters.- C:
f(1)
; Haskell equivalent:f 1
. - C:
f(1,2)
; Haskell equivalent:f 1 2
.
- C:
- Example of how function calls bind tighter than operators; aka functions take precedence over operators, like how multiplication binds tighter than addition in algebra.
- C:
a+b
; Haskell:a+b
- C:
f(a) + f(b)
; Haskell:f a + f b
- C:
f( a + g(b))
; Haskell:f( a + g(b) )
- C:
- NOTE: function application associates left.
- 1.6.2 Syntax of Types
- Int is a signed 64bit number; Integer is an unbounded number type
- Double is a floating point number
- Bool true/false
- String is an array of chars [Char]. In Section 1.6.3, they poit out that String and [Char] are synonyms and can be used interchangeably.
1.7 Golden Ratio Exercise (Structure of a Haskell Program)
- Created Gold module stored in
Gold.hs
file. Ran with bothrunghc Gold.hs
andstack runhaskell Gold.hs
and verified it works.- Note that executing the
stack
command kicked off an install process to the latest version of GHC.
- Note that executing the
1.8 Working with Examples
- For a simple one-line command, you can just paste it into ghci (Glasgow Haskell Compiler - Interactive).
- If one wants to cut and paste in multiple lines into ghci, use the
:{
and:}
syntax and paste in between. - Of course, one can use
:load
aka:l
to load a file to run within ghci. - Note there are two types of division operators:
/
anddiv
, connected toNum
andFractional
. More explanation of this when they discuss type classes.
1.9 Basic Structures
1.9.1 Conditionals instead of if
statement.
- In languages like Java, if is a statement. If it doesn’t have a value, it just conditionally executes other statements.
- In Haskell, if is an expression; it must have a value. It must select between 2 other expressions. It corresponds to the
?:
operator in C or Java. Let’s express this in 3 languages
1.9.1a Java
int price = product.equals("milk") ? 1 : 2;
1.9.1b Python
price = 1 if product == "milk" else 2
1.9.1c Haskell
price = if product == "milk" then 1 else 2
10/24/2024
1.9.1.1 Functions returning Bool
- In order to write if expressions in Haskell, one needs to know how to get values of type Bool.
- The most common way is binary (aka accepts 2 arguments as input) comparison operators like:
- == equal to
- < less than
- <= less than or equal to
- > greater than
- >= greater than or equal to
- One oddity is that Haskell uses
/=
rather than the more commonly used!=
operator for the not-equals operator. - And of course, one can get Bool output values as a result of these operators:
&&
logical and||
logical ornot
function
1.9.2 Local Definitions
- For now, both the
let...in
andwhere
constructs can be used interchangeably for small local variables.
1.9.3 A Word about Immutability
- Note that all “variables” in Haskell are more properly called defintions because they are immutable.
- They are not just like a box that can contain some value like in many other programming languages.
- This is why idioms like
i++
will not work in Haskell. - JH: I think Haskell “variables” (really, definitions), are similar to modern JavaScript variables declared by the
const
keyword. although that only prevents reassignment of the variable, but the value itself is changeable. Perhaps from theObject.freeze()
and a recursive function like the below inventeddeepFreeze()
function which is a recursive version ofObject.freeze()
:
function deepFreeze(obj) {
Object.freeze(obj);
for (const key in obj) {
if (typeof obj[key] === "object" && obj[key] !== null) {
deepFreeze(obj[key]);
}
}
return obj;
}
const myObj = {
name: "Alice",
address: { city: "New York" },
};
deepFreeze(myObj);
myObj.address.city = "Los Angeles"; // This will have no effect.
- Note that shadowing is when local definitions ‘shadow’ the names of variables defined elsewhere.
- Shadowing creates a new variable within a more restricted scope that uses the same name as some variable in the outer scope. See here for an example.
- It is best to always choose new names for local variables so that shadowing never happens. This way, the reader of the code will understand where the variables are used in an expression come from. See example.
10/25/2024
1.9.4 Pattern Matching
- The definition of a function can consist of multiple equations.
- The equations are matched in order against the arguments until a suitable one is found.
- This is called pattern matching.
- Consider this example:
greet:: String -> String -> String greet "Finland" name = "Hei, " ++ name greet "Italy" name = "Ciao, " ++ name greet "England" name = "How do you do, " ++ name greet _ name = "Hello, " ++ name
- The function
greet
generates a greeting given a country and name (which are both strings). - There is a special case for 3 countries: Finland, Italy, and England, and default case for everywhere else.
- I had to modify this code to work via
runghc Hello.hst
with amain = doprint
construct because I wasn’t using Prelude. Probably works better if I modify this to work in ghci.
1.9.4a the _ pattern
- In any case, the
_
is a special pattern that matches anything. Be careful to place it as the last case b/c it will search for everything. Which can be a problem if you use this as one of the early/middle cases.
1.9.4b the SHOW function from the standard library
show True
echoes back a variable value which is the bool True.- Similarly
show 3
prints the number type 3. - Consider this simple example:
describe :: Integer -> String describe 0 = "zero" describe 1 = "one" describe 2 = "an even prime" describe n = "the number " ++ show n
- Let’s try running the above interactively in ghci:
- Invoke ghci
- Use the multiple line command
:{
- Paste in the above commands
- Close the block with
:}
.
- Now, you can invoke the
describe
function as shown below, which should return the following output interactively.ghci> describe 0 "zero" ghci> describe 1 "one" ghci> describe 2 "an even prime" ghci> describe 82432 "82432"
1.9.4c Reimplementation of the unicorn73 login password from before
- More efficient using pattern matching and the (
_
) pattern.
10/26/2024
1.9.5 Recursion
- Link to section of the lecture
- In Haskell, many loops are implemented with recursion rather than the
for-loop
anddo-while
loops from Python, C/Java/similar languages. - Here is the first recursive function which computes a factorial.
factorial :: Int -> Int factorial 1 = 1 factorial n = n * factorial (n-1)
- Paste this into ghci, where ==> indicates what the expected output to be. Internal logic of what’s happening:
factorial 3 becomes 3 * factorial (3-1) 3 * factorial (2) 3 * 2 * factorial (1) 3 * 2 * 1 FINAL OUTPUT: 6
1.9.5a JH Explanation of factorial code
- There is a case statement asking: Is the input argument n an integer that equals 1?
- If yes, then output = 1.
- If no, then output = n times recursive function factorial(n - 1).
- In the above case, n = 3. This means:
- n = 3, which is not equal to 1. So therefore…
- factorial 3 = n times factorial (n minus 1)
- This simplifies to 3 times factorial (3 - 1)
- Which simplifies to 3 times factorial (2)
- Which simplifies to 3 times [ 2 times factorial (2 minus 1) ]
- Which simplifies to 3 times [ 2 times factorial (1) ]
- Now note that
factorial (1)
in the above expression is a case statement where if n=1, then output of factorial(1) = 1. So we can rewrite Step 6 above as:
- 3 times [ 2 times 1 ].
- 3 * 2 * 1 =
Final answer = 1
.
- 3 * 2 * 1 =
- When one tries to evaluate
factorial(-1)
, get this error:
<interactive>:8:11: error:
• No instance for (Num (Int -> Int)) arising from a use of ‘-’
(maybe you haven't applied a function to enough arguments?)
• In the expression: factorial - 1
In an equation for ‘it’: it = factorial - 1
10/28/2024
- Used pen and paper to work out the internal logic steps listed above from 10/26. Very interesting. In contrast, this is the implementation in JavaScript…
function factorial(n) {
// Check for invalid input
if (n < 0) {
return "Factorial is not defined for negative numbers";
}
// Base case
if (n === 0 || n === 1) {
return 1;
}
// Recursive case
return n * factorial(n - 1);
}
console.log(factorial(5)); // Output: 120
1.9.5b SquareSum example
- squareSum is a function that calculates: 1 squared + 2 squared + 3 squared + … + n squared
- aka
12 + 22 + … + n2
squareSum 0 = 0 squareSum n = n^2 + squareSum (n-1)
- aka
12 + 22 + … + n2
- Output is:
ghci> squareSum 1
1
ghci> squareSum 2
5
ghci> squareSum 3
14
ghci> squareSum 4
30
ghci> squareSum 5
55
1.9.5c Fibonacci Sequence Intro
- Definition: The Fibonacci Sequence starts with
1,1
. To get the next element of the sequence, sum the previous two elements of the sequence. - First several elements: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, …
1.9.5d Fibonacci Sequence (Slow Version)
- Compare to 2.1.2c Fibonacci Sequence (Fast Version) below.
fib
is a recursively called function in the below.
fibonacci 1 = 1
fibonacci 2 = 1
fibonacci n = fibonacci ( n-2 ) + fibonacci ( n-1 )
1.9.5e Fib Sequence in JavaScript
- From this page, there are multiple ways of implementing the Fibonacci sequence in JS.
- Here is the implementation using a for-loop:
function fibonacci(num) {
let num1 = 0;
let num2 = 1;
let sum;
if (num === 1) {
return num1;
} else if (num === 2) {
return num2;
} else {
for (let i = 3; i <= num; i++) {
sum = num1 + num2;
num1 = num2;
num2 = sum;
}
return num2;
}
}
console.log("Fibonacci(5): " + fibonacci(5));
console.log("Fibonacci(8): " + fibonacci(8));
- A more apples-to-apples comparison uses recursion in JS:
function fibonacci(num) {
if (num == 1)
return 0;
if (num == 2)
return 1;
return fibonacci(num - 1) + fibonacci(num - 2);
}
console.log("Fibonacci(5): " + fibonacci(5));
console.log("Fibonacci(8): " + fibonacci(8));
1.9.5f Tree-like visualization of Haskell Fibonacci function
- Consider the slow but easily understandable recursive tree-like structure generated by the v1 implementation of Fibonacci.
10/29/2024
1.10 All Together Now!
- Finally, here is a complete Haskell Module that uses ifs, pattern matching, local definitions, and recursion.
- This module concerns the Collatz conjecture.
- From 2017, Numberphile video. About 10 minutes long.
- 2021 YouTube video (22:00 long) by Veritasium
- Played around with using f_ as a prefix denoting functions. Initial digits and initial underscore don’t work. Also, initial capitalization like F_function doesn’t seem to work.
- Complete Haskell program labelled Collatz_b.hs. Note that one can independently call any of the three functions:
- f1_step: Given a positive integer as input, this function will output the next step in the Collatz sequence. If the input integer x is even, output integer = x/2. If the input integer x is odd, output integer = 3x+1.
- f2_collatz: Given a positive integer as input, this function will output the number of steps needed to turn that postive integer into 1 using the Collatz sequence. e.g., f2_collatz 20 outputs
10
which means there are 10 numbers from 20…1. Specifically, 20, 10, 5, 16, 8, 4, 2, 1. (Odd members of the sequence are listed in bold. In this case, only 5 is bold; every other member leads to x/2 in the next step.) If we count that how many numbers are in that sequence (including 20 at the beginning and 1 and the end), we get 10.- Similarly, f2_collatz 97 outputs
118
because it takes 118 steps to get from 97 to 1.
- Similarly, f2_collatz 97 outputs
- f3_longest: Given a positive integer as an input, this function returns the positive integer below that input that produces the longest Colltaz sequence, aka the largest stopping time, per this section of the Wikipdia article.
- The largest stopping time for numbers below 10 is 9 which requires 19 steps.
- The largest stopping time for numbers below 100 is 97 which requires 118 steps.
- The largest stopping time for numbers below 1000 is 871 which requires 178 steps.
- The largest stopping time for numbers below 10,00 is 6171 which requires 261 steps.
- Here is the program itself, using JH notation
module Collatz_d where
-- Original program found at https://haskell.mooc.fi/part1#all-together-now
-- Modified to clarify functions with f1_ prefix
-- and clarify variables with a v1_ prefix
-- Function 'f1_step()' generates the next step in the Collatz sequence
f1_step :: Integer -> Integer
f1_step x = if even x then v_down else v_up
where
v_down = div x 2
v_up = 3*x + 1
-- Function 'collatz(x)' computes how many steps it takes for the Collatz sequence
-- to reach 1 when starting from x
f2_collatz :: Integer -> Integer
f2_collatz 1 = 0
f2_collatz x = 1 + f2_collatz (f1_step x)
-- Function longest() finds the number with the longest Collatz sequence for
-- initial values between 0 and upperBound
f3_longest :: Integer -> Integer
f3_longest upperBound = f4_longest_helper 0 0 upperBound
-- helper function for Function longest()
f4_longest_helper :: Integer -> Integer -> Integer -> Integer
-- end of recursion, return longest length found
f4_longest_helper number _ 0 = number
-- recursion step: check if n has a longer Collatz sequence than the
-- current known longest
f4_longest_helper number maxlength n =
if len > maxlength
then f4_longest_helper n len ( n-1 )
else f4_longest_helper number maxlength (n-1)
where len = f2_collatz n
Update on 12/20/2024
- Used this ChatGPT dialogue to convert the Haskell Collatz functions into JS equivalents.
- Same dialogue examines comuptational efficiency of Haskell vs. JS here.
- JS code exists in
proj-2/_js-haskell/09...
js file. Tested and runs fine using node.
10/30/2024
1.11 Indentation
Two main rules
- Things that are grouped together start at the same column (depth in tabs/spaces).
- if an expression or equation needs to be split into multiple lines, increase the indentation.
General thoughts
- Indentation definitely matters, like in Python.
- Best to use spaces.
- If one can’t get indentation to work at first, try putting everything onton one long line
Example 1
-- this is ok
i x = let y = x+x+x+x in div y 5
-- but this is better
j x = let y = x+x
x+x
in div y 5
Example 2
-- this is ok
k = a + b
where a = 1
b = 1
-- but this is better
l = a + b
where
a = 1
b = 1
Example 3: bad
-- indentation not increased even though
-- expression split on many lines
i x = let y = x+x+x+x+x+x
in div y 5
Example 4: bad
-- indentation not increased even though
-- expression is split
j x = let y = x+x+x
+x+x+x
in div y 5
Example 5: bad
-- grouped things are not aligned
k = a + b
where a = 1
b = 1
Example 6: bad
-- grouped things are not aligned
l = a + b
where
a = 1
b = 1
Example 7: bad
-- where is part of the equation, so indentation needs to increase
l = a + b
where
a = 1
b = 1
11/01/2024
Lecture 2: Either You Die a Hero…
2.1 Recursion and Helper Functions
- Often, you’ll find you need helper variables in recursion to keep track of things.
- You can get them by defining a helper function with more arguments.
- Analogy: arguments of the helper function are variables you update in your loop.
2.1.1 First Example
2.1.1a Java
public String repeat String( int n, String str) {
String result = "";
while (n>0) {
result = result + str;
n = n-1;
}
return result;
}
2.1.1b Python
def repeatString( n, str ):
result = ""
while n > 0:
result = result + str
n = n-1
return result
- Validated that it works. To execute, copy and paste above into
repeat.py
file. Then invoke interactive python environment with this script loaded withpython -i repeat.py
. - To execute, type something like
repeatString(3, "Hello! ")
which outputs'Hello! Hello! Hello! '
.
2.1.1c Haskell version 1
repeatString n str = repeatHelper n str ""
repeatHelper n str result = if ( n==0 )
then result
else repeatHelper (n-1) str ( result ++ str )
2.1.1d More notes
- Verified that the above works. Example usage:
ghci> repeatString 5 "Abc"
generates output"AbcAbcAbcAbcAbc"
. - You might have noticed that the above implementations in Java and Python look a bit weird because they use
do-while
loops instead offor
loops. - This simply makes for a more straightforward conversion to Haskell.
- Let’s make a tidier Haskell (version 2) by using pattern matching rather than an if (version 1)
2.1.1e Haskell version 2
repeatString n str = repeatHelper n str ""
repeatHelper 0 _ result = result
repeatHelper n str result = repeatHelper ( n-1 ) str ( result ++ str )
2.1.2 Second Example: Fibonacci Numbers
2.1.2a Java implementation
public int fibonacci( int n ) {
int a = 0;
int b = 1;
while ( n>1 ) {
int c = a+b
a = b;
b = c;
n--;
}
return b;
}
2.1.2b Python implementation
def fibonacci(n):
a = 0
b = 1
while n>1:
c = a+b
a = b
b = c
n = n-1
return b
- Validated that it works. To execute, copy and paste above into
fib.py
file. Then invoke interactive python environment with this script loaded withpython -i fib.py
. - Within the python environment, enter sample commands like
fibonacci(n)
where n is an integer input. Will return the corresponding Fib number, e.g., forn = 1, 2, 3, 4, 5, 6
, the output will respectively be 1, 1, 2, 3, 5, 8.
2.1.2c Fibonacci Sequence (Fast Version)
- Fast version of Fibonacci numbers
- Compare to 1.9.5d Fibonacci (Slow Version) above.
- Primary function:
f1_fib
- Helper function:
f2_helper
f1_fib :: Integer -> Integer
f1_fib n = f2_helper 0 1 n
f2_helper :: Integer -> Integer -> Integer -> Integer
f2_helper a b 1 = b
f2_helper a b n = f2_helper b (a+b) (n-1)
11/14/2024
2.2 Guards
- One more piece of syntax
- Instead of the usual
if...then...else
syntax, esp. if there are multiple cases, we can use the conditional definition aka guarded definition structure. - This is somewhat like pattern matching in that there are multiple equations.
- But, one can have arbitrary code which decides which equation to use. Exmple:
f x y z | condition1 = result1 | condition2 = result2 | otherwise = miscellaneous_result
- Note that: otherwise is a haskell keyword.
2.2.1 Examples
- Here are some examples using guards.
- First off, we have a function that describes the given number.
- Note how important it is ot have the
Two
case before theEven
case.
- Note how important it is ot have the
describe :: Int -> String
describe n
| n==2 = "Two"
| even n = "Even"
| n==3 = "Three"
| n>100 = "Big!!"
| otherwise = "The number "++show n++" is odd and below 100."
- Here is a factorial, implemented with guards instead of pattern matching.
- Unlike the above pattern-matching version, this one does not loop forever when given negative inputs.
factorial n | n<0 = -1 | n==0 = 1 | otherwise = n * factorial (n-1)
- You can even combine guards with pattern matching.
- Here’s an implementation of a simple age guessing game:
guessAge :: String -> Int -> String guessAge "Griselda" age | age < 47 = "Too low!" | age > 47 = "Too high!" | otherwise = "Correct!" guessAge "Hansel" age | age < 12 = "Too low!" | age > 12 = "Too high!" | otherwise = "Correct!" guessAge name age = "Sorry, please guess another name and age."
11/15/2024
2.3 List
- So far, we’ve only worked with singleton values like numbers and booleans.
- Minor exception of strings which contain multiple characters.
- However, we need to handle data structures.
- The basic Haskell data structure is the list.
- Lists are used to store multiple values of the same type; aka they are homogenous.
- Example of a list literal:
[0,3,4,1+1]
. - A list type is written as
[Element]
, where Element is the type of the list elements. - Haskell lists are implemented as singly-linked lists. More on this topic later.
- More examples:
[True,True,False] :: [Bool] ["Oui", "moi"] :: [String] [] :: [a] -- more about this later [[1,2],[3,4]] :: [[Int]] -- a list of lists [1..5] :: [Int] -- uses range syntax -- value is [1,2,3,4,5]
2.3.1 List Operations
- The Haskell Standard Library comes with lots of functions that operate on lists.
- Here are some of the most important ones together with their types. (For now, imagine a represents any list.)
head :: [a] -> a -- returns the first element last :: [a] -> a -- returns the last element tail :: [a] -> [a] -- returns everything except the first element init :: [a] -> [a] -- returns everything except the last element take :: Int -> [a] -> [a] -- returns the n first elements drop :: Int -> [a] -> [a] -- returns everything except the n first elements (++) :: [a] -> [a] -> [a] -- lists are concatenated with the ++ operator (!!) :: [a] -> Int -> [a] -- lists are indexed with the !! operator reverse :: [a] -> [a] -- reverse a list null :: [a] -> Bool -- is this list empty? length :: [a] -> Int -- the length of a list
- Note: the last two operations (
null
andlength
) actually jave more generic types, but for now we are pretending that we can only use them on lists. - Lists can be compared with the
==
operator. - String is just an alisa for
[Char]
which means string is a list of characters. This means you can use all list operators on strings!ghci> :t "testString" output... "testString" :: [Char]
- Some list operations come from the module
Data.List
. You can import a module in code or in GHCi with the import Data.List syntax. Exampleghci> import Data.List ghci Data.List> sort[1,0,5,3] output... [0,1,3,5]
2.3.2 Examples
- Here are some examples of working with lists. In this case, instead of showing you output from GHCi, we just use the
==>
to illustrate what the expression evaluates to.
2.3.2.1 Indexing a List
2.3.2.2 Define a function that discards the 3rd and 4th elements using ‘take’ and ‘drop’
2.3.2.3 Rotating a list by taking the first element and moving it to the end
2.3.2.4 Reversing a list with ‘range’ syntax
2.4 A Word About Immutability
- Because Haskell is pure, it means that a function cannot change (aka change) their inputs.
- Mutation is a side effect; Haskell functions are only allowed output via their return value.
- This means that Haskell list functions always return a new list.
- Consider this…
GHCi, version 9.4.8: https://www.haskell.org/ghc/ :? for help
ghci> list = [1,2,3,4]
ghci> reverse list
[4,3,2,1]
ghci> list
[1,2,3,4]
ghci> drop 2 list
[3,4]
ghci> list
[1,2,3,4]
- The above immutabiity of lists (and variables) in Haskell amy seem inefficient. But it turns out to be both performant and useful. More on this later.
11/17/2024
2.5 A Word About Type Inference and Polymorphism
- So what does a type like
head :: [a] -> a
mean? - It means that given a list that contains elements of any type a, the return value will be of the same type a.
- In this type, a is called a type variable.
- Type variables are types that start with a lowercase letter like
a
,b
,newTypeVariable
. - A type variable means a type that is not yet known; aka a type that could be anything.
- Through the process of type inference (aka unification), type variables can be transformed into concrete types (where Int, Bool, etc. are concrete types).
2.5.0a Examples of Type Inference
- Let’s look at some examples.
- If we apply
head
to a list of booleans, type inference will compare the type of head’s input argument[a]
, with the type of the actual argument[Bool]
and deduce that thea
must be aBool
. - This means that the return type of
head
will in this case also be aBool
.
head :: [a] -> a
head [True, False] :: Bool
- The function
tail
takes a list, and returns a list of the same type. - If we apply
tail
to a list of booleans, the return value will also be a list of booleans.
tail :: [a] -> [a]
tail [True, False] :: [Bool]
- If types don’t match, we get a type error.
- Consider the operator
++
, which requires 2 list of the same type, as we can see from its type[a] -> [a] -> [a]
. - If we try to apply
++
to: (1) a list of booleans, plus (2) a list of chracters; we will get an error. See console output here:
ghci> "What, " ++ "moi?"
"What, moi?"
ghci> [True, False] ++ [True, False]
[True,False,True,False]
ghci> [True, False] ++ "moi?"
<interactive>:3:18: error:
• Couldn't match type ‘Char’ with ‘Bool’
Expected: [Bool]
Actual: String
• In the second argument of ‘(++)’, namely ‘"moi?"’
In the expression: [True, False] ++ "moi?"
In an equation for ‘it’: it = [True, False] ++ "moi?"
ghci>
- Type inference is really powerful. It uses the simple process of unification to get us types for practically any Haskell expression.
- Consider the following two functions:
f xs ys = [head xs, head ys]
g zs = f "Moi" zs
- We can ask GHCi for their types and see that what Haskell understands about the types for functions f and g.
- See console output from GHCi below:
ghci> :{
ghci| f xs ys = [head xs, head ys]
ghci| g zs = f "Moi" zs
ghci| :}
ghci> :tail f
unknown command ':tail'
use :? for help.
ghci> :t f
f :: [a] -> [a] -> [a]
ghci> :t g
g :: [Char] -> [Char]
ghci>
- Through type inference, Haskell has figured out that the two arguments to
f
must have the same type, since their heads get put into the same list. - The function g, which fixed one of the arguments of f to a stringgets a narrower type.
- Type inference has decided that the argument zs for g must also have a type
[Char]
(i.e., a list of Char’s). - Because otherwise, the type of f would not match the call to f.
- Type inference has decided that the argument zs for g must also have a type
2.5.1 Sidenote: Some Terminology
- In a type like
[Char]
, we call Char a type parameter. - A type like the list type that needs a type parameter is called a parameterized type.
- The fact that a function like head can be used with many different types of arguments is called polymorphism.
- The head function is said to be polymorphic.
- Note: there are many forms of polymorphism, and this Haskell form–which uses type variables–is called parametric polymorphism.
2.5.2 Sidenote: Type Annotations
- Since Haskell has type inference, you don’t need to give any type annotations.
- However, even though type annotations aren’t strictly required, there are good reasons to add them:
- Type annotations act as documentation.
- Type annotations act as assertions that the compiler checks to help you spot mistakes
- Type annotations let us give a narrower type to a function than Haskell might infer.
- A good rule of thumb is to give top-level definitions type annotations.
2.6 The Maybe Type
- In addition to the list type, Haskell has other parameterized types too.
- Let’s look at a very common and useful one: the
Maybe
type. - Sometimes, an operation doesn’t have a valid return value (e.g., division by zero). We have a few options:
- We can use an error value, like
-1
. This is a bit ugly, but not always possible. - We can throw an exception. This is impure.
- In some other languages, we would return a special null value that exists in (almost) all types. However, Haskell does not have a
null
built into the language.
- We can use an error value, like
- The solution Haskell offers us instead is to change our return type to a
Maybe
type. - This is pure, safe, and neat.
- The type
Maybe a
has two constructors: Nothing and Just. - Nothing is simply a constant.
- Just takes a parameter. See table in the Mooc course, Section 2.6.
- We can think of
Maybe a
as being a bit like[a]
except there can only be 0 or 1 elements, not more. - Alternatively, one can think of
Maybe a
as introducing a null value to the typea
.- On can analogize
Maybe Integer
as Haskell’s equivalent to Java’sOptional <Integer>
.
- On can analogize
- One can create Maybe values by either specifying Nothing or Just someOtherVlaue. For more, see console output in the Section 2.6.
2.7 Sidenote: Constructors
- As you can see above, we can pattern match on Just and Nothing–which are the constructors of
Maybe
. - Other constructors include constructors for Bool–which are True and False.
- The next lecture introduces constructors for the list type.
- Constructors can be used just like Haskell values.
- Note: constructors that have no input arguments are simply constants. Examples: Nothing and False are just constants.
2.8 The Either Type
- Sometimes it would be nice if one could add an error message or something to Nothing.
- This is why we have the Either type. The Either type takes two type arguments. The type Either a b has 2 constructors:
Left
andRight
. - Both take an argument;
Left
takes a andRight
takes b - See examples in table in Section 2.8.
- Note: Haskell has a convention where
Left
is for errors andRight
is for success. - Example here where the readInt function only knows a few numbers; it returns a descriptive error for all other numbers.
readInt :: String -> Either String Int
readInt "0" = Right 0
readInt "1" = Right 1
readInt s = Left ("Unsupported string: " ++ s)
- Note: the constructors of
Either
are calledLeft
andRight
because they refer to the left and right type arguments ofEither
.- Notice how in
Either a b
, a is the left argument and b is the right argument. - Thus,
Left
contains a value of typea
and likewiseRight
of typeb
.
- Notice how in
- Another example:
iWantAString :: Either Int String -> String
iWantAString (Right str) = str
iWantAString (Left number) = show number
- As you recall, Haskell lists can only contain elements of the same type. You can’t have a list like:
[1, "foo", 2]
. - However, one can use
Either
to represent lists that can contain two different types of values. - Example, where we track the number of people in a lecture, with the possibility of adding an explanation if a value is missing:
lectureParticipants :: [Either String Int]
lectureParticipants = [Right 10, Right 13, Left "lecturer was sick", Right 3]
2.9 The case-of Expression
- We’ve seen pattern matching in function arguments.
- But there’s also a way to pattern match in an expression. e.g.,
case <value> of <pattern> -> <expression>
of <pattern> -> <expression>
- As an example, let’s rewrite the describe example from lecture one using case.
describe :: Integer -> String
describe 0 = "zero"
describe 1 = "one"
describe 2 = "an even prime"
describe n = "the number " ++ show n
- Can be reimplemented as:
describe :: Integer -> String
describe n = case n of 0 -> "zero"
1 -> "one"
2 -> "an even prime"
n -> "the number " ++ show n
- A more interesting example is when the value we are pattern matching on is not a function argument. Parse country code into a country name; return ‘Nothing’ if code not recognized in this example:
parseCountry :: String -> Maybe String
parseCountry "FI" = Just "Finland"
parseCountry "SE" = Just "Sweden"
parseCountry _ = Nothing
flyTo :: String -> String
flyTo countryCode = case parseCountry countryCode of Just country -> "You're flying to " ++ country
Nothing -> "Sorry, you're not flying anywhere"
- console output
ghci> flyTo "FI"
"You're flying to Finland"
ghci> flyTo "DE"
"Sorry, you're not flying anywhere"
ghci>
- One could write the flyTo function using a helper function for pattern matching instead of using the case-of expression.
flyTo :: String -> String
flyTo countryCode = handleResult (parseCountry countryCode)
where handleResult (Just country) = "You're flying to " ++ country
handleResult Nothing = "Sorry, you're not flying anywhere."
- In fact, a case-of expression can always be replaced by a helper function. Variant #1. Given a sentence, decide whether it is a statement, question, or exclamation
sentenceType :: String -> String
sentenceType sentence = case last sentence of '.' -> "statement"
'?' -> "question"
'!' -> "exclamation"
_ -> "not a sentence"
- Variant #2 which is like Variant #1 before but uses a helper function instead of case-of syntax:
sentenceType sentence = classify (last sentence)
where classify '.' = "statement"
classify '?' = "question"
classify '!' = "exclamation"
classify _ = "not a sentence"
- Sample console output for either version of the sentenceType function.
11/19/2024
2.9.1 When to Use Case Expressions
- You might be asking “What is the point of having another pattern matching syntax?”
- Let’s discuss some pros and cons
- First and most importantly, case expressions enable us to pattern match against function outputs. We might want to write an early morning motivational messages to get lazy Haskellers going:
motivate :: String -> String
motivate "Monday" = "Have a nice week at work!"
motivate "Tuesday" = "You're one day closer to weekend!"
motivate "Wednesday" = "3 more day(s) until the weekend!"
motivate "Thursday" = "2 more day(s) until the weekend!"
motivate "Friday" = "1 more day(s) until the weekend!"
motivate _ = "Relax! You don't need to work today. :)"
- Using a case expression, we can run a helper function against the argument and pattern match on the result:
motivate :: String -> String
motivate day = case distanceToSunday day of
6 -> "Have a nice week at work!"
5 -> "You're one day closer to the weekend!"
n -> if n > 1
then show (n - 1) ++ " more day(s) until the weekend!"
else "Relax! You don't need to work today!"
- Btw, here is a 3rd implementation using guards:
motivate :: String -> String
motivate day
| n == 6 = "Have a nice week at work!"
| n == 6 = "You're one day closer to the weekend!"
| n > 1 = show (n - 1) ++ " more day(s) until the weekend!"
| otherwise = "Relax! You don't need to work today!"
where n = distanceToSunday day
- Soon, we will see how to define distanceToSunday using equations and case expressions.
- Secondly, if a helper function needs to be shared among many patterns, then equations won’t work. e.g.:
area :: String -> Double -> Double
area "square" x = square x
area "circle" x = pi * square x
where square x = x * x
- This won’t compile because the where clause only appends in the
circle
case; so the square helper function is not available in thesquare
case. - On the other hand, we can write:
area :: String -> Double -> Double
area shape x = case shape of
"square" -> square x
"circle" -> pi * square x
where square x = x*x
- Third, case expressions may help to write more concise code in a situation where a (long) function name would have to be repeated multipe times using equations.
- Consider this naive, wordy implementation:
distanceToSunday :: String -> Int
distanceToSunday "Monday" = 6
distanceToSunday "Tuesday" = 5
distanceToSunday "Wednesday" = 4
distanceToSunday "Thursday" = 3
distanceToSunday "Friday" = 2
distanceToSunday "Saturday" = 1
distanceToSunday "Sunday" = 0
- Compare with this more concise implementation using case isyntax:
distanceToSunday :: String -> Int
distanceToSunday d = case d of
"Monday" -> 6
"Tuesday" -> 5
"Wednesday" -> 4
"Thursday" -> 3
"Friday" -> 2
"Saturday" -> 1
"Sunday" -> 0
- The above 3 benefits make the case expression a versatile tool in the Haskeller’s toolbox.
2.10 Recap: Pattern Matching
2.11 Quiz
11/20/2024
Lecture 3: Catamorphic
3.1 Functional Programming, At Last!
- In Haskell, a function is a value, just like a number or a list is.
- Functions can be passed as parameters (arguments) just like ‘regular variable’ can be.
- Consider this simple example, with function applyTo1 which accepts as input a function of type
(Int->Int)
, applies it to the number 1, and returns the result:
applyTo1 :: (Int -> Int) -> Int
applyTo1 f = f 1
- Let’s define a simple function of type
Int->Int
and see the function applyTo1 i action.
addThree :: Int -> Int
addThree x = x + 3
applyTo1 addThree
==> addThree 1 -- Step 1
==> 1 + 3 -- Step 2
==> 4 -- Step 3: final output
- Let’s go back to the type annotation for
applyTo1
applyTo1 :: (Int -> Int) -> Int
- The parentheses are needed because the type Int -> Int -> Int would be the type of a function taking two Int arguments. More on this later.
- Let’s look at a slightly more interesting example. This time, we’ll implement a polymorphic function
doTwice
. - Note how we can use it various types of values and functions.
doTwice :: (a -> a) -> a -> a
doTwice f x = f(f x)
doTwice addThree 1
==> addThree (addThree 1) -- output
==> 7 -- output
doTwice tail "abcd"
==> tail (tail "abcd") -- output
==> "cd" -- output
11/30/2024
- Additional experiments.
- Note: Need to use the
tail
andinit
functions b/c they return arrays rather than singleton elements. Otherwise,addTwice
will choke because it expects the same array input and output. - Console output
ghci> doTwice init "abcd"
"ab"
ghci> init "abcd"
"abc"
ghci> tail "abcd"
"bcd"
ghci> tail "abcdefg"
"bcdefg"
ghci> doTwice tail "abcdefg"
"cdefg"
ghci> doTwice doTwice tail "abcdefg"
"efg"
ghci> doTwice doTwice tail "abcdefg"
- JH note: In GHCi, one can reuse the same
x
variable in the same ghci block under different function declarations, akax
is reused in addThree() and doTwice().
:{
applyTo1 :: (Int -> Int) -> Int
applyTo1 f = f 1
addThree :: Int -> Int
addThree x = x + 3
doTwice :: (a -> a) -> a -> a
doTwice f x = f (f x)
:}
- More
makeCool :: String -> String
makeCool str = "WOW" ++ str ++ "!"
doTwice makeCool "Haskell"
==> "WOW WOW Haskell!!" -- output
11/21/2024
3.1.1. Functional Programming on Lists.
- That was a bit boring. Luckily there are many useful list functions that take functions as arguments.
- By the way, functions that take functions as arguments (or return functions) are often called higher-order functions.
- 12/16/2024 update. Review: basic functions accept simple types. If a Special_Function accepts “nested” or multiple levels of “sub-nested” functions, that Special_Function is called a **higher-order function.
- The most famous of these list-processing higher-order functions is map.
- It gives you a new list by applying the given function to all elements of a list.
map :: (a -> b) -> [a] -> [b]
map addThree [1,2,3]
==> [4,5,6] -- output
- The partner in crime for map is filter.
- Instead of transforming all elements of a list, filter drops some elements of a list and keeps others.
- In other words, filter selects the elements from a list that fulfill a condition:
filter :: (a -> Bool) -> [a] -> [a]
- Here’s an example: selecting the positive elements from a list:
positive :: Int -> Bool
positive x = x>0
filter positive [0,1,-1,3,-3]
==> [1,3] -- output
- Note how both the type signatures of map and filter use polymorphism.
- They work on all kinds of lists.
- The type of map even uses two type parameters!
- Here are some examples of type inference using map and filter.
onlyPositive xs = filter positive xs
mapBooleans f = map f [False,True]
- Console output…
:t onlyPositive...
ghci> :t onlyPositive onlyPositive :: [Int] -> [Int] ghci> :t mapBooleans mapBooleans :: (Bool -> b) -> [b] ghci> :t mapBooleans not mapBooleans not :: [Bool]
- One more thing: remember how constructors are just functions? That means you can pass them as arguments to other functions!
wrapJust xs = map Just xs
- Console output…
ghci> :t wrapJust
wrapJust :: [a] -> [Maybe a]
ghci> wrapJust [1,2,3]
[Just 1, Just 2, Just 3]
Interlude
- 12/16/2024 update. Note that it’s just a programming convention to use
xs
to indicate a list of values instead of the simpler x. But per this ChatGPT dialogue, it’s the map function in the function declarationwrapJust xs = map Just xs
that makes haskell expect a list as input.
wrapJust x = map Just x -- option 1
wrapJust apple = map Just apple -- option 2
wrapJust bananas = map Just bananas -- option 3
- You could equally write the wrapJust() function with any literal tokens and it would always act the same b/c the embedded map() function causes the enclsoing wrapJust() function to expect a list as an argument.
- But for readability for other Haskell programmers, we follow the
xs
= a list of elementsx
convention.
3.1.2 Examples of Functional Programming on Lists
12/01 - 12/03
3.1.2.1 Palindromes
3.1.2.1a Palindromes function
- How many “palindrome numbers” are between 1 and n?
-- a predicate that checks if a string is a palindrome
palindrome :: String -> Bool
palindrome str = str == reverse str
-- palindromes n takes all numbers from 1 to n, converts them to strings using show, and keeps only palindromes
palindromes :: Int -> [String]
palindromes n = filter palindrome (map show [1..n])
palindrome "1331" ==> True
palindromes 150 ==>
["1", "2", "3", "4", "5", "6", "7", "8", ... "141"]
length (palindromes 9999) ==> 198
3.1.2.1b JH version of Palindromes function
- Here is my version which renames the 2 functions for easier reading (f1, f2,…).
- See chatgpt log for granular explanation.
-- f1_check -- a predicate that checks if a string is a palindrome
f1_check :: String -> Bool
f1_check str = str == reverse str
-- f2_palindromes n takes all numbers from 1 to n, converts them to strings using show, and keeps only palindromes
f2_palindromes :: Int -> [String]
f2_palindromes n = filter f1_check (map show [1..n])
3.1.2.1c JS implementation of Palindromes function
- To refresh myself on how Haskell’s expressions based language works, I used ChatGPT to help me write a simple JS equivalent of the
f1_check
haskell helper function above.
function isPalindrome(str) {
// Convert the string to lowercase and remove non-alphanumeric characters
const cleanStr = str.toLowerCase().replace(/[^a-z0-9]/g, '');
// Compare the string with its reversed version
var result = cleanStr === cleanStr.split('').reverse().join('');
return result;
}
//--------
// Some simple tests
console.log(isPalindrome("aba")); // true
console.log("--- ");
console.log(isPalindrome("abac")); // false
- I then queried ChatGPT about idiomatic Haskell. Do I need to have an equivalent to JS’s intermediate variables and
return result;
statement?- The answer is No, because the result of the expressions is constantly ‘coming out’ of the function in Haskell.
- For more complex functions or expressions, it’s ok to use
let
bindings orwhere
clauses to clarify intent. This is proper idiomatic Haskell. - For more, see this chatgpt convo
3.1.2.1d using ‘map’ and ‘filter’ in f2_palindromes
- I used this ChatGPT dialogue to break down the
f2_palindromes
function. Let’s dive in deeper - From the same ChatGPT page:
- map f() list applies the f() function to each element of the list.
- filter pred list keeps elements
x
in list such thatpred x == True
for each elementx
in the list.
- So… let’s deconstruct this Haskell code:
f2_palindromes n = filter f1_check (map show [1..n])
- We construct a range of integers with this syntax
[1..n]
. - The show() function converts an input into a string representation. Meanwhile, map() applies that function to every element to a list. So
map show [1,2,3]
converts each element into the string equivalent. The output: [“1”, “2”, “3”] - f1_check() as discussed earlier, returns True if the input is a string palindrome; False otherwise. Meanwhile, the filter() function only keeps the elements of the list for which the input evaluates to true.
- Therefore
filter f1_check [ *list of numbers represented as strings* ]
, outputs a new list that only has elements which are palindromes.
- We construct a range of integers with this syntax
12/04/2024
JH Interlude: learning more about the show() function
- From this ChatGPT answer
- To execute any of the following examples commands in GHCi, just enter in the code surrounded by the usual
:{
and:}
and then, in the ghci cmd prompt, type “main”.
Example 1: show() and basic data types
main :: IO ()
main = do
print (show 42) -- Converts Int to String, output "42"
print (show 3.14) -- Converts a Float to a String, output "3.14"
print (show True) -- Converts a Bool to a String, output "True"
Example 2: show() and lists
main :: IO ()
main = do
print (show [1, 2, 3]) -- Converts a list of Ints to a String; output "[1,2,3]"
print (show "Hello") -- Converts a String (already a list of Char) to a String representation; output "\"Hello\""
Example 3: show() and custom data types
- Note, below example is an example of automatically deriving show(). See ChatGPT for additional example on manually deriving show().
data Point = Point Int Int deriving Show
main :: IO ()
main = do
let p = Point 3 4
print (show p) -- Converts a Point value to a String; output: "Point 3 4"
Example 4: show() and IO
main :: IO ()
main = do
let x = 5
putStrLn ("JH says the value of x is: " ++ show x ++ "!")
-- Output in console: JH says the value of x is: 5!
-- To execute command at the GHCi prompt, just type "main"
12/04/2024
3.1.2.2 Counting words that start with ‘a’
- How many words in a string start with the letter “a”?
- This uses the function words from tme module Data.List that splits a string into words.
countAWords :: String -> Int
countAWords string = length (filter startsWithA (words string))
where startsWithA s = head s == 'a'
countAWords "does anyone want an apple?"
==> 3 -- output
3.1.2.3 tails() from the Data.List library
- The function tails from Data.List returns the list of all suffixes (“tails”) of a list.
- We can use tails for many string processing tasks.
- Here is sample program and output from GHCi. Note that we need to import Data.List first b/c that contains tails() (not to be confused with tail()).
ghci> :{
ghci| import Data.List
ghci| main :: IO ()
ghci| main = do
ghci| let x = tails "echo"
ghci| putStrLn("Output is: " ++ show x)
ghci| :}
ghci>
ghci> main
Output is: ["echo","cho","ho","o",""]
- Note, per this ChatGPT, this alternate implementation also works (invoke in GHCi with
main
like above):
import Data.List (tails)
main :: IO ()
main = print (tails [1, 2, 3])
12/05/2024
3.1.2.3 substringsOfLength() using tails(), map(), and take()
3.1.2.3a Preliminaries
- Here’s an example where we find what characters come after a given character in a string.
- We use tails, map and take to get all substrings of a certain length.
import Data.List (tails)
substringsOfLength :: Int -> String -> [String]
substringsOfLength n string = map shorten (tails string)
where shorten s = take n s
3.1.2.3b whatFollows()
- Now that the helper function substringsOfLength() is completed, we can find all occurrences of character
c
in the strings
, and then output thek
letters that come after these occurrences. - Paste this code after the above substringsOfLength code above:
whatFollows :: Char -> Int -> String -> [String]
whatFollows c k string = map tail (filter match (substringsOfLength (k+1) string))
where match sub = take 1 sub == [c]
- See execution here:
ghci> whatFollows 'a' 2 "abracadabra"
["br","ca","da","br",""]
3.1.2.3d Explanation from ChatGPT
- Link to ChatGPT answer
3.1.2.3e JS implementation
// Function to generate all substrings of a given length
function substringsOfLength(n, string) {
const result = [];
for (let i = 0; i <= string.length - n; i++) {
result.push(string.slice(i, i + n));
}
return result;
}
// Function to find substrings of length `k` that follow the character `c`
function whatFollows(c, k, string) {
const substrings = substringsOfLength(k + 1, string); // Generate substrings of length `k + 1`
return substrings
.filter(sub => sub[0] === c) // Keep only substrings that start with `c`
.map(sub => sub.slice(1)); // Remove the first character (`c`)
}
// Example usage
const result = whatFollows('a', 2, 'abracadabra');
console.log(result); // Output: ['br', 'ca', 'da', 'br']
3.1.2.3f explanation of JS function chaining coding style
- Spent some time with ChatGPT understanding the syntax of chaining 2 functions. Final
return substrings
statements with .filter and .map is just a stylistic choice for bettern JS readability. Functionally the same as:
return substrings.filter(sub => sub[0] === c).map(sub => sub.slice(1));
3.2 Partial Application
- When using higher-order functions, you can find yourself defining lots of small helper functions, like addThree or shorten from the previous examples.
- Rather than laboriously defining all these functions, the concept of partial application simplifies our task. This works for all functions in Haskell.
3.2.1 Example 1 – between()
between :: Integer -> Integer -> Integer -> Bool
between lo high x = x < high && x > lo
- Sample usage:
ghci> between 3 7 5
True
ghci> between 3 6 8
False
ghci> between 5 50 23
True
ghci> between 5 50 87
False
3.2.2 Example 2 – map() . between()
- Now consider this composite aka higher-order function:
ghci> map (between 1 8) [2, 3, 4, 7, 11]
[True,True,True,True,False]
ghci>
- We can give
between
fewer arguments and still get back new functions, just like in the example withadd
above.
3.2.3 Currying
- To examine this more carefully, consider this console output:
ghci> :{
ghci| between :: Integer -> Integer -> Integer -> Bool
ghci| between low high x = x < high && x > low
ghci| :}
ghci> between 3 10 6
True
ghci> between 3 10 11
False
ghci> :t between
between :: Integer -> Integer -> Integer -> Bool
ghci> :t between 1
between 1 :: Integer -> Integer -> Bool
ghci> :t between 1 2
between 1 2 :: Integer -> Bool
ghci> :t between 1 2 3
between 1 2 3 :: Bool
ghci>
- As you can see, the process of currying allows us to incrementally reduce the multiple input mapping into a single input function.
- What’s really happening is that when one writes
Int -> Int -> Int -> Bool
, it really meansInt -> [Int -> (Int -> Bool)]
. - That is, a multi-argument function is just a function that returns a function.
- Similarly, an expression like
betweeen 1 2 3
can be thought of as[(between 1) 2] 3
. - Actually, passing multiple input arguments to a function is really happening via multiple single-argument calls.
- As mentioned above, currying is what makes partial applications possible.
- Named after logician, mathematician, and namesake Haskell B. Curry.
3.2.4 Example 3 – map() . drop()
- Another example using a partial application with
map
:
map (drop 1) ["Hello", "World!"]
- Sample output:
ghci> map ( drop 1 ) ["Hello", "World!" ]
["ello","orld!"]
ghci> map ( drop 1 ) ["Jeff", "Hwang" ]
["eff","wang"]
ghci> map ( drop 2 ) ["Jeff", "Hwang" ]
["ff","ang"]
ghci> map ( drop 3 ) ["Jeff", "Hwang" ]
["f","ng"]
3.2.5 Operator Sections aka Partially Applied Operators
- In addition to normal functions, partial application also works with operators.
- These partially applied operators are also called operator sections aka sections.
- With operators you can choose whether you apply the left or the right argument. See left example
(*2)
and right example(2*)
below.
ghci> map (*2) [1,2,3]
[2,4,6]
ghci> map (2*) [1,2,3]
[2,4,6]
ghci> map (1/) [1,2,3,4,5,6]
[1.0,0.5,0.3333333333333333,0.25,0.2,0.16666666666666666]
ghci>
12/09/2024
3.3 Prefix and Infix Notations
- Normal Haskell operators are applied with prefix notation, which is just a fancy way of sayhing that the function name comes before the arguments.
- In contrast, operators are applied with infix notation; aka the name of the function occurs between the arguments.
- An infix operator can be changed to a prefix function by adding parenthese around it, e.g.,
1+2
is infix. To change that to prefix, rewrite it as(+) 1 2
. - This is useful when an operator needs to be passed as an argument to another function.
3.3.1 Example zipWidth()
- Consider the function zipWith which accepts 2 lists, a binary function, and joins the lists using that function.
- We can use
zipWith (+)
to add the elements pf List A with the elements of List B, element-by-element.
ghci> :t zipWith
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
ghci> zipWith (+) [1,2,3] [9,10,12]
[10,12,15]
ghci>
- Without the ability to change an infix operator like + into a prefix function
(+)
, we’d have to use a helper function like add(). - Note that omitting the parentheses leads to a type error.
ghci> zipWith (+) [0,0,0] [1,2,3]
[1,2,3]
ghci> zipWith + [0,0,0] [1,2,3]
<interactive>:3:11: error:
• Couldn't match expected type: [a1]
-> (a -> b -> c) -> [a] -> [b] -> [c]
with actual type: [a0]
• The function ‘[0, 0, 0]’ is applied to one value argument,
but its type ‘[a0]’ has none
In the second argument of ‘(+)’, namely ‘[0, 0, 0] [1, 2, 3]’
In the expression: zipWith + [0, 0, 0] [1, 2, 3]
• Relevant bindings include
it :: (a -> b -> c) -> [a] -> [b] -> [c]
(bound at <interactive>:3:1)
ghci>
3.3.2 Backticks (`) turn prefix functions into infix operators
div 6 2
means “Divide 6 by 2” with result:3
- Backticks mean we can turn that prefix functions like div into infix operators as long as these functions are binary – aka they require/accept 2 arguments as input:
6 `div` 2 -- output: 3
- Another example. Consider these
map
expressions and output
ghci> map (+1) [1,2,3]
[2,3,4]
ghci> map (+2) [1,2,3]
[3,4,5]
ghci> map (*2) [1,2,3]
[2,4,6]
ghci> map (2*) [1,2,3]
[2,4,6]
ghci> map (3*) [1,2,3]
[3,6,9]
ghci>
- The following syntax uses backticks to convert the
map
function into an operator that does the same thing:
ghci> (+1) `map` [1,2,3]
[2,3,4]
ghci> (+2) `map` [1,2,3]
[3,4,5]
ghci> (*2) `map` [1,2,3]
[2,4,6]
ghci> (2*) `map` [1,2,3]
[2,4,6]
ghci> (3*) `map` [1,2,3]
[3,6,9]
ghci>