←back to thread

498 points azhenley | 1 comments | | HN request time: 0s | source
Show context
EastLondonCoder ◴[] No.45770007[source]
After a 2 year Clojure stint I find it very hard to explain the clarity that comes with immutability for programmers used to trigger effects with a mutation.

I think it may be one of those things you have to see in order to understand.

replies(17): >>45770035 #>>45770426 #>>45770485 #>>45770884 #>>45770924 #>>45771438 #>>45771558 #>>45771722 #>>45772048 #>>45772446 #>>45773479 #>>45775905 #>>45777189 #>>45779458 #>>45780612 #>>45780778 #>>45781186 #
emil0r ◴[] No.45770884[source]
The way I like to think about is that with immutable data as default and pure functions, you get to treat the pure functions as black boxes. You don't need to know what's going on inside, and the function doesn't need to know what's going on in the outside world. The data shape becomes the contract.

As such, localized context, everywhere, is perhaps the best way to explain it from the point of view of a mutable world. At no point do you ever need to know about the state of the entire program, you just need to know the data and the function. I don't need the entire program up and running in order to test or debug this function. I just need the data that was sent in, which CANNOT be changed by any other part of the program.

replies(1): >>45772025 #
DrScientist ◴[] No.45772025[source]
Sure modularity, encapsulation etc are great tools for making components understandable and maintainable.

However, don't you still need to understand the entire program as ultimately that's what you are trying to build.

And if the state of the entire programme doesn't change - then nothing has happened. ie there still has to be mutable state somewhere - so where is it moved to?

replies(9): >>45773150 #>>45773166 #>>45773254 #>>45773339 #>>45774040 #>>45774256 #>>45774298 #>>45775098 #>>45778109 #
1. fwip ◴[] No.45773339{3}[source]
It's moved toward the edges of your program. In a lot of functional languages, places that can perform these effects are marked explicitly.

For example, in Haskell, any function that can perform IO has "IO" in the return type, so the "printLine" equivalent is: "putStrLn :: String -> IO". (I'm simplifying a bit here). The result is that you know that a function like "getUserComments :: User -> [CommentId]" is only going to do what it says on the tin - it won't go fetch data from a database, print anything to a log, spawn new threads, etc.

It gives similar organizational/clarity benefits as something like "hexagonal architecture," or a capabilities system. By limiting the scope of what it's possible for a given unit of code to do, it's faster to understand the system and you can iterate more confidently with code you can trust.