Understanding the basic use of functions in functional programming (javascript)
Slow ThinkerMar 4, 2023
I write code for a living, I've been doing it for around ten years. I mostly focus on JavaScript and Typescript. I've been interested in FP (functional programming) since the first few years of my professional career and for a while I thought I got the hang of it. It's only recently when I've decided to re-learn the fundamentals in a more structured way. This article is my attempt in summarizing some of the thoughts I have on it and my learning experience so far.
The main resource I used for re-learning and consolidating my knowledge is a book called: Professor Frisby's Mostly Adequate Guide to Functional Programming.
Mathematical functions
I remember functions from math class, I remember plotting the graphs for various functions. A common exercise might have looked something like this:
Given the functionf(x) = x(2x - 5)
and the domain{1, 2, 3}
, find the codomain.
and the solution might've looked like a table such as:
+--------+------+------+-----+
| x | 1 | 2 | 3 |
+--------+------+------+-----+
| f(x) | -3 | -2 | 3 |
+--------+------+------+-----+
or when the domain would be R
a graph was a more useful representation:
Basically for each input there should be an output, every input must have exactly one output but different inputs can have the same output.
Getting back to the example above, the f(x) = x(2x - 5)
function and the {1, 2, 3}
domain, I can write a simple program to solve it. It could look something like this:
const domain = [1, 2, 3];
let codomain = [];
for (i=0; i<domain.length; i++) {
codomain.push(domain[i]*(2*domain[i] - 5));
}
console.log(codomain); // [-3, -2, 3]
In my opinion this code doesn't look great, but knowing more about FP can help improve it.
Side-effects in functional programming
In FP the desired objective is to avoid state mutation and other side-effects. Side-effects are any changes in the system state or observable interactions with the outside in the process of calculating a result. A few examples of common side-effects are: state mutations, logging, http calls, access to outside states such as the DOM and others.
A program with no side-effects, in theory, would look like it's doing nothing, so avoiding side-effects altogether is not very feasible.
Although realistically, side-effects cannot be fully avoided, it's wise to minimize them when possible. When minimizing them is not possible there is usually the potential to isolate them and have a deliberate and controlled way of using them.
The code for the previous program cannot be considered functional as there was no significant attempt in avoiding side-effects.
A more FP friendly approach could be the following:
const domain = [1, 2, 3];
const f = (x) => x*(2*x - 5);
const codomain = domain.map(f);
console.log(codomain); // [-3, -2, 3]
The code still has the console.log
side-effect, but there was an attempt to minimize other side-effects. One advantage of this style of coding is the increased clarity of what that piece of code is trying to achieve.
Pure functions
One of the basic concepts of FP is the pure function. A function can be considered pure if given the same input it returns the same output and has no observable side-effects. The f
function in the previous example can be considered pure.
Functions can also return other functions and receive other functions as arguments, these are called higher-order functions.
Pure functions are not necessarily restricted to having just one parameter like the example before. Functions that have a single parameter are called unary.
An otherwise pure function can still be considered pure even if it calls an outside function that is also pure.
A function that mutates data is not a pure function, furthermore, a function that calls an impure function is also not pure. A common example for function purity, also present in Professor Frisby's Mostly Adequate Guide to Functional Programming, is the difference between Array.slice
and Array.splice
.
const arr = [0, 1, 2, 3, 4];
// pure
arr.slice(0, 3); // [0, 1, 2]
arr.slice(0, 3); // [0, 1, 2]
arr.slice(0, 3); // [0, 1, 2]
// impure
arr.splice(0, 3); // [0, 1, 2]
arr.splice(0, 3); // [3, 4]
arr.splice(0, 3); // []
The example ilustrates that Array.splice
does not behave like a pure function, as it has the side-effect of mutating the initial array. Calling it multiple times with the same inputs, does not guarantee the same outputs.
A common term that can be applied to impure functions is procedure.
"A procedure is an arbitrary collection of functionality. It may have inputs, it may not. It may have an output (return value), it may not."
― Kyle Simpson, Functional-Light JavaScript
Practical example
As part of my own learning experience the next challenge would be to apply the principles discussed in the article practically and in the spirit of FP.
The plan is to build a tool that will generate the graph for a function defined by the user. The user can also define the domain of the function. So for example a user could provide the function 1/x
and the domain R
(real numbers) and the tool would plot the adequate graph.
For building this particular tool I decided to use the CanvasRenderingContext2D to draw the axis and the graph and rely on HTML Inputs to provide the user interaction. I started with a bit of exploration through the process of trial and error and reached the conclusion that this particular program would need to:
- get the user input for the function definition and domain
- generate the coordinates for each point in the domain
[x, f(x)]
- if the domain is infinite, set some calculation limits
- set a precision parameter for drawing line graphs (when the domain contains rational numbers)
- render the xOy axis based on some initial configuration
- plot the graph based on scaling the coordinates to match the axis configuration
- allow some customization
- find a way to minimize or isolate side-effects for user input, DOM interaction, rendering
In the end the program ended up with 4 parts:
- the
GraphAdapterCanvas
providing the generic canvas interaction and functionalityrenderText
used for rendering the axis notation and markersrenderPoints
used for rendering coordinates as pointsrenderRectangle
used for rendering the backgroundrenderLine
used for rendering the axis and plotting the line graph
- the
UIGraphAdapterHTML
used for retrieving the user input:getRawFunction
used for retrieving the text element contentgetFunctionDomain
used for finding the selected domain and retrieving the valueonFunctionDefinitionError
used for marking an error state on the function definition fieldonCustomDomainDefinitionError
used for marking an error state on the custom domain definition fieldstartTriggerListener
used for listening to button click events in order to trigger a graph re-render
- the
GraphingUtility
which is the actual logic for drawing the axis, markers, plotting the graph, calculating coordinates and scaling them accordinglyrenderAxis
renders the cartesian coordinate system (axis, markers, labels)renderGraph
renders the function graph based on coordinates and domain
- the
init
function to wire together the parts and initialize them
I would consider the UIGraphAdapterHTML
and GraphAdapterCanvas
to be at the edges of the system, handling the inputs and outputs. The GraphingUtility
is encapsulating the "business logic" and does the rendering through the abstracted layer of the GraphAdapterCanvas
.
The init
part of the system relies on the UIGraphAdapterHTML
to provide the raw input data to the GraphingUtility
and initialize the program.
Having adapters helps in keeping the core logic independent of the concrete infrastructure of the canvas and DOM, it opens the possibility, for example, to potentially use the tool in a terminal environment by rendering the graph using ASCII symbols and receiving the inputs (function definition and domain) through command line arguments. This could be accomplished by building new adapters which can internally handle the concrete needs of inputs and outputs in a terminal environment. The adapters need to preserve the same kinds of "contracts" as the other adapters so only minimal changes are needed when porting the program to a different environment (from browser to console).
Another benefit of the adapters is to minimize side-effects. If the GraphingUtility
was using the CanvasRenderingContext2D directly it would mean that all functions that were rendering something would be impure and everything else depending on them would also be considered impure. This was my attempt in minimizing side-effects for this program with the knowledge I currently have. As I start learning more I might find some mistakes in my current thinking or find better simpler ways of achieving the same or better results.
Although this program was built with care and was a worthwhile experience, I wanted to avoid using external libraries, that's why I had to rely on eval
. I would say this code still leans more on the academic rather than production, but if I had to make this a production level tool, I would probably look into math.js evaluate
and might also use ramda to make the "functional" parts more readable.
See the Pen Basic graphing utility by (@slowthinker) on CodePen.
Takeaway
All in all, I've enjoyed the process of learning and practicing the concepts I've learned.
All in all, I've enjoyed the process of learning and practicing the concepts I've learned.
The intention is not to give a definite answer about how one should or shouldn't write their code, but rather to give a perspective from my own learning experience.
I find it also useful to log the incremental progress that comes from focused learning and validating the knowledge through practical work.