Depending Less on Structure
lmatteis.github.io
lmatteis.github.io
2. Good luck trying to optimise this (just merging all behaviours together at the end does not equal optimisation).
3. You're just pushing the problem elsewhere - now I need to understand all behaviours from a black box, then wrap them, all while dealing with all the other wrappers people have already put in place. Imagine dealing with that complexity.
But as presented, the code complexity scales like a nightmare; it makes the worst Ruby monkeypatching look sane. Something would have to be done about that before even a toy implementation could be done, because this will fall apart very quickly.
When you give away structured programming, you give away its benefits too. One of its benefits is that in a structured program, you can freeze a thread at any point and you have a clean mechanism for determining from there what its stack trace is, and all the relevant scopes it has access to and could be affecting it. In real code, this process may produce a very large number of variables and stack levels, but it'll still be a fraction of the program's possibilities. This is how structured programming helps us approach programs as structured slices of the code base, instead of a holistic view of the entire code base at once (this is the true reason goto-based code was so evil in the day; you could never do this analysis). This approach throws this away. That is not intrinsically wrong. But I'm not convinced at this time that the compensating features we get make up for losing that clarity, especially because the benefits are going to have terrible scaling problems as presented.
If someone was going to pursue this, this is the angle I'd take; how do I freeze my program and determine what is affecting the behavior of the current execution trace? Assume an arbitrarily powerful debugger attached to your running process, as long as it doesn't "magic" anything into existence. I can see sketches of ideas in my head, I am by no means saying this is unsolvable. But I think it's something I'd need to see laid out a bit more before I got too excited. To anyone thinking about this, I'd advise you to also not forget scale. Anything works for 100 lines of code. I need you to present me with something that can make sense of when I'm running, let's say, thousands of these advisory functions at a time on a single question. Structured programming has an answer to that; being a thousand layers deep in a stack trace may be a lot for a human to take in, but nevertheless, the procedures that structured programming provides will still give you answers. (And I'm not asking for the solution to work any better than structure programming does in that case. Thousands of anything is irreducibly complicated. But it needs to at least give me understanding in the ballpark of what I have in a thousand-deep stack trace.)
I would think if continually layering over the top of existing ideas was somehow better, then evolution would have gone that route instead.
Hinting at solutions that are closer to how we generally think or at least closer to how "non-coders" interact with computer systems should be more mainstream. For instance, this approach is much more similar to how non-coders write down requirements: when we discuss with people "clicking the button should stop the door from opening" we don't open a bunch of other documents and modify them accordingly to satisfy such statement. Instead we just "pile-on-top" such fact that might overwrite, contradict or replace other pieces of requirements.
The whole point of this article is to make programming similar to the way we write requirements.
If you literally cannot guess at a glance how it will work as a programmer, how can you expect a user to do so?
Even this trivial example is rather wrong. People do not think of message passing, synchronization or locks. We generally work on unordered, timesliced but fuzzy logic.
When I press a button, it will immediately light up a red lamp - nothing in this is remotely near to how a machine will implement it. There's no message passing in the transistor that sits on a wire. Immediately does not exist, but you can consider some things "perceptually immediate", putting a hard time bound. Yet our programming does not deal with time explicitly. Pressing a button means a contact, but it does not say for how long or whether any bounce has to be handled, because we think in terms of stable states when really they don't exist either. Yet there's immutability and hard state everywhere in our programming.
A human will handle a hardware error with a workaround, but software will fail in any unexpected scenario. And there's tons of these. We simplify programming by relying on the intelligence of the operator in many cases, but do not provide guidance. Literally not even whom or where to ask. It's good if the software even attempts to collect error info.
From what I can tell that's exactly what a 'module' is. By weakening the contracts between modules and turning synchronous execution into asynchronous calls, you've taken on creating a framework / language that takes the difficulties of distributed systems and made them local, so every bit of important logic is a distributed systems call.
I think this is a massive understatement. Imagine replacing most of the ifs in your program with new top-level constructs that run in parallel and exchange events. The maintenance overhead of this idea seems astronomical, for relatively little benefit. I would be terrified to add new modules to such a beast, knowing that they could affect the behavior of any other module, in complicated cascades.
In essence we are used to restricting our understanding of a piece of software through the structure and the code (and maybe some documentation). Instead this approach aims at a much more dynamic and feedback oriented view on debugging: if blocking an event causes other things to break one should be able to run tests with the new blocking module in place and figure out why things break; just like you do in normal code.
I would argue that also a small change to a strictly-structured program might cause the wreckage of other use cases.
In badly-designed conventional code, changes can have far-reaching consequences, which makes it hard to understand, but under this proposal, it would seem to be the default. Reasoning about asynchronous behavior is hard. (These points are demonstrated by how much had to be written to explain the example.)
The author says that merging is the problem, and that this approach obviates it. With conventional programming, the pain of merging can be mitigated with the careful separation of concerns, while this approach seems to turn every aspect of programming into merging.
Everything I have said here about conventional programming depends on doing it well, but at least you have the option to do so, and simplify things through the separation of concerns, while I suspect the approach here makes combinatorial explosions of complexity almost unavoidable. The goal is not to make complex software, but to solve complex problems with software that is as simple as possible to produce and verify.
This.
Nevertheless, taking ideas to the extreme might yield interesting insights.
My main takeaway was that you could build complex behavior by layering together reflexes. You wouldn't actually disable an existing layer, just add an overlay to disable it in certain circumstances.
I heard in a lecture once that our hands are wired similarly; we have a nerve that triggers a grasping motion involving all fingers, then we have overlay nerves that can disable the signal on a per-finger basis.
It may be that this is a fine way to build complex systems that's not optimal for _human_ reasoning patterns, but may for evolution (or other hypothetical non-human reasoning processes).
[0] - https://en.wikipedia.org/wiki/Subsumption_architecture
This sounds similar to the Cue configuration language? I haven't used it but I understand that you can add new constraints without necessarily understanding all the other constraints.
You probably do need to test the results to make sure you haven't overconstrained things, though?
One of the issues when then number of modules grow to a large number is that they may all have different conditions to keep track of. It’s then possible that the modules all contradict each other and never yield an executable state. Imagine trying to debug a system like that.
The next layer is then to ask, with all the modules and their output state, will their combinations ever be satisfiable. At that point, we turn the states into a large predicate logic equation and can use a SAT solver to figure that out.
I left off there in 2005, but the project is still being researched on. If you’re interested I can get you in contact with the professor.
- https://arxiv.org/abs/1909.00408
- https://www.researchgate.net/publication/338358976_Executing...
I am currently reading through your research and will sure get back to you about this.
But there are times you do want to understand how things work. With lack of clear "connections" to follow in the code, it seems like it would become harder to reason about what is going on in the first place.
OTOH, if one were to think about it, we do anyways build software in the manner described -- the only difference being that we 'sync()' on a different layer of abstraction -- the OS, the network, the libraries, the apis ..etc. Building software to be modular, pluggable, extensible, swappable at those layers has always been considered to be good practice. Is the benifit of creating additional absraction layers within the software itself really worth the complexity ? I'm not convinced.
(pseudocode) bool satisfies_all(value, functions) { for each funcction in functions { return false if not function(value) }; return true }
I get that the author wanted to go with a very basic example to keep it easy to understand, but it seems like that's the main problem with the proposed mechanism: It gets very hard to understand very quickly, leading to spaghetti code.
In this new Behavioral Programming style we can instead map these changes quite naturally to new modules that can be swapped into the program without touching or even seeing how the system works. </quote>
If it's so natural, where's the example?
An interesting way to think about it.
Sure, you can "lock out" using/modifying such operations, but one must know when and where to lock someone/something out. Programming is still a persnickety endeavor with or without using events.
Rather, "while leaving integration work undone, pretending it doesn't exist".
Just my understanding after reading the article ¯\_(ツ)_/¯
sync() calls that allow a module to peek at other modules and control their execution.
> Let’s rewrite the program above using a sort of new “language” with different execution semantics.
> Let’s rewrite the program above using a sort of new “language” with different execution semantics.
The semantics of it seem to be "wait for an event with some tags".
return isMultipleOfThree(readInput());However, I’d hardly describe it as “just”, it’s a fairly interesting idea.
I'd argue that it's an absolutely amazing idea however this idea has been brought into CS 31 years ago and utilised heavily ever since, so an article like this should imho at least mention it so that people can go away and learn about these concepts.
PS: My GP comment got heavily downvoted, which means that I'm missing something here.
I'm also wondering why this is; is there some obvious innovation described in the article that I missed?
If I'm missing something, could you point to some monad that can be used to `wait until an event named "good" is generated, and abort if an event named "bad" is generated`?
But even from a non-functional perspective, I think the article is reinventing the wheel. We have a name for that sort of behavior: Object Orientation. Not in the UML/Java/C++ sense, but the original idea that centered around message-passing. Think erlang or elixir, those work in a very similar way already, and it's why people love them so much.
There is certainly some inovative thinking in the article, but that's mostly relating to the syntax and the specific semantics, not the overall idea of evaluating conditions concurrently and joining them in the end.
I really don't think this is like map-reduce, map-reduce usually focuses on pure functions applied in a series and/or in parallel, with the entire pipeline acting essentially like one large pure function that can easily be distributed.
In contrast, this idea seems to focus on persistent side-effectful procedures that react to a string of events, with constant global syncing between an unbounded number of modules, which makes it impossible to distribute and highly non-pure.
Also, this seems like a much more specific idea than Objects or even than Erlang. It specifically focuses on extremely small "objects"/"processes", it specifies rigid semantics for their communication, and a frankly insane composition model for the whole program.
I'm pretty sure you could implement this in Erlang, but also pretty sure that this is not how most Erlang programs are written. Similarly, this is within the paradigm of Object Orientation, but it is a very specific example and that there are other (saner, imho) ways of composing Object Oriented programs that conform to the original Alan Kay ideas.
The idea would be to encapsulate your whole program in a very specific monad, one that:
- runs all functions in parallel
- implements sync points between functions
- implements an event system with named events and attached values
This does not at all reduce to "reinventing monads".
Note: I think the idea is horrible, any moderately complex program written this way would be entirely unmaintainable in my opinion. The author touches on this towards the end, but I think they are completely minimizing the down-side.
I agree so much. I can't help but think that modifying code you don't understand by injecting more code via hooks is a recipe for disaster, and the next person maintaining the composite product has to chase down all (ab)users of these events like we once had to hunt down abusers of global variables to understand what the hell is going on.
I don't even agree with the premise that this thing is less dependent on structure. Arguably, it is more dependent. It's just that instead of structure at the syntax level, we're now dealing with structure at a higher level, structure that is opaque to syntax but nevertheless present. Those named events and synchronization mechanisms make up their own structure.
Treating code as a black box with regard to making behavioural changes is essentially taking the worst aspects of TDD to extremes. It is not enough that it gives the right answers for your test cases, since you can only test a vanishingly tiny subset of all cases which need to be handled. It must be possible to reason about all possible outcomes, and that means removing unnecessary logic, not plastering over it.
Removing logic can only go as far as the essentials that are required for solving the problem.