Haskell Notes, page 3
12/09/2024
3.4 Lambdas
- The last “spanner” we need in our functional programming toolbox is the lambda (λ).
- Lambda expressions are anonymous functions.
- Lambdas let us rewrite a function like:
let big x = x>7 in filter big [1,10,100]
- into this:
filter (\x -> x>7) [1,10,100]
-
In other words, we don’t need to name or define the helper function (e.g.,
let big x = ...
) - Note that in Haskell, the backslash character (\) represents the lambda (λ).
- The Haskell expression
\x -> x + 1
is trying to mimic the mathematical notation λx . x+1 - Note! You never need to use a lambda expression. You always have the option of defining a function normally using let or where.
- JavaScript equivalent is the arrow function
x => x+1
. - Python equivalent is
lambda x: x+1
. - For more on arrow functions within JS, see David Flanagan, Chapter 8, section 8.1.3 Arrow Functions. P. 344 of 1245.
- See also prelim on p. 339. ES6 introduced arrow functions to JS.
- JH: It seems to me that only a small part of JS ‘arrow functions’ is the syntatic sugar of writing a concise function quickly and anonymously (no name to function in function declaration). The bigger aspect is ability to compose functions, pass functions as arguments / input parameters into other functions to make JS accept a functional programming paradigm. Note the bind() aspect of JS programming.
- Mooc says ‘lambda expressions are quite powerful constructs which have a deep theory of their own, known as lambda calculus.’
- Some even consider purely functional programming languages such as Haskell to simply be typed extensions of lambda calculus with extra syntax.
12/11/2024
3.5 Sidenote: The . and $ operators
- The two most common operators in Haskell codebases are
.
and$
.
3.5.1 The . operator – Function Composition
(.) :: (b -> c) -> (a -> b) -> a -> c
- And this is what it does:
(f.g) x -- output: f (g x)
- Examples:
double x = 2*x
quadruple = double . double -- computes 2*(2*x) == 4*x
f = quaduple . (+1) -- computes 4*( x+1 )
g = (+1) . quadruple -- computes 4*x + 1
third = head . tail . tail -- fetches the third element of a list
12/13/2024
3.5.1.2 Reimplementing doTwice() with .
- Note that one can use the
doTwice
both as an applied only to a fucnction, or as applied to a function + applied as a value.
doTwice :: (a -> a) -> a -> a
doTwice f = f . f
let ttail = doTwice tail
in ttail [ 1, 2, 3, 4 ] -- output [3,4]
(doTwice tail) [1,2,3,4] -- output: [3,4]
doTwice tail [1,2,3,4] -- output: [3,4]
- Often function composition is not used when defining a new function; but instead to avoid defining a helper function.
- Consider the difference between these 2 expressions:
let notEmpty x = not (null x)
in filter notEmpty [[1,2,3],[],[4]]
--output: [[1,2,3],[4]]
filter ( not . null ) [[1,2,3],[],[4]]
--output: [[1,2,3],[4]]
12/18/2024
3.5.2 The $ operator more details
- Consider it’s type:
ghci> :t ($)
($) :: (a -> b) -> a -> b
- It takes as an input/argument something of type
(a->b)
and returns a value of type b. - In other words, $ is a function application operator.
- The expression
f $ x
is the same asf x
. - This seems pretty useless, but it means that the $ operator can be used to eliminate parentheses!
- These 2 expressions are the same:
head (reverse "abcd") -- Expression 1
head $ reverse "abcd" -- Expression 2
- A more impressive example:
-- v1: Lots of parentheses
reverse (map head (map reverse (["Haskell", "pro"] ++ ["dodo", "lyric"])))
-- v2: Rewritten with '.'
(reverse . map head . map reverse) (["Haskell","pro"] ++ ["dodo","lyric"])
-- v3: Rewritten with '.' and '$'
reverse . map head . map reverse $ ["Haskell","pro"] ++ ["dodo","lyric"]
- Sometimes the operators
.
and$
are useful as fnctions in their own right. - For example, a list of functions can be applied to an argument using a map and a section of
$
:
--Expression 1
map ($"string") [reverse, take 2, drop 2]
--Expression 2
[reverse $ "string", take 2 $ "string", drop 2 $ "string"]
--Expression 3
[reverse "string", take 2 "string", drop 2 "string"]
- See this ChatGPT to understand how/why the above three expressions are identical.
- If this seems complicated, don’t worry. You don’t need to use
.
or$
in your own code until you are comfortable with these infix operators. - For more on reading other people’s Haskell that uses
.
and$
, check out this article
3.6 Example: Rewriting whatFollows()
- Now let’s rewrite
whatFollows
example using these tools. The original version:
substringsOfLength :: Int -> String -> [String]
substringsOfLength n string = map shorten (tails string)
where shorten s = take n s
whatFollows :: Char -> Int -> String -> [String]
whatFollows c k string = map tail (filter match (substringsOfLength (k+1) string))
where match sub = take 1 sub == [c]
- To get started, let’s get rid of the helper function
substringsOfLength
and move all the code towhatFollows
.
1/08/2025
- Next, paste in output from rewritten version with f1, f2, …
- and then reimplement in Haskell
1/13/2025
- Furthermore, Haskell Types are going to look a lot like Classes in Java/C++ or other OO-languages. But don’t be fooled they are not classes. Probably Types are most similar to Interfaces.
1/22/2025
- Let’s re-examine the original implementation of helper function
substringsOfLength()
and original version ofwhatFollows()
from 12/05/2024. There was a JS implementation from that date as well as this ChatGPT explanation.
1/23/2025
- starting from section 3.6, note that the Mooc has a copy of the original whatFollows(). see
3whatFollows.hs
. - With some help from Claude, got verion 0 working. remember to use single quotes (*
'
) for individual characters and double quotes ("
) for strings.- e.g., the proper way to call the function in GHCi is
whatFollows 'u' 4 "superman"
. Which outputsperm
.
- e.g., the proper way to call the function in GHCi is
- Now going through v0 line by line.
- After some more Claude dialogue, learned more about how the
string
andn
work together in the subStringsOfLength function.shorten
is a generic function and it doesn’t necessarily key off ofs
. Instead ofs
, one can place any parameter there, it doesn’t need to be a string. It’s a higher order function (I think).
1/25/2025
- Rewriting v0 with function names:
import Data.List
f2_substringsOfLength :: Int -> String -> [String]
f2_substringsOfLength n string = map f3_shorten (tails string)
where f3_shorten s = take n s
f1_whatFollows :: Char -> Int -> String -> [String]
f1_whatFollows c k string = map tail (filter f4_match (f2_substringsOfLength (k+1) string))
where f4_match sub = take 1 sub == [c]
- After a dialogue with Claude, decided there are:
Five ways to declare a function in Haskell
- Top-Level Declaration
functionName :: Type1 -> Type2 -> OutputType functionName arg1 arg2 = expression
- Where Clause
mainFunction args = expression where helperFunction args = helperExpression
- Let Binding
mainFunction = let localFunction args = expression in localFunction args
- Case Expression
function args = case expression of pattern1 -> result1 pattern2 -> result2
- Lambda/Anonymous Function
\arg1 arg2 -> expression -- Example usage: map (\x -> x + 1) [1,2,3]
- Remember, Method 2 with the where clause means that function is only available within that the larger function it is embedded in. I got into trouble trying to use
f3_shorten
separately in GHCi because it doesn’t mean anything outside off2_subStringsOfLength
.
- Remember, Method 2 with the where clause means that function is only available within that the larger function it is embedded in. I got into trouble trying to use