3 ms·
I have changed my views since. I'm now much more okay with mutation than I used to be, under certain conditions. Ideally I want my mutations to stay local (at t
by loup-vaillant 7y ago
I have changed my views since. I'm now much more okay with mutation than I used to be, under certain conditions. Ideally I want my mutations to stay local (at the function or module/class scope), and have as few cross module consequences as possible.
My view of a program is that of a dependence graph, ideally directed acyclic. Ideally we want that graph to be as sparse as possible: good old decoupling. Mutable state (not just global variables) tend to introduce hidden dependencies, making that graph denser than what it initially appears. That can be a killer.
Surprisingly though, as years pass, I am less and less certain what the "correct" structure of a program should be. I sure want to keep it small, simple, easy to maintain… but the method to achieve this still eludes me.
- mannykannot 7y agoIn general I share your increasing uncertainty, but I think it is safe to say that understanding a program is made much more difficult once one has the possibility that a variable might be modified outside of the scope it is created for. The idiom of using mutable variables to track the current state of affairs in a loop is harmless up to the point where one or more is passed by mutable reference to a subroutine or impure function; at that point, it has, almost literally, fallen down a rabbit-hole wherein almost anything could happen.
- chubot 7y agoYeah exactly, I think of this as "imperative in the small" and "functional in the large". There's no real cost to local state, but there's a big cost to tracking global state. (Although game programmers seem to have necessarily built their mental models on global state, and think about it differently. Their programs generally don't get maintained for decades though.) But "functional in the large" is basically the same thing as dependency injection in OOP languages. State and I/O are explicit parameters rather than globals. Here is a comment I wrote about that a long time ago, on Disadvantages of Purely Functional Programming: https://news.ycombinator.com/item?id=11841893 https://news.ycombinator.com/item?id=11841893 To me there's no real advantage to expressing something like "split a string by a delimiter" in a purely functional style. Either way, you have a trivial referentially transparent function you can reuse without causing complexity in your program. You might as well do the obvious imperative thing. ... So both "functions and data" are effectively and usefully implemented as classes. The key is to make some classes like functions, and some classes like data. And have more of a bipartite dependency graph, where functions depend on data, and data depends on functions. ----- I also explicitly structure the program as a graph, which I try to make acyclic, but again the problems and solutions are domain-specific. (Programming is very diverse and programmers tend to generalize from their own few examples.) For example, Compilers and interpreters are inherently mutually recursive, which gives you cyclic dependencies. As far as I can tell, this is the essential reason that Fabrice Bellard's QuickJS has one file that's 70K lines of C. And you can see the same thing if you look at the Zig compiler. In Oil I "modularize" the cyclic dependencies so you can plug them in and out at runtime (which is actually used in the program), but it has a performance cost.
- ben509 7y ago> Yeah exactly, I think of this as "imperative in the small" and "functional in the large". There's no real cost to local state, but there's a big cost to tracking global state. That's the basis of what I call "faux-mutability", wherein you have an immutable type system, but all updates are expressed as mutations. In the current docs for a language I'm working on, I give a brief example[1], but in a nutshell, you can transform an expression like so: let x.a.b := 5 let x := set_rec(x, #a, set_rec(x.a, #b, 5)) It does prohibit[2] making arbitrary graphs, but for the use case I'm targeting this is fine. (Also, as a prototype for how this can work, I wrote a python module[3] that implements it with the pyrsistent module.) [1]: https://tenet-lang.org/#how-does-it-work https://tenet-lang.org/#how-does-it-work [2]: Or you'd need another mechanism to do so. [3]: https://pypi.org/project/pyrsistent-mutable/ https://pypi.org/project/pyrsistent-mutable/
- kosievdmerwe 7y ago> Yeah exactly, I think of this as "imperative in the small" and "functional in the large". There's no real cost to local state, but there's a big cost to tracking global state. I think of it as having to deal with the fact that humans only have so much mental space/short term memory. The problem with stateful everything is that you can't build a complete understanding of it; there's just too much. However, for short bits of code that don't leak their imperativeness to the outside world, it doesn't matter. You can read the whole function and understand it.
- AnimalMuppet 7y agoI wish I had more than one upvote for this. > I sure want to keep it small, simple, easy to maintain… but the method to achieve this still eludes me. It eludes all of us. At best, we know some methods that have worked moderately well for some people in some circumstances.