I likely am asking a very stupid question here, please forgive me.
I am a Java and C# backend-engineer with relatively good knowledge of OOP design patterns. I have recently discovered the debate about OOP vs functional programming and the question i simply can't wrap my head around is this: If there is no state, then how could we update an element based on user input?
Here's a small example showing the problem i am facing (i am aware that JS is not a strictly functional language, but i think it shows my problem relatively well.):
Let's say hat we have a small web page that simply displays a counter and increments its value every time the user clicks a button:
<body>
The count is <span id="data">0</span><br>
<button onClick="inc()">Increment</button>
</body>
Now there's the strictly imperative approach, using a counter variable to store the counter's state:
let data;
window.onload = function(){
data = document.getElementById("data");
}
let counter = 0;
function inc(){
data.innerHTML = counter;
}
A more functional approach (at least in my understanding) would be the following code:
let data;
window.onload = function(){
data = document.getElementById("data");
}
function writeValue(val){
data.innerHTML = val;
}
function doIncrement(val){
return val 1;
}
function readValue(){
return parseInt(data.innerHTML);
}
function inc(){
writeValue(doIncrement(readValue()));
}
The issue i am facing now is that, while the data variable is never altered, data's state still changes over time (every time the counter is updated). I do not really see any real solution to this. Of course, the counter's state needs to be tracked somewhere in order for it to be incremented. We could also call document.getElementById("data")
every time we need to read or write the data, but the problem essentially remains the same. I have to somehow track the page's state in order to process it later.
Edit: Note that i have reassigned (which, i am told, is a bad thing in FP) the value val
to the variable innerHTML
in the writeValue(val)
function. This is the exact line at which i am starting to question my approach.
TL;DR: how would you handle data that is naturally subject to a change in a functional way?
CodePudding user response:
This question seems to originate from the misunderstanding that there's no state in Functional Programming (FP). While this notion is understandable, it's not true.
In short, FP is an approach to programming that makes an explicit distinction between pure functions and everything else (often called impure actions). Simon Peyton-Jones (SPJ, one of the core Haskell developers) once gave a lecture where he said something to the effect that if you couldn't have any side effects, the only thing you could do with a pure function would be to heat the CPU, whereafter one student remarked that that would also be a side effect. (It's difficult to find the exact source of this story. I recall having seen an interview with SPJ where he related the story, but searching the web for a quote in a video is still hard in 2022.)
- Changing a pixel on the screen is a side effect.
- Sending an email is a side effect.
- Deleting a file is a side effect.
- Creating a row in a database is a side effect.
- Changing an internal variable that, via cascading consequences, causes anything like the above to happen, is a side effect.
It is impossible to write (useful) software that has no side effects.
Furthermore, pure functions also don't allow non-deterministic behaviour. That excludes even more necessary actions:
- Getting a (truly) random number is non-deterministic.
- Getting the time or date is non-deterministic.
- Reading a file is non-deterministic.
- Querying a database is non-deterministic.
- Etc.
FP acknowledges that all such impure actions need to take place. The difference in philosophy is the emphasis on pure functions. A pure function has many desirable traits (predictability, referential transparency, possible memoization, testability) that makes it worthwhile to pursue a programming philosophy that favours such functions.
A functional architecture is one that minimises the impure actions to their essentials. One label for that is Functional Core, Imperative Shell, where you push all impure actions to the edge of your system. That would, for example, include an HTML counter. Actually changing an HTML element stays imperative, while the calculation required to produce the new value can be implemented as a pure function.
Different languages have varying approaches to how explicitly they model the distinction between pure functions and impure actions. Most languages (even 'functional' languages like F# and Clojure) don't explicitly make that distinction, so it's up to the programmer to keep that separation in mind.
Haskell is one language that famously does make that distinction. It uses the IO
monad to explicitly model impure actions. I've attempted to explain IO
for object-oriented programmers using C# as an example language in an article: The IO Container.