Re:Ask HN: How does Common Lisp deal with side effects?
by Jtsummers on Tuesday January 18, 2022 @11:37AM
(#29981666)
> How come Common Lisp doesn't get bogged down in a quagmire of complexity like managing state in other programming languages end up being?
This notion that you either have pure functional programming or a quagmire is grossly exaggerated. Pure functional programming reduces the things that you can do (in a sense), so that you never have to deal with some complexity (but you have to deal with other complications instead). In a typical procedural/imperative language (which is pretty much everything outside declarative or pure functional languages), you avoid a quagmire by practicing sane programming.
This is the entire point behind the idea of loose coupling and high cohesion, as well as modular programming in general. If your components are loosely coupled (and have high cohesion) you narrow the scope of each component (whatever that may mean in your language, "module", "class", "package"). This lets you focus on the essentials of that area and avoid the "quagmire". The real quagmires tend to crop up when people:
1. Make excessive use of mutation in separate areas (where it is hard, from a reading comprehension perspective, to recognize that this is happening, too). Like in a project I was on in my first job (embedded system). They had a "task" system and variables were reused across tasks, this made it hard to know how a variable was changed especially as some tasks would only conditionally change the variable. It was not meant to communicate between tasks (like a signal to a later task, "Hey, there's something new for you to look at" and the reply "Hey, I looked at it"), but was instead being shared by complicated distributed logic. The simplification was to move the mutation into one task and change the logic so it was more comprehensible. Each task became loosely coupled in a way they previously weren't. This also increased the cohesion (there was no rational reason for some of the tasks to be changing it at all, the logic was just dumped in them because they had cycles to spare in their 100ms slot).
2. Reach into other components and change their state directly. Related to the above, but somewhat saner (still a bad idea). A component should control its own state. There are optimizations to be made to avoid copies and excessive function calls (more important in the embedded world, not something I fret about on desktops, and usually resolvable with compiler optimizations like function inlining), but at least as a first pass direct changes from outside should be minimized if not eliminated. The principle effect of this is related to DRY. If you allow direct access to the data of a component (particularly changing that data or state, reading is, or can be, different), then the knowledge of the invariants related to it must be distributed throughout the system. This knowledge easily becomes out of sync as the system evolves over time. Working on a system that controls a physical device right now, you would not want arbitrary parts of the system to be able to direct the motor to rotate X degrees in some direction. That's a great way to break it. Instead one part of the system owns the motor, and contains all the sanity checks (based on present position, 90º clockwise rotation is impossible, only rotate 30º or signal an error or whatever is appropriate).
And that certainly happens a lot, but there's no requirement for it when you're writing a program. Spend some time thinking, be sane, and you can avoid the "quagmires".
This notion that you either have pure functional programming or a quagmire is grossly exaggerated. Pure functional programming reduces the things that you can do (in a sense), so that you never have to deal with some complexity (but you have to deal with other complications instead). In a typical procedural/imperative language (which is pretty much everything outside declarative or pure functional languages), you avoid a quagmire by practicing sane programming.
This is the entire point behind the idea of loose coupling and high cohesion, as well as modular programming in general. If your components are loosely coupled (and have high cohesion) you narrow the scope of each component (whatever that may mean in your language, "module", "class", "package"). This lets you focus on the essentials of that area and avoid the "quagmire". The real quagmires tend to crop up when people:
1. Make excessive use of mutation in separate areas (where it is hard, from a reading comprehension perspective, to recognize that this is happening, too). Like in a project I was on in my first job (embedded system). They had a "task" system and variables were reused across tasks, this made it hard to know how a variable was changed especially as some tasks would only conditionally change the variable. It was not meant to communicate between tasks (like a signal to a later task, "Hey, there's something new for you to look at" and the reply "Hey, I looked at it"), but was instead being shared by complicated distributed logic. The simplification was to move the mutation into one task and change the logic so it was more comprehensible. Each task became loosely coupled in a way they previously weren't. This also increased the cohesion (there was no rational reason for some of the tasks to be changing it at all, the logic was just dumped in them because they had cycles to spare in their 100ms slot).
2. Reach into other components and change their state directly. Related to the above, but somewhat saner (still a bad idea). A component should control its own state. There are optimizations to be made to avoid copies and excessive function calls (more important in the embedded world, not something I fret about on desktops, and usually resolvable with compiler optimizations like function inlining), but at least as a first pass direct changes from outside should be minimized if not eliminated. The principle effect of this is related to DRY. If you allow direct access to the data of a component (particularly changing that data or state, reading is, or can be, different), then the knowledge of the invariants related to it must be distributed throughout the system. This knowledge easily becomes out of sync as the system evolves over time. Working on a system that controls a physical device right now, you would not want arbitrary parts of the system to be able to direct the motor to rotate X degrees in some direction. That's a great way to break it. Instead one part of the system owns the motor, and contains all the sanity checks (based on present position, 90º clockwise rotation is impossible, only rotate 30º or signal an error or whatever is appropriate).
And that certainly happens a lot, but there's no requirement for it when you're writing a program. Spend some time thinking, be sane, and you can avoid the "quagmires".