4 ms·
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 tracki
by chubot 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.
(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.