Optimizing Haskell Code for Performance

It means that expressions are only evaluated when they need to be used. This can lead to some pretty cool features like infinite lists and recursive functions without stack overflow errors. But it also comes with a price: sometimes Haskell’s laziness can cause performance issues due to unnecessary allocations or thunking (delayed evaluation).
To combat this, we use strictness annotations! By adding the `seq` function before any expensive computations, we force them to be evaluated immediately instead of waiting for a later time. For example:

let x = ... -- some computation that might cause performance issues due to laziness
let y = seq x (... more stuff) -- add strictness annotation here!

This ensures that `x` is evaluated before we move on to the next line of code.
Another way to optimize Haskell code for performance is by using appropriate data structures. For example, if you have a list with many duplicates or repeated elements, it might be more efficient to use a set instead! Sets are implemented as hash tables in Haskell and can provide constant-time lookup and insertion operations.
Here’s an example:

-- Create a list with duplicates
let myList = [1, 2, 3, 4, 5]

-- Import the Set module to use sets
import Data.Set as Set

-- Use the fromList function to convert the list into a set
-- Set.fromList :: Ord a => [a] -> Set a
let uniqueNumbers = Set.fromList (filter (\x -> notElem x myList) myList)

-- The filter function removes elements from the list that are not unique
-- filter :: (a -> Bool) -> [a] -> [a]
-- In this case, the lambda function checks if the element is not already present in the list
-- notElem :: Eq a => a -> [a] -> Bool
-- This ensures that only unique elements are added to the set

-- The resulting set will contain only unique elements from the original list
-- This is more efficient than using a list with duplicates, as sets use hash tables for constant-time lookup and insertion operations.

This creates a new set called `uniqueNumbers` that contains only the non-duplicate elements from our original list.
Finally, we can use strict types to optimize Haskell code for performance! By specifying the exact type of a variable or function argument, we can avoid unnecessary allocations and improve memory usage. For example:

-- Define a function that takes in a list of integers and returns a list of unique integers
uniqueNumbers :: [Int] -> [Int]
uniqueNumbers [] = [] -- If the list is empty, return an empty list
uniqueNumbers (x:xs) -- If the list is not empty, pattern match on the first element and the rest of the list
  | x `elem` xs = uniqueNumbers xs -- If the first element is already in the rest of the list, skip it and continue with the rest of the list
  | otherwise = x : uniqueNumbers xs -- If the first element is not in the rest of the list, add it to the result and continue with the rest of the list

-- Define a function that takes in an integer and returns an integer
compute :: Int -> Int
compute x = ... -- Some computation involving `x`

-- Specify that `x` is an integer instead of a generic value
let x :: Int
-- Perform the computation using `x` and assign the result to `y`
let y = compute x

This ensures that `y` will be evaluated as an integer, rather than a more general type like `Any`.

SICORPS