from Hacker News

Coroutines make robot code easy

by bvisness on 6/22/23, 4:35 AM with 122 comments

  • by jcarrano on 6/22/23, 9:34 AM

    Yes. The callback is not a natural construct (i.e. it does not map well to our intuitive understanding of X is doing something while Y is doing something else).

    I'm annoyed when coroutines are reserved for use only in high performance, c10k-type of situations. For example, the KJ library's doc says:

    "Because of this, fibers should not be used just to make code look nice (C++20's co_await, described below, is a better way to do that)."

    With this "stackless or nothing" attitude we do not have good C++ coroutine libraries outside C++20's.

    For me the purpose IS to make the code look nice and I do not care if the coros are stackfull and consume more stack memory. At the end of the day, for my application, I saved more in programmer's time and bugs than I lost in RAM (and I'm on an embedded board with only 64MB)

  • by taeric on 6/22/23, 2:40 PM

    Coroutines are covered in Knuth's first volume. And, I confess, I think I went years thinking he was just describing method calls. Yes, they were method calls that had state attached, but that felt essentially like attaching the method to an object and calling it a day.

    Seeing them make an odd resurgence in recent years has been awkward. I'm not entirely clear that they make things much more readable than alternatives. Reminds me of thinking continuations were amazing, when I saw some demos. Than I saw some attempts at using them in anger, and that rarely worked out that well.

    Also to the point of the article, I love being "that guy" that points out that LISP having a very easy "code as data" path makes the concerns expressed over the "command" system basically go away. You can keep the code as, essentially:

        (DriveForward 0.5 48)
        (while (NotCarryingBall)
            (Grab)
            (pause 2)) 
        (DriveBackward -0.5 48)
        (Shoot)
    
    With god knows how much bike shedding around how you want to write the loop there.

    Of course, you could go further for the "pretty" code that you want by using conditions/restarts such that you could have:

        (DriveForward 0.5 48)
        (Grab)
        (DriveBackward -0.5 48)
        (Shoot)
    
    And then show what happens if "Grab" is unsuccessful and define a restart that is basically "sleep, then try again." Could start plugging in new restart ideas such as "turn a little, then try again." All without changing that core loop.
  • by samsquire on 6/22/23, 8:48 AM

    I think that developers have minimal scheduling primitives available to them, to schedule complicated work, in the order and timings you want it to have.

    I don't like hardcoding functions in coroutine pipelines. Depending on the ordering of your pipeline, you might have to create things and then refer to them, because of the forward reference problem.

    Here's my stackoverflow question for what I'm getting at:

    https://stackoverflow.com/questions/74420108/whats-the-canon...

    Coordinating work between independent threads of execution is adhoc and not really well developed. I would like to build a rich "process api" that can fork, merge, pause, yield, yield until, drop while, synchronize, wait (latch), react according to events. I feel every distributed systems builds this again and again.

    Go's and Occam's CSP is pretty powerful.

    I've noticed that people build turing completeness ontop of existing languages, probably due to the lack of expressivity of the original programming langauge to take turingness as an input.

  • by quantified on 6/22/23, 7:03 AM

    Thankfully a piece that emphasizes that coroutines are functions that pause. Java frameworks like Quasar became focused on other goals besides that basic capability and lost their way (IMHO).

    Java's not the easiest to pick up in high school unless you really make a big after-school effort. Something like Lua is probably better.

  • by rtpg on 6/22/23, 8:59 AM

    Coroutined make programming for pico8 very chill as well.

    The biggest challenge is sometimes you do want external flow control, and there coroutines can get hard to untangle if your design is a bit messy.

    Something like “A signals to B to do something else” starts to be a bit tricky (along with interruptible actions). I think there are good patterns in theory but I’ve found myself with pretty tangled knots at times.

    This is ultimately a general problem when programming everything as functions. Sometimes you need to mess with state that’s “hidden away” in your closure. Building out control flow data structures ends up becoming mandatory in many cases.

  • by pcblues on 6/22/23, 5:46 PM

    I took computer science at school in 1989 because I hated my chemistry teacher in 1988. Never looked back. I really loved the author's call for attention to whether kids are "getting" the concepts being taught and adjust accordingly.

    My teacher (Hi Mr Steele if you are still kicking around!!) taught us the algorithms without coding, but instead used playing cards or underwater bubbles or whatever. We had our a-ha moments intellectually before we implemented them in code.

    As an aside, our school had just got macs and we spent most of the day playing digitised sound files from Monty Python.

    "YOU TIT!"

  • by mgaunard on 6/22/23, 9:11 AM

    People always say that coroutines make code easier to understand, but I've always found normal asynchronous code with callbacks much easier to understand.

    They're equivalent except that asynchronous callbacks is what actually happens and you have clear control and visibility on how control flow moves.

  • by Izkata on 6/22/23, 12:09 PM

    The structure of the coroutine version looks very close to what I've been settling towards for my own background code (not robots but a similar "do a sequence of things that may take different amounts of time and rely on external state"). I'm not sure if it has a name so in my head it's been something like "converging towards a 'good' state":

    Every tick, inspect the state of the world. Then do the one thing that gets you a single step towards your goal.

    At first I wasn't sure the "inspect" part was possible in the robot system, but the Lua code makes it look like it is? If so, the change is basically changing the "while" to "if" and adding additional conditions, maybe with early returns so you don't need a huge stack of conditions.

    The "converging" style doesn't use coroutines and is more robust. Let's say, for example, another robot bumps into yours during the grab - the Lua code couldn't adapt, but the "converging" style has that built in since there's no assumed state that can get un-synchronized with the world like with a state machine / coroutine version. It was because of external interactions like that, that I couldn't 100% rely on but were inspectable, that I originally came up with this style.

  • by taberiand on 6/22/23, 8:24 AM

    Similar code could be written in C#, with code like

        IEnumerable<RobotCommand> MyRobotBrain() 
        {
            while(drivetrain.getDistanceInches() > -48) 
            {
                yield drivetrain.arcadeDrive(0.5, 0);
            }
    
            yield shooter.shoot();
        }
  • by vanderZwan on 6/22/23, 12:36 PM

    As always when this topic comes up, I have to plug Ceu[0] (formerly Céu) and the programming paradigm it represents. It doesn't use coroutines but synchronous concurrency. On top of that it's a reactive language:

        // an external input event channel
        input int KEY;
        
        // par/or concurrently executes two
        // or more blocks (called "trails")
        // if one of them terminates, the
        // other trails are aborted.
        // Compare: par/and, which waits
        // for all trails to terminate
        // before resuming code
        par/or do  
          // an infinite loop that awaits
          // a timer event. Therefore this
          // trail never terminates by itself
          every 1s do
            // Ceu uses C as a host language
            // and compiles to (essentially)
            // a giant finite state machine
            // C functions can be accessed
            // using an underscore prefix
            _printf("Hello World!\n");
          end  
        with
          // this trail awaits a keypress,
          // then terminates, ending the entire
          // par/or block.
          await KEY;   // awaits KEY input event
        end
        _printf("Bye!\n");
    
    The above code prints "Hello World!" ever second, until a key is pressed, after which it prints "Bye!" before terminating the program.

    The reactivity and intuitive single-threaded concurrency makes it a really nice language for low-powered devices. Or robotics, which is a lot about reacting to sensor input.

    It has a dedicated Arduino repo too[1].

    In practice it's more of a research language by Francisco Sant’Anna, a professor at UERJ, Brazil, than a language with a big community around it. He's currently working on a new version caleld Dynamic Ceu, or dceu[2]

    In the same paradigm there is the Blech[3] language, I believe originating from Bosch. Sadly, that project also has lost some steam.

    [0] https://github.com/ceu-lang/ceu-arduino

    [1] http://ceu-lang.org/

    [2] https://github.com/fsantanna/dceu

    [3] https://github.com/blech-lang/blech

  • by gavinray on 6/23/23, 3:25 AM

    You could have used Kotlin if you wanted to stay on the JVM and use coroutines.

    Your desired pseudo-code in Java would look like this in Kotlin:

        coroutineScope {
            // Drive backward
            launch {
                while (drivetrain.getDistanceInches() > -48) {
                    drivetrain.arcadeDrive(-0.5, 0)
                }
                drivetrain.arcadeDrive(0, 0)
            }
    
            // Grab for two seconds
            launch {
                val grabTimer = Timer()
                while (grabTimer.get() < 2) {
                    intake.grab()
                }
                intake.stopGrabbing()
            }
    
            // Drive forward
            launch {
                while (drivetrain.getDistanceInches() < 0) {
                    drivetrain.arcadeDrive(0.5, 0)
                }
                drivetrain.arcadeDrive(0, 0)
            }
        }
  • by ifyalciner on 6/22/23, 9:53 PM

    Coroutines (or the concept of saving context to come back to) is already heavily explored in robotics in recent years. Behaviour Trees [1] approximates what coroutines do in system level controlled ticks mainly to avoid pitfalls of state machines. They are also extensively used in game development too. ROS2 have Nav2[2] package which is based on BTCpp library [3]. Not surprisingly BTCpp library uses boost coroutines to implement some behaviours.

    Shameless plug, I have also been developing (weren't planning to advertise yet so no docs or plans for release) a behaviour based C++ library completely built on coroutines [4] to avoid some problems of "Behaviour Tree"s.

    [1] https://en.wikipedia.org/wiki/Behavior_tree_(artificial_inte... [2] https://en.wikipedia.org/wiki/Behavior_tree_(artificial_inte... [3] https://github.com/BehaviorTree/BehaviorTree.CPP [4] https://gitlab.com/ifyalciner/ferguson

  • by alex-moon on 6/22/23, 7:37 AM

    Had a go at writing a game a while back, purely for fun, reinventing the wheel to learn about what makes writing games hard. The command pattern here is a really great way to solve a persistent difficulty I had (largely the same one discussed in the article). I have certainly missed the point of the article but that is a great takeaway for me personally.

    Funnily enough I have used the command pattern before to automate sequences of steps in a workflow. Back then I kind of stumbled on it - it is super effective for certain kinds of things.

  • by dools on 6/22/23, 10:39 AM

    I don’t understand why doing this with normal Java is difficult. Just have a list of objectives. Each objective is a class. The autonomous loop picks the next objective from the list and each tick, asks if it has finished, if so get the next objective and so on.

    All the details about the actual commands to complete the objective and checking the state and so on go into the classes.

    If no more objectives in the list, mission accomplished.

    You can also easily test each objective independently.

    Maybe the trouble is trying to fight abstraction so hard in the first place.

  • by brooke2k on 6/22/23, 10:42 PM

    I had a similar revelation a few months ago when working on a little game in Lua.

    Since a game runs as a loop, all synchronous code needs to be able to execute within one frame.

    So you end up making overcomplicated state machines to represent processes that last for multiple frames.

    Coroutines make writing this code sooooo much easier. You can actually start to see the logic again at a glance rather than having to dive into a big stateful mess.

  • by jezzamon on 6/22/23, 2:37 PM

    I've been playing with JavaScript generators recently, and the ability to "pause" a function is exactly what I'm using them for. The goal is to create an animation of how a maze generation algorithm works, so it lets me write loops like I normally would but allow the function to pause which the website recenders the current state.

    When I create games, there's also a similar tick() function that's called every frame to handle the main logic, and normally to handle logic that needs to execute over multiple frames I write state machines like the article talks about, coupled with promises to allow things to chain off each other. This article might change how I write that code??

    I'm also thinking that instead of using a coroutine and yield(), you could asynchronous functions and "await nextTick()". Mostly for my own sake, trying to think of the differences between the two between the two:

    - Coroutines allow the decision of when to execute them to be made outside the function. Whereas asynchronous code goes off and does its own thing. With the corountine approach you fit into the normal code flow of having a tick function that does something every frame, so that seems better.

    - This approach only supports one coroutine at a time, whereas it's easy to kick off parallel operations with async code. Not being able to do things in parallel is probably desirable for a robot like this, as otherwise you might accidentally try to move towards two different goals at the same time.

    - You can interrupt a coroutine by just not calling it anymore. Async code generally can't be interrupted by an external function. So it would be easier to switch to doing new behaviour.

    - Composability. A quick search seems to indicate that support for nested coroutines in lua is not very good [1]. Which means that you can't split your main logic into small functions. In my language of JavaScript though, this isn't a problem thanks to yield*. But seems like asynchronous functions might win out there.

    Hm. More to think about. I suppose I'll try it on my next game jam.

    [1]: Is this the only way to yield all the results of a second coroutine in Lua?? https://gist.github.com/nicloay/2b893b3de1d964dcc92023c6318a...

  • by erdaniels on 6/22/23, 1:14 PM

    Maybe I'm missing something but why doesn't the Java section right before the Lua section not work? It looks like normal procedural code that can just keep running. The lua version is just one coroutine and it's not yielding to anything else. Is it just a matter of some kind of timing constraint/controller from FRC in the background that need to keep calling myAuto?
  • by shadowgovt on 6/22/23, 12:09 PM

    > We were basically creating a crappy programming language out of Java classes.

    At least the pedagogy is accurate. From UIs to database accessors, coding modern Java basically is creating a crappy programming language out of Java classes.

  • by ptr on 6/22/23, 9:45 AM

    Something that coroutines made a big impact on for us was testing. Multi-step integration tests became a breeze. With state machines, each test would need its own FSM, and callbacks would make the flow hard to read.
  • by calf on 6/22/23, 11:21 AM

    The Command pattern looks like transactional programming which is used in SystemC and similar systems-level programming languages. The idea is that transactions or commands have properties like atomicity and so forth to deal correctly with concurrent behaviors.

    Language support may be the deciding factor for young students, but conceptually the more interesting engineering debate would be which paradigm is better for a given purpose, coroutines or commands/transactions assuming the programming language can cleanly express both.

  • by foxbyte on 6/22/23, 6:53 PM

    I agree, the shift from Java's state machines or "command" system to Lua's coroutines does seem to make the code more intuitive and readable for beginners . Lua's in-built coroutine functionality can be a game-changer for FIRST teams dealing with the complexity of autonomous code 1. It would be fascinating to see how this technique could be applied to other areas of programming where tasks need to be paused and resumed. It's a testament to the versatility of coroutines.
  • by baylessj on 6/22/23, 4:19 PM

    It's really neat to see some FRC code here, lots to be learned from student robotics competitions! Shameless plug: I work on [PROS](https://github.com/purduesigbots/pros), an open source programming environment for VEX. We've talked about adding coroutine support there, this article is an additional push for getting that done!
  • by enum on 6/22/23, 12:05 PM

    Very cool to see. I had worked on something similar but in the context of JavaScript a few years ago (https://arxiv.org/abs/1909.03110). Without coroutines/continuations, it really would have been impossible to get people up to speed in the time we had (one week).
  • by mike_hearn on 6/22/23, 5:34 PM

    Although the procedural blocking style is definitely a lot easier, you don't need language support for coroutines to do that. They could have implemented this in Java by just having two threads. One manages the robot and the other main thread blocks whilst waiting for commands to execute. The two threads swap messages using a linked blocking queue.
  • by nstbayless on 6/22/23, 6:02 PM

    "Deep coroutines:" where you can yield from a function called from the coroutine. Lua supports this, but python doesn't (as far as I can tell). Is there a term for this?

    To the author: you could make the code even cleaner by moving the yield to within the action functions. Though maybe this won't work as well for parallel actions...

  • by actinium226 on 6/22/23, 2:38 PM

    Oddly, the kids I mentored in FIRST loved the command/subsystem framework. I kept trying to convince them to do some procedural code to make things simpler (they had little experience coding, even for high school robotics kids), but command/subsystem was their comfort zone and they didn't want to leave it.
  • by Dudester230602 on 6/22/23, 8:31 AM

    So many similarities with game development: central loop, coroutines, character state machine etc.
  • by pas on 6/22/23, 8:36 AM

    leaky abstractions and all aside, the coroutine code is unreadable to me after clean looking commands. :|

    yes, new Java(new Java(1), new ...) is bad, and asynchronous programming paradigms are usually fitting for robotics, but abstractions are good.

  • by giovannibonetti on 6/22/23, 10:46 PM

    Related discussion: "Notes on structured concurrency, or: Go statement considered harmful"

    https://news.ycombinator.com/item?id=16921761

  • by scotty79 on 6/22/23, 12:29 PM

    Maybe coroutines might become syntax for general hierarchical finite state machines if we manage to implement serialization of current execution state.

    I'd really love to see async/parallel language based on these ideas.

  • by TheAlchemist on 6/22/23, 12:12 PM

    Great article ! Thanks. I will definitely think about this approach when teaching my kids.

    I'm currently watching some videos by David Beazley, and he does several talks on coroutines in Python - very much recommended.

  • by mkoubaa on 6/22/23, 11:57 AM

    Is there such a thing as universally easier to grasp patterns? Maybe there are just brain types that map better to certain programming abstractions and we just have to accept that.
  • by cryptonector on 6/22/23, 2:34 PM

    Yes, threads and co-routines == sequential code, while callbacks == continuation passing style code. Our minds like sequential thinking.
  • by noobermin on 6/22/23, 8:56 AM

    Ah coroutines, essentially glorified goto. My 10keV take is that dijsktra's paper while probably right about goto back then has cursed us and we are still cursed till this day. Some logical structures map well to just using goto in a series of steps, but because of this allergy to goto, we're stuck where basically jumping into a random place in a function is "novel."
  • by greatgib on 6/22/23, 6:39 AM

    If you want to make it easy for high schoolers, just don't use java in the first place...
  • by sesuximo on 6/22/23, 1:10 PM

    Arguably this devalues the programming they are learning since Coroutines aren’t as generally applicable