
Creating a new function or assignment in Haskell means creating a new scope. A scope in programming terms relates to the context in which data and code are used – effectively what your program “sees” when typing in named functions and variables.
If a function or variable isn’t matched within the current scope, the program looks to the next scope higher up, e.g. if we try to use a variable or function that isn’t defined in the current scope, the program checks outside the current scope to try and find it:
-- Will produce an error as x is not in scope (x doesn't exist!)
myCalculation = x - 5
Here, x is in a higher-level scope so myCalculation will discover it:
x = 6 -- you can think of this x as being "globally available"
myCalculation = x - 5
This process of scope-checking in Haskell is called Lexical Scope and is important in understanding and planning the behaviour of a program. Placeholders such as ‘x’ and ‘y’ are often used throughout multiple functions and calculations and it is the aspect of lexical scope which allows the use of duplicate-named variables.
A powerful feature of scope in Haskell is that we can build general-purpose functionality and overwrite with specific functionality without having to redefine or create separate functions:
x = 2
y = 3
-- Here, myFunction's x and y pertain to its own scope, not the above
myFunction x y = x - y
-- We can supply externally scoped variables to myFunction:
myFunction x y -- (2-3) = -1
myFunction y x -- (3-2) = 1
-- We can also specify values directly to myFunction's scope:
myFunction 5 5 -- 5 and 5 will be used as myFunction's x and y
This is useful when working with large programs as you don’t need to jump around the source file and double check all of your variable names, you can just focus on writing your functions.
Let’s explore further, what if we were to do something like this:
x = 5
y = (\x -> x) 3
Can you guess what this will return? Does y == x? Here’s where an understanding of scope becomes essential; the x value within our anonymous function is contained in its own scope (notice the surrounding brackets ( )), it is found by the program before the “global” x = 5 variable and so is used instead. In this case y = 3 as its local x is mapped to 3 by the anonymous function!
Focus on Scope

Here are some more examples of the previous concept combined with named functions:
x = 5
-- add the argument x by itself, not the "global" x = 5
addBySelf x = (\x -> x + x) x
-- ignore the argument passed and use x in own scope, which = 0
ignoreMe x = (\x -> x + x) 0
-- Here we use our functions:
addBySelf x -- 5 + 5
addBySelf 2 -- 2 + 2
ignoreMe x -- 0
ignoreMe 8273 -- 0
The key to understanding scope is focus. When trying to figure out what code is doing, start from the inner-most code block and work backwards, trying to account for every “x” used at once will drive you insane. Focus on relevant info you need with tunnel-vision and only expand when you need a greater context, in other words: think like the program itself!
A locational-checklist can help you understand program scope: body -> definition -> other (e.g. your libraries, methods, etc.)
Following this paradigm, let’s take a look at the ignoreMe function we had previously. First, we’ll single out the body of the function (everything to the right of its assignment, =):
(\x -> x + x) 0
Simple enough, we have a lambda function which maps a local x variable to itself, moving outwards we see it’s supplied a 0.
Now let’s consider the full definition of the ignoreMe function:
ignoreMe x = (\x -> x + x) 0
Here, in addition to what the function does, we learn how the function is invoked, we must supply a value for the argument x (to the left of the =); however, in this example the x argument has nothing to do with the ‘x’s used in the function body, in fact we could rewrite the function as such and get the exact same functionality:
ignoreMe x = (\y -> y + y) 0
To use the x argument within the body of our function, we would replace the 0, now the ‘x’s within the ( ) and outside are separate data:
-- again, the inner ( ) 'x's could be replaced with anything
ignoreMe x = (\x -> x + x) x
ignoreMe 3 -- = (3 + 3) = 6
If we now supply a global x variable in our program, we can use the value of this as an argument for the function:
x = 3
ignoreMe x = (\x -> x + x) x
ignoreMe x -- = 6
To better illustrate how we’re using the above I will tag the independent ‘x’s according to their usage:
x1 = 3
ignoreMe x2 = (\x3 -> x3 + x3) x2
ignoreMe x1 -- = 6
Note: You can change the names of any grouping of the ‘x’s here to whatever you like and achieve the same results.
Conclusion
I’ll leave you with some needlessly complicated sum functions to play around with, try changing the variable names within each function to see what works and what doesn’t:
x = 1
y = 2
z = 3
sum x = (\x -> x) x -- sum one number
sum2 x y = (\x -> (\y -> y + x) y) x -- sum two numbers
sum3 x y z = (\x -> (\y -> (\z -> z + y + x) z) y) x -- etc.
There are many ways to intentionally (and unintentionally) override program scope. Understanding scope comes naturally with practice (i.e. breaking things) and care should always be taken when duplicating variable and function names.
~ July 2024









Leave a comment