1. 17
  1.  

  2. 11

    Huzzah, more spooky action at a distance, just what programs need. The points of contact between modules become your messages, which are essentially global in scope. And the rules may contradict each other or otherwise clash, and understanding what’s going on requires you to go through each module one by one, understand them fully, and then understand the interactions between them. This isn’t necessarily a deal breaker, but it also isn’t any simpler than any other method.

    Interesting idea, but I’m deeply unconvinced. It seems like making an actual complex system work with this style would lead to exactly the same as any other paradigm: a collection of modules communicating through well-defined interfaces. Because this is a method of building complex machines that our brains are good at understanding.

    1. 7

      IMO this comes from the fact that the act of writing/extending software easily that you’ve spent N years understanding and reading software later are two entirely different activities, and push your development style in different directions.

      The ability to write software that integrates easily pushes folks to APIs that favor extension, inversion of control, etc. This is the “industrial java complex” or something like it - and it appears in all languages I’ve ever worked on. I’ve never seen documentation overcome “spooky action at a distance”.

      The ability to read software and understand it pushes you to “if this and this, then this” programming, but can create long methods, lots of direct coupling of APIs etc. I’ve never seen folks resist the urge to clean up the “spaghetti code” that actually made for delicious reading.

      It’s my opinion that this is where we should build more abstractions and tools for human software development, similar to literate programming, layered programming, or model oriented programming. One set of tools are for writing software quickly and correctly, and another set of tools for reading and understanding, i.e. macroexpand-1 or gcc -E style views of code for learning & debugging, and a very abstract easy to manipulate view of code that allows for minimal changes for maximal behavioral extension.

      ¿por qué no los dos?

      1. 2

        The points of contact between modules become your messages, which are essentially global in scope.

        This was exactly my thought, too. It reminds me of a trade-off in OOP where I think you had to decide whether you want to be able to either add new types (classes) easily or add new methods easily. One approach allowed the one, the other approach the other. But you could not have both at the same time. Just can’t wrap my head around what exactly was the situation… (it might have been related to the visitor pattern, not sure anymore)

        In this case, the author seems to get easy addition/deletion of functions by having a hard time changing the “communication logic” / blocking semantics (which operation blocks which other operation, defined by block and waitFor). While in the standard way the “communication logic” is easy to change, because you just have to replace && by || or whatever you need, but the addition of new functions is harder.

        1. 3

          That’s sometimes known as the “expression problem”.

          https://eli.thegreenplace.net/2016/the-expression-problem-and-its-solutions/

      2. 3

        I think I see what this is trying to do, but I can’t see it leading to anything but an unmaintainable mess. It feels like it would be the kind of code that you only throw patches and tests at until it starts passing. I will admit that such a style is nice when you’re exploring or prototyping. Not having to care about structure is nice in that situation. I’m not seeing this working with large projects.

        Key difference is: there is no contact point between modules. They are always oblivious about each-other [sic].

        Sure, but there are still dependencies between them. Specifically data and behavioural dependencies. It’s not like you can avoid that. For example, is the input() stuff shared across everything? Controlling access to that sounds like a real chore. Based on what I’m seeing here, it seems to be pushing all the structural complexity of a system into the data.

        I will have to read the research to get a better idea. This introduction only makes me foresee maintenance and debugging nightmares.

        1. 3

          I’ve recently become very interested in behavioral programming through reading some of Harel’s publications.

          Almost all the programs I’ve seen in my working life have already been tangled up, confusing, hard to follow, etc, and behavioral programming seems to me like a very rare idea with real potential to improve system construction.

          I suspect that the notion of “action at a distance” isn’t really well-defined in software, or rather, that we must deal with such distances all the time, because of the very nature of software systems. We can’t make programs linear like billiard ball trajectories or whatever—a click here causes an action over there; a boolean toggled at time T1 causes a branch at T1000, etc.

          Behavioral threads are just a different way to organize these effects in order to get “feature locality” by which I mean that a single coherent piece of code implements a single feature (use case, scenario, etc). Indeed that is how Harel formulates the idea.

          One widely accepted practice is to formalize requirements in the form of use cases and scenarios. Our work extends this approach into using scenarios for actual programming.

          He also writes:

          As a general paradigm, behavioral programming is still in its infancy. It has been applied to a relatively small number of projects, and the existing tools are not yet of commercial power. More research and development is needed in expanding implementations in a variety of programming contexts and for larger real-world applications. We should also experiment with the collaboration of multiple development groups, and expand the work on formal verification, synthesis, performance and scalability, automated learning and adaptability, the use of natural language, and enhanced play-in.

          1. 3

            Intuitively a program written this way allows us to observe specific traces and swap-in and out modules to implement a change without having to deeply understand the structure of the program: because the changes don’t depend on the structure but on the combined behavior of the modules.

            This feels awful. Combined states are multiplicative. Having to know the exhibitor of the whole system to make any change means you’re managing a huge number of possible states.

            While there is no longer flow control in these modules, what’s worse is that there is implicit flow control in the timing and flow of Globally namespaces events. Modules have subtle pre- and -post-conditions in these events. How do know you need to block “bad”? Because (a) other upstream modules may be sending it and (b) other downstream models may react to it differently than “good”. Now multiply that against every potential state in the system.

            (A) and (B) above are integration points just like any other, but they’re totally implicit.

            You could solve this by hiding parts of the event space from certain modules. That feels like a necessity. But I don’t see how you do this without again integrating these modules.

            1. 3

              Won’t behavioural programming tend to create programs where you have no idea what the is going on any more, because who knows what code affects what?

              if (isMultipleOfThree(x) && endsWithDigitFive(x)) is very easy to understand, but isMultipleOfThree(x) and endsWithDigitFive(x) in different modules is a lot harder, even in your simple example.

              You already see this problem to some degree in event-driven programming (e.g. DOM, some GUI libraries like Qt) or promises in JavaScript, where following the exact flow of the program can be hard. This seems to make it even harder.

              1. 1

                Indeed, but I see this more as a trade-off: trading control flow for extensibility.

                However there are measures to understanding the flow even if isMultipleOfThree(x) and endsWithDigitFive(x) are in different modules. For instance check out this paper about “following the flow” at runtime: https://www.researchgate.net/profile/Gera_Weiss/publication/221219698_On_Visualization_and_Comprehension_of_Scenario-Based_Programs/links/0c960517ae9db11524000000.pdf

                1. 1

                  Also to add on what I just said, even though it’s a trade-off, I still think that reading each module on their own, one handling isMultipleOfThree and another endsWithDigitFive, is a lot more natural. When I have to deal with the logic concerning a number ending with the digit five, I just need to consult its appropriate module. While reading it I don’t have to account for the logic about “being multiple of three”. Makes sense?

                  1. 1

                    While reading it I don’t have to account for the logic about “being multiple of three”

                    But answering the question “under what conditions does this particular thing happen?” (a very common question) becomes a lot harder to explain because you need to somehow know all the modules that might act on this particular situation, then read all of them and know how they interact.

                    It seems to me that this will become harder when the business logic becomes more difficult (like “under this OR this circumstance, but only if that other thing occurs”)

                2. 3

                  This idea introduces complexity on so many levels, I’d love to see a language or framework that implements it just to see if it actually complicates software development.

                  Imagine being able to specify what is not allowed to happen, using block, even before the logic that generates such behavior is written:

                  …you could write const allowManualUpdate = false?

                  1. 1

                    Indeed, this approach can be implemented using general methods. It’s mainly an architecture. If you want to implement your behavior using global flags for forbidding/allowing different behavior, that can essentially be done. In the implementation I’ve shown I just used events instead of flags.

                  2. 3

                    I’ve been super interested in this idea ever since watching “Append only development with React”, and reading this Medium article which I now realize are by the author of this article. I’d love to see some more complicated real-world examples of where this approach has been applied.

                    1. 2

                      I always try to apply these to the code at work. Let’s see, back in 2013, we only accepted input from one source, and depending upon the features of the request, may or may not query A and send the response back. In 2015, we added a second input source. Then in 2017, if we query A, then we also need to query B at the same time (concurrent requests, because we’re under a realtime constraint). Now we have to handle requests that don’t make any queries, some that do A and B at the same time, some that do B only, and some that do B and then maybe A (sequentially), and some of this is dependent upon the source (added back in 2015).

                      1. 2

                        I really enjoyed reading this article, I find it’s very well written. Thank you for sharing it!

                        1. 1

                          Couldn’t get past…

                          const x = readInput();
                          if (isMultipleOfThree(x)) {
                            return true;
                          } else {
                            return false;
                          }
                          

                          Whhhyy!!?!? Why not…

                          const x = readInput();
                          return isMultipleOfThree(x);
                          
                          1. 1

                            The branching was used to introduce the counterpart example where each state is explicitly marked to allow other modules to behave differently.