But it's still not a function. And the semantics matter here - functional programming is, at root, a paradigm for programming with functions. You can't just say that if it were a function it would have side effects; if my granny had wheels she'd be a skateboard.
We could - we should - really encapsulate both solutions in their own functions. To do so is trivial yet would be revealing: the imperative code, to the consumer of the function, is indistinguishable from the functional code.
What I'm driving at here is that your points about purity, referential transparency and dude effects are irrelevant; they are not the real difference between the two implementations.
I'd perhaps consider whether one 'reads' better than the other, at least as a starting point.
The point isn't about whether it's literally a function or not.
The point is, the loop breaks referential transparency, which means that you have opened up the possibility for a class of errors that misuse it.
This example is trite, but if this were a bigger codebase and a larger loop, you can imagine the potential errors I've allowed to be introduced when someone mutates something in that loop inappropriately.
One of the ideas of any style of programming is what class of errors you allow. Static typing reduce a class of errors that dynamic typing allows. Each reduction typically comes with a cost, whether it be in boilerplate or mental know-how. In this example, one can argue that reducing the class of errors that functional impurity allows is worth the cost of learning HOF and expression based programming, especially since it gets you a lot of free things like expression based testing and debugging, easier maintenance, less risky of bugs, etc.
You don't have to agree that the trade-off is worth it, but the point is much more than just "it doesn't literally use the function keyword, so the points don't matter".
As Kevin mentions above me, this example is contrived so doesn't show the full extent of how, imperative, effectful, mutable code opens you up to a whole range of errors that can be super tricky to debug.
I still believe the for loop is an example of side effects regardless of if is strictly a function itself. If you draw a circle around the block inside the curly braces, you can clearly see that it has to reach outside that circle to mutate an array, it is changing the world around it.
And yes, if you encapsulate both versions in their own function, to their consumers they do the same thing. Even more reason for FP approach IMO, if you knew functions were written in a immutable side effect free fashion,you wouldn't have to look at the implementation (especially if static types are present) to double check it's not wiping your hard drive or sending your data to some foreign API.
(Exaggerated but the point stands)
Now we've got some structure into this (just to keep Dijkstra happy) we can perform a fair comparison.
Both of these functions are 'pure'.1 They are both stateless, returning the same value for the same set of arguments. They do not mutate state - they have no effect upon any value outside of the function.
to their consumers they do the same thing
They don't only 'do' the same thing, they are indistinguishable. Other than the imperative one is faster. You do the maths.
The point is, the loop breaks referential transparency, which means that you have opened up the possibility for a class of errors that misuse it.
The loop does break referential transparency. But the solution as a whole doesn't. Do you know what else breaks referential transparency:
Nice, thanks for the great discussion, and I agree with your points, as always nothing is black and white, both paradigms have benfits, and yes FP can be daunting, I'm very lucky to have landed my dev job at a company with a strong FP community so have a lot of people around me to learn from.
One question, in regards to the filter function not being referentially transparent, could you make it so like this?:
itemToSearch is still closed over by the lambda (i.e. it's not one of its arguments).
Before I get into this - I really wouldn't do this in real life! It's fine that the lambda closes over itemToSearch. I'm only doing this because it's fun and I enjoy FP :D.
But since you asked... one way of handling this - if you really, really only wanted pure functions everywhere - would be to pass itemToSearch as an argument and return the friendFilter function, essentially currying the function:
You can look at the block between curly brackets of the for loop as a function that is run for each iteration of the for statement.
No matter the particular semantics, the side effect point still stands
But it's still not a function. And the semantics matter here - functional programming is, at root, a paradigm for programming with functions. You can't just say that if it were a function it would have side effects; if my granny had wheels she'd be a skateboard.
We could - we should - really encapsulate both solutions in their own functions. To do so is trivial yet would be revealing: the imperative code, to the consumer of the function, is indistinguishable from the functional code.
What I'm driving at here is that your points about purity, referential transparency and dude effects are irrelevant; they are not the real difference between the two implementations.
I'd perhaps consider whether one 'reads' better than the other, at least as a starting point.
The point isn't about whether it's literally a function or not.
The point is, the loop breaks referential transparency, which means that you have opened up the possibility for a class of errors that misuse it.
This example is trite, but if this were a bigger codebase and a larger loop, you can imagine the potential errors I've allowed to be introduced when someone mutates something in that loop inappropriately.
One of the ideas of any style of programming is what class of errors you allow. Static typing reduce a class of errors that dynamic typing allows. Each reduction typically comes with a cost, whether it be in boilerplate or mental know-how. In this example, one can argue that reducing the class of errors that functional impurity allows is worth the cost of learning HOF and expression based programming, especially since it gets you a lot of free things like expression based testing and debugging, easier maintenance, less risky of bugs, etc.
You don't have to agree that the trade-off is worth it, but the point is much more than just "it doesn't literally use the function keyword, so the points don't matter".
As Kevin mentions above me, this example is contrived so doesn't show the full extent of how, imperative, effectful, mutable code opens you up to a whole range of errors that can be super tricky to debug.
I still believe the for loop is an example of side effects regardless of if is strictly a function itself. If you draw a circle around the block inside the curly braces, you can clearly see that it has to reach outside that circle to mutate an array, it is changing the world around it.
And yes, if you encapsulate both versions in their own function, to their consumers they do the same thing. Even more reason for FP approach IMO, if you knew functions were written in a immutable side effect free fashion,you wouldn't have to look at the implementation (especially if static types are present) to double check it's not wiping your hard drive or sending your data to some foreign API.
(Exaggerated but the point stands)
Whew - some fine responses!
Let's do this!
here's the one with imperative guts:
and here's one with functional guts
Now we've got some structure into this (just to keep Dijkstra happy) we can perform a fair comparison.
Both of these functions are 'pure'.1 They are both stateless, returning the same value for the same set of arguments. They do not mutate state - they have no effect upon any value outside of the function.
They don't only 'do' the same thing, they are indistinguishable. Other than the imperative one is faster. You do the maths.
The loop does break referential transparency. But the solution as a whole doesn't. Do you know what else breaks referential transparency:
Yup, that there lambda don't get
itemToSearch
as an argument... I guess it's not functional. But the solution as a whole is.Anyway, obligatory xkcd comic:
schoolofhaskell.com/school/startin... ↩
Nice, thanks for the great discussion, and I agree with your points, as always nothing is black and white, both paradigms have benfits, and yes FP can be daunting, I'm very lucky to have landed my dev job at a company with a strong FP community so have a lot of people around me to learn from.
One question, in regards to the filter function not being referentially transparent, could you make it so like this?:
Classy! But It still involves a non-referentially transparent function.
itemToSearch
is still closed over by the lambda (i.e. it's not one of its arguments).Before I get into this - I really wouldn't do this in real life! It's fine that the lambda closes over
itemToSearch
. I'm only doing this because it's fun and I enjoy FP :D.But since you asked... one way of handling this - if you really, really only wanted pure functions everywhere - would be to pass
itemToSearch
as an argument and return thefriendFilter
function, essentially currying the function:Lambda and currying make a great way to add data into your functions as and when its needed.
small refactor:
stupid why-oh-why refactor:
Ridiculous, please-make-it-stop refactor
If you enjoy this madness, might I recommend reading some books about the Scheme programming language.
Haha nice, I actually work with Scala day to day and a lot of code ends up looking similar to the second last example (with types).
Thanks again mate :)
Thanks for sharing all your insights (all of you), I will have a closer look at them.