After a long time understanding pure functions, and idiomatic ClojureScript functions when no side effects are desired, to mean functions avoiding causing side effects, I've just come across a different definition that adds avoiding being affected by side causes. (From here: https://practical.li/clojure/thinking-functionally/pure-functions/) Is avoiding not just side effects but also side causes good practice? Is it the norm? For instance, in the little webapp I'm working on, I have functions whose operations include referring to (just reading, not mutating) global vars. Should I instead avoid referring to global vars (even those that are not atoms but essentially constants, defined with basic (def _) forms), and just write each function to take more arguments and call the functions with those vars as arguments? Would that be any better? Could it actually be worse, maybe due to some kind of processing overhead from the extra passing of arguments instead of just accessing the vars directly? Case study: Would this:
(def a 1)
(defn f [] (+ a 1)) ; function refers to global (immutable) var
(f)
be better written as this?
(def a 1)
(defn f [x] (+ x 1))
(f a) ; global (immutable) var (value) gets passed to function
----
Note: I just posted this question on StackOverflow as well, since I could not find anything there that dealt with the concept of "side causes".
https://stackoverflow.com/questions/79999452/should-side-causes-even-just-global-constants-be-avoided-when-writing-functi
I could update here if an answer comes up there, especially if it's to save a fellow clojurian who answers there from writing it twice. 🙂 Not sure what the best way to do this would be, and I'd especially like to know because going forward I'd like to make the answers I see more discoverable/accessible to other people by having them on SO.Yes, the second would generally be preferable. In both cases, f has a dependency it needs to do its work. Making dependencies explicit by passing them in (rather than implicit by referencing a global) makes your function more reusable, composable, and testable
Any performance difference between doing it one way or the other? (Not that that outweighs the reasons you've given. Just curious.)
Not in any general sense. Maybe if we're heading into micro optimization territory, which you'd have to measure and see (I don't know off the top of my head what differences there might be there between accessing a var and accessing a local)
Better than performance, ergonomics would be a motivator for doing something like the global approach. Consider if println forced you to pass in the output stream all the time. Instead, it uses a global *out* that you can rebind to write elsewhere (e.g. with-out-str)
(this is a bit different though in that the global def is dynamic, allowing it to be rebound properly)
Thank you. I think I will default to writing functions the second way, then (such that they do not refer to global variables but instead have such operands passed as arguments)...and I have some refactoring to go do.
It sure is strange that side causes would not normally be brought up and discussed at the same time as side effects (not until the 100th or so mention of side effects I came across!). I get that side causes are less important (less potentially damaging), but the two concepts are so obviously complementary they really should be brought up together, at least in learning material.
By the way, the answers to my StackOverflow question so far don't seem very supportive of the idea of that kind of refactoring! Very strange. I wonder why the difference. And by the way, I find the definition question, of whether a pure function must not just not affect anything but its return value (no side effects) but also not be affected by anything but the arguments passed to it (no side causes), still seems unanswered.
Generally a pure function is a function of input -> output, where input refers strictly to the input arguments. So it would imply no side-effect or causes. I think sometimes it gets simplified as no-side-effects, but most people mean pure functions.
In fact, pedantically, pure functions should be replaceable by a hard-coded input to output mapping, so can be cached (or memoized) without ever causing issues. That definition would even say for example that it cannot internally rely on the current time, or randomness, you'd have to inject those as well.
I think the SO answers are good, but the real answer is yes, favor pure functions, always, unless you know better
On the assumption that you'll find this nitpick helpful: https://clojure.org/reference/vars they exist and work the way they do specifically because they are mutable.
Wrt the differences in the SO responses: I didn't take a to be a constant, but rather a bit of data that f needed to do its job. I skimmed the responses, and it looks like a lot of them were looking at the question from that perspective, which I think is too narrow a view of the question
But aside from that they seem like good answers
yes, favor pure functions, always, unless you know betterThis makes sense. Kind of a Chesterton's Fence situation. ...and it is simple to adapt to
(defonce __ __) (allowed 👍) and (def __ (atom __)) (not allowed 👎), but it's the pesky (def __ __) non-atom situation that is the real issue.
I still don't get why the default is defining vars, with def, and kind of treating them as constants, instead of defining contants, with defonce. I would have expected the norm to be defs being used only for atoms, where mutability is specifically wanted. (I haven't gotten into refs and agents yet, but those seem to show up much more rarely.)
Regardless or perhaps because of that confusion, I am going to just stop having my functions refer to and act on anything not defined within the functions or passed to the functions.I wouldn't allow defonce either. I'd only allow:
(def ^:const foo ...)
Which is a true constant, and when compiled gets inlined.What people mean with we treat "def ..." as "constant" is that strangely in Clojure all top-level forms are mutable, as you can re-def them or mutate their value with alter-var-root or var-set. But doing so except for at the REPL or in your tests is frowned upon, hence you should treat them as "constant", meaning never ever write a program that will mutate them. Only use their mutable capability at the REPL or in your tests. So they're treated as "constant" by convention and no programmers except them to change value even though the language doesn't guarantee they are immutable.
This is why for example if function A depends on function B we still say function A is pure, even though function B isn't constant and could change between calls to A, but by convention we don't do that so it's pure by convention. And the reason it's not actually constant is that if it was the REPL would be useless and stubbing for tests would be much more difficult.
So nice to finally see reasons for the apparent mismatch (mutability of things that shouldn't get mutated). Thanks so much.
I suppose I'll keep using def those reasons, but by the way why would defonce not be a good way to be sure a constant stays a constant if needed? I have read that it's generally used during (hot) code reloading to prevent some things from being wiped during each reload, but it also seems like it could work well in place of defs with extra assurance.
defonce is for things that break if a file is reloaded, eg. to avoid re-creating a class so you have two classes with the same name that are not equal
defonce prevents a later def from changing its value. But at the REPL you'll likely want to be allowed to re-def most things, which is why generally you def most things and only defonce those that you explicitly need not to reload at the REPL.
> It sure is strange that side causes would not normally be brought up and discussed at the same time as side effects
I think that "side causes" is just another way of saying "side effects", but from the other side of the relationship. If the results of a function depend on the content of some other state that can change, then it's not a pure function. I think that Clojure tends to treat you like an adult and allow you to define "can change" for your own program, so you don't have to worry about the definition of core functions like + changing, and if you setup some global config and don't let it change while your program is running then maybe that's good enough. But I think it's up to you to decide how much you want to relax the definition of "pure" to get work done.
"side effect" implies that there's some other effect besides the direct one that is explicit in the code (eg. doing some calculation and getting a result). I don't use the concept of a "side cause" because some imperative thing that drives the calculations is not some hidden cause, it's the source of the input, it's the driver that makes the code run. this fits in with the idea of having an imperative shell around a functional core. you can't build a functional shell around imperative code - the side effect always leaks. you can build abstractions around pure code - the layers entail more resources used but don't hide unanticipated behavior. you put pure code in the core because it behaves well, you put impure code on the outside because it behaves erratically and can't be built on.