Before we learn any syntax or anything. Let's first explore the most important question to learn anything.
That is to learn why learn Haskell?
Let's explore what makes Haskell worth learning and what makes it unique.
1. Pure Functional Programming:
This is the most important point.
Haskell is purely functional programming, which means it treats computation as the evaluation of mathematical functions. Pure functional programming means that functions are pure, having two main properties:
They always produce the same output for the same input
More predictable code which has no side effects like modifying global state
Let's see what it means:
In imperative languages, functions can often have side effects, which can lead to unexpected behaviour. In Haskell, functions are predictable.
Example:
//function add(x: number, y: number): number
add :: Int -> Int -> Int
//const add = (x, y) => x + y;
add x y = x + y
later we will understand keywords and syntax i.e :: . For now just try to get the gist.
Int -> Int -> Int means:
The function takes two Int parameters
It returns an Int
The arrow (->) is used to separate parameter types and the return type
we don't need parentheses around the parameters.
we don't use an arrow or the return keyword
The sign= is used to define the function body.
For easy explanation we will compare Haskell to Typescript:
Type system:
Similarities: Both Haskell & Typescript are statically typed, meaning types are checked at compile time
Difference: Haskell's type system is more powerful & expressive
Function Syntax:
Haskell's syntax are more concise. Params are listed after the function name, separated by spaces.
Typescript uses Javascript functions syntax with added type annotations.
Curring:
In Haskell currying is automatic
addFive :: Int -> Int
addFive = add 5
Typescript, you need to manually implement curried function.
const addFive = (y: number): number => add(5, y);
What is Currying?
The names comes from Haskell Curry, a mathematician and logician.
Currying is a technique in functional programming where a function that takes multiple arguments is transformed into a sequence of functions, each taking single arguments.
Currying is a way of structuring functions that makes them more flexible.
Let's use a real world analogy:
Imagine you have a coffee machine that makes different types of coffee drinks.
A non-curried version would be like a machine where you input all ingredients at once (coffee, milk, sugar) and get your drink.
function makeCoffee(coffee, milk, sugar) {
return `coffee: ${coffee} + ${milk} milk ${sugar} sugar.`;
}
const coffee = makeCoffee("Espresso", "100ml", "1 spoon");
Your coffee is: Espresso with 100ml milk and 1 spoon sugar.
A curried version would be like a machine where you:
Here a curried version is more specialized than the non curried one.
Key points to take away:
In curried version, instead of taking all arguments at once, the function takes one argument & return a new function that takes new argument.
This allow us to partially apply function. For example:
**************** non curried function *************
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // Outputs: 5
**************** curried function ****************
function add(a) {
return function(b) {
return a + b;
}
}
console.log(add(2)(3)); // Outputs: 5
const addTwo = add(2); // This creates a new function
console.log(addTwo(3)); // Outputs: 5
console.log(addTwo(4)); // Outputs: 6
In Haskell, this currying is automatic. Every function is curried by default:
add :: Int -> Int -> Int
add x y = x + y
addTwo :: Int -> Int
addTwo = add 2
main = do
print(add 2 3) -- Outputs: 5
print(addTwo 3) -- Outputs: 5
Currying breaks down a function that takes multiple arguments into a series of function, each taking a single argument which make your code more flexible and easier to reuse.
2. Concurrency & Easier Parallel Programming.
Because the pure function don't have any side effects and always produce the same output for the same input, they can be easily parallelized without worrying about shared mutable state.
Immutability: Data in Haskell is immutable by default, which means once a value is created, it can't be changed.
This functional nature makes it easier to write parallel code compared to imperative languages.
These properties makes it safer to run operations in parallel because you don't have to worry about different threads interfering with each other's data or producing inconsistent results.
Thus makes it easier to write concurrent & parallel programs.
import Control.Parallel.Strategies
// type signature of our function
parallelMap :: (a -> b) -> [a] -> [b]
parallelMap f xs = map f xs `using` parList rseq
-- Usage
result = parallelMap (\x -> x * x) [1..1000000]
Here parallelMap function
takes
function that goes from a to b
tales a list of a
returns a new list [b], where each element of a has been transformed by the function.
We will discuss this syntax at the end if you don't understand.
3. Strong static typing.
-- Define a func that takes two integers and returns their sum
addIntegers :: Integer -> Integer -> Integer
addIntegers x y = x + y
-- Define a function that takes two floating-point numbers and returns their sum
addFloats :: Float -> Float -> Float
addFloats x y = x + y
4. Mathematical foundations:
Haskell was designed with strong mathematical principles in mind. Learning haskell can help develop a more mathematical way of thinking about programming problems.
Complex ideas can often be expressed very concisely.
Focusing on what things are, rather than sequences of steps( declarative vs imperative)
Has powerful abstractions that can model complex systems elegantly.
Using function composition to build complex behaviours from simple parts.
5. Lazy Evaluation:
Allows us to define potentially infinite computation & data structures, but only compute the parts you actually use. This can lead to more efficient programs & allow for elegant solutions to certain problems.
-- A function that prints a message and returns a value
expensiveFunction :: Int -> Int
expensiveFunction x =
trace ("Computing expensive function for " ++ show x) (x * 1000)
where trace = Debug.Trace.trace -- For demonstration purposes
-- A lazy list of expensive computations
lazyList :: [Int]
lazyList = map expensiveFunction [1..]
main :: IO ()
main = do
putStrLn "Defining lazyList..."
putStrLn "Taking the second element:"
print (lazyList !! 1)
putStrLn "Taking the first element:"
print (head lazyList)
putStrLn "Program finished."
Defining lazyList...
Taking the second element:
Computing expensive function for 2 2000
Taking the first element:
Computing expensive function for 1 1000
Program finished.
Delayed computation:
when we defined lazylist, nothing was actually computed, the list is more like a recipe than a computed results.
Compute only what's needed:
when we asked for 2nd element, it only computed that element (& the 1st element, which it had skip over). It didn't compute the entire infinite list.
Breaking down the codes:
For examples provided earlier in this notes, we will discuss it here for clearly understanding.
Type signature:
::
It's read as has type of or is of type. This symbol is a fundamental part of Haskell's syntax for explicitly declaring the types of expression, functions or variables.
Let's break it down with some examples:
For variables:
x :: Int
x = 5
declares that x has type of Int & then assigns the value 5
For functions
add :: Int -> Int -> Int
add x y = x + y
declares that add is a function that takes two Int parameters and returns Int
Since we also learnt what is currying this same statement could be written as
add :: Int -> (Int -> Int)
add x y = x + y
Int -> Int -> Int vs a -> b
Last Int and the b is always the retun type
In Int -> Int -> Int, the last Int is the final return type
In a -> b, b is the return type.
a -> b -> c, where c would be the final return type.
import Control.Parallel.Strategies
parallelMap :: (a -> b) -> [a] -> [b]
parallelMap f xs = map f xs `using` parList rseq
-- Usage
result = parallelMap (\x -> x * x) [1..1000000]
Dissecting it:
Why did we use (a -> b) -> [a] ->[b] instead of a -> b -> [a] -> [b]?
In hasklel, -> is a right associative. This means that a -> b -> c is interpreted as a -> (b -> c).
If we wrote a -> b -> [a] -> [b], it'd mean a function that takes four arguments:a, b, & a list of a and returns a list of b. This is not what we want for a map function.
Let's use an analogy of T-shirt customization service. Your service does two things:
Takes a design function like
add a logo
change color
It applies this design to a batch of plain T-shirts.
type PlainShirt = String
type CustomShirt = String
customizeBatch :: (PlainShirt -> CustomShirt) -> [PlainShirt] -> [CustomShirt]
customizeBatch designFunc shirts = map designFunc shirts
-- Example design functions
addLogo :: PlainShirt -> CustomShirt
addLogo shirt = shirt ++ " with Logo"
changeColor :: PlainShirt -> CustomShirt
changeColor shirt = "Blue " ++ shirt
-- Usage
plainShirts = ["T-Shirt", "Polo", "Tank Top"]
withLogo = customizeBatch addLogo plainShirts
colorChanged = customizeBatch changeColor plainShirts
main = do
print withLogo -- ["T-Shirt with Logo","Polo with Logo","Tank Top with Logo"]
print colorChanged -- ["Blue T-Shirt","Blue Polo","Blue Tank Top"]
(PlainShirt -> CustomShirt) is our design function. It's in parentheses because it's one complete thing. A func that knows how to customize one shirt.
[Plainshirt] is your batch of plain shirts
[CustomShirt] is your resulting batch of customized shirts.
The func (customizeBatch designFunc shirts = map designFunc shirts)
takes
a design fucntion (a -> b) i.e goes from a to b
a list of plain shirts
returns a list of customized shirts.
Key points:
The parentheses in (a -> b) indicate that this is one argument that happens to be a function.
This function is then applied to each element in the list [a].
The result is a new list [b] where each element has been transformed by the function.
Before we learn any syntax or anything. Let's first explore the most important question to learn anything.
That is to learn why learn Haskell?
Let's explore what makes Haskell worth learning and what makes it unique.
1. Pure Functional Programming:
This is the most important point.
Haskell is purely functional programming, which means it treats computation as the evaluation of mathematical functions. Pure functional programming means that functions are pure, having two main properties:
Let's see what it means:
In imperative languages, functions can often have side effects, which can lead to unexpected behaviour. In Haskell, functions are predictable.
Example:
later we will understand keywords and syntax i.e :: . For now just try to get the gist.
For easy explanation we will compare Haskell to Typescript:
What is Currying?
The names comes from Haskell Curry, a mathematician and logician.
Currying is a technique in functional programming where a function that takes multiple arguments is transformed into a sequence of functions, each taking single arguments.
Currying is a way of structuring functions that makes them more flexible.
Let's use a real world analogy:
Imagine you have a coffee machine that makes different types of coffee drinks.
Here a curried version is more specialized than the non curried one.
Key points to take away:
In Haskell, this currying is automatic. Every function is curried by default:
Currying breaks down a function that takes multiple arguments into a series of function, each taking a single argument which make your code more flexible and easier to reuse.
2. Concurrency & Easier Parallel Programming.
3. Strong static typing.
4. Mathematical foundations:
Haskell was designed with strong mathematical principles in mind. Learning haskell can help develop a more mathematical way of thinking about programming problems.
5. Lazy Evaluation:
Allows us to define potentially infinite computation & data structures, but only compute the parts you actually use. This can lead to more efficient programs & allow for elegant solutions to certain problems.
Breaking down the codes:
For examples provided earlier in this notes, we will discuss it here for clearly understanding.
Type signature:
It's read as has type of or is of type. This symbol is a fundamental part of Haskell's syntax for explicitly declaring the types of expression, functions or variables.
Let's break it down with some examples:
declares that x has type of Int & then assigns the value 5
For functions
declares that add is a function that takes two Int parameters and returns Int
Since we also learnt what is currying this same statement could be written as
Last Int and the b is always the retun type
Dissecting it:
#Haskell #functionalProgramming #CSCE550