No history yet

Haskell Foundations

A Different Mindset

In imperative languages like Python or Java, you write sequences of statements: do this, then do that, modify this variable. A program is a recipe of commands. Haskell operates on a fundamentally different principle: everything is an expression that evaluates to a value. There are no statements, only definitions and expressions to be solved.

This shift is more than just syntax. It reframes programming to be closer to mathematics. Since you have a background in functional analysis, think of a Haskell function not as a procedure but as a true mathematical mapping. It defines a relationship between inputs and an output. For any given input, it will always produce the same output, without exception and without causing any other changes in the system. This property is called purity, and it's the core of Haskell's design.

purity

noun

The property of a function whose output value is determined only by its input values, with no observable side effects such as modifying a global variable, performing I/O, or changing its own internal state.

This leads directly to the concept of referential transparency. If a function is pure, you can replace any call to it with its resulting value without changing the program's behavior. This makes code predictable and much easier to reason about. You don't need to track the state of the entire application to understand what one small function does.

-- Pure Haskell function
-- This is a mapping from Int to Int.
-- For any given n, it will ALWAYS return n * 2.
double :: Int -> Int
double n = n * 2

-- Compare to an impure function in Python
-- Its output depends on an external, mutable state.
-- Calling it multiple times with the same input
-- can yield different results.
global_counter = 0

def impure_function(x):
    global global_counter
    global_counter += 1
    return x + global_counter

Execution on Demand

In most languages, function arguments are evaluated before the function is called. This is known as strict or eager evaluation. Haskell does the opposite. It uses lazy evaluation, which means expressions are not evaluated until their results are actually needed.

When you define an expression, Haskell doesn't compute it right away. Instead, it creates a thunk—a sort of promise to compute the value later. The program becomes a graph of these thunks. Evaluation only happens when a result is demanded, for example, by the main function trying to print something to the screen. This call-by-need approach avoids unnecessary computation and allows for elegant handling of concepts like infinite data structures.

thunk

noun

A data structure representing a delayed computation. It stores an expression and its environment, and once evaluated, the result is cached (memoized) to avoid re-computation.

Functions as Building Blocks

In Haskell, functions are first-class citizens. They can be passed as arguments to other functions, returned as results, and stored in data structures. Functions that take other functions as arguments are called higher-order functions. This is a powerful abstraction, allowing you to write generic, reusable code.

The map function is a classic example. It takes a function and a list, and applies the function to every element in the list, producing a new list.

-- `map` takes a function of type (a -> b) and a list of type [a]
-- and returns a new list of type [b].
-- Here, we apply the `double` function to each element.

-- ghci> double n = n * 2
-- ghci> map double [1, 2, 3, 4]
-- [2, 4, 6, 8]

-- We can also use an anonymous function (a lambda)
-- ghci> map (\x -> x - 1) [10, 20, 30]
-- [9, 19, 29]

Haskell also handles functions with multiple arguments in a unique way through a process called currying. Technically, every Haskell function takes exactly one argument. A function that looks like it takes three arguments, say f a b c, is actually a series of nested functions. It's a higher-order function that takes a and returns a new function that takes b. That function, in turn, returns a final function that takes c and produces the result.

This structure allows for partial application. You can supply fewer arguments than a function expects and get a specialized function back. This is an incredibly useful technique for creating reusable components.

-- A function that seems to take two arguments
add :: Int -> Int -> Int
add x y = x + y

-- Its type `Int -> Int -> Int` can be read as `Int -> (Int -> Int)`.
-- It's a function that takes an Int and returns a function of type `Int -> Int`.

-- We can partially apply it by providing only the first argument.
addFive :: Int -> Int
addFive = add 5

-- `addFive` is now a new function that adds 5 to its input.
-- ghci> addFive 10
-- 15

-- This is equivalent to `add 5 10`
-- ghci> add 5 10
-- 15

Getting Started with GHCi

The best way to experiment with Haskell is using the Glasgow Haskell Compiler's interactive environment, or GHCi. It’s a REPL (Read-Eval-Print Loop) that lets you test expressions and load code from files.

Haskell code is organized into modules. A simple module is just a text file (e.g., MyFunctions.hs) that begins with a module declaration. You can then load this file into GHCi to use the functions you've defined.

-- File: MyFunctions.hs
module MyFunctions where

isEven :: Int -> Bool
isEven n = n `mod` 2 == 0

-- In your terminal:
-- > ghci
-- Prelude> :load MyFunctions.hs
-- [1 of 1] Compiling MyFunctions      ( MyFunctions.hs, interpreted )
-- Ok, one module loaded.
-- *MyFunctions> isEven 4
-- True
-- *MyFunctions> isEven 5
-- False

This foundation of purity, lazy evaluation, and powerful function composition sets Haskell apart. It encourages a declarative style where you define what you want to compute, leaving the how of evaluation to the compiler and runtime system.