[HN Gopher] Coroutines make robot code easy
___________________________________________________________________
Coroutines make robot code easy
Author : bvisness
Score : 187 points
Date : 2023-06-22 04:35 UTC (18 hours ago)
(HTM) web link (bvisness.me)
(TXT) w3m dump (bvisness.me)
| scotty79 wrote:
| 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.
| enum wrote:
| 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).
| noobermin wrote:
| 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."
| kachnuv_ocasek wrote:
| Coroutines/continuations are not "basically jumping into a
| random place in a function". It's pausing the execution of a
| function in a well-defined place with well-defined,
| understandable semantics for resuming the execution.
| ptr wrote:
| 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.
| alex-moon wrote:
| 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.
| rtpg wrote:
| 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.
| ifyalciner wrote:
| 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
| josephg wrote:
| Given your expertise, why use behaviour trees when coroutines
| are available? What value do they provide in languages like
| javascript where generators work well?
| ifyalciner wrote:
| Behaviour Trees are a bit more than that. They also
| effect/advice how you structure your project (not component
| based but behaviour based: exp. you don't have "controller"
| component in your architecture to handle all locomation but
| "go_to_pose" or "turn_around" behaviour) plus there are
| tooling provided around those architectures.
|
| Design of Behaviour Trees were not an answer to lack of
| coroutines/generators (although they were missing from C++
| standards until C++20) but an alternative to state machines.
|
| Although I agree that "tick" based BT implementation is
| outdated and should completely be replaced by coroutines and
| mandatory yields (thats what I did in my implementation).
| There is still value in behaviour based architectures in
| robotics. They make building complex/reactive behaviours much
| easier than component based ones.
| erdaniels wrote:
| 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?
| bvisness wrote:
| autonomousPeriodic is a tick function; we need to keep ticking.
| If we spent more than our allotted 20ms in a single
| autonomousPeriodic then the robot framework's safety systems
| kick in and disable all the motors.
| erdaniels wrote:
| Ah okay that makes sense, thanks! It may help to make that
| note near the final java example.
| dwohnitmok wrote:
| The robots operate on N ticks per second. You only get one set
| of robot inputs per tick and can only make one command.
|
| The Java code is tickless. During a single tick none of the
| while conditions will change.
|
| Therefore the while loop will run forever and never allow the
| tick to finish.
|
| The Lua code yields at the end of any loop iteration allowing
| the tick to complete. Then whatever is orchestrating the robot
| on the next tick calls resume on the coroutine allowing the
| another iteration to continue, this time with new inputs.
| shadowgovt wrote:
| > 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.
| Izkata wrote:
| 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.
| mike_hearn wrote:
| 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.
| baylessj wrote:
| 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!
| taberiand wrote:
| 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(); }
| sparkie wrote:
| I seem to recall reading (though quite some time ago so I may
| be mistaken), that `yield` was developed for use in robotics as
| part of the CCR (Concurrency and Coordination Runtime), and
| it's inclusion in C# 2.0 was to support this.
| thaumasiotes wrote:
| I read the article, but I failed to understand the problem.
| What's happening when you pause? Why do you need your
| functions to pause?
|
| The example robot involves taking a deterministic sequence of
| actions in a fixed order. The state machine is a line. The
| article seems to be pretty clear that this code is bad:
| while(drivetrain.getDistanceInches() > -48) {
| drivetrain.arcadeDrive(0.5, 0); }
|
| and this code is good:
| while(drivetrain.getDistanceInches() > -48) {
| yield drivetrain.arcadeDrive(0.5, 0); }
|
| But pausing doesn't seem to be the functionality that's
| missing. The first loop will drive backwards until
| getDistanceInches is at most -48. The second one will also
| drive backwards until getDistanceInches is at most -48, but
| it will "pause" intermittently while it drives there. If my
| robot drives all the way in, I guess, one step (?), what will
| go wrong?
|
| The tick function is called 50 times per second. Is it
| constrained to terminate within 0.02 real-time seconds? What
| if you want to think hard about something?
| taeric wrote:
| The problem is more in how you combine that with something
| else. Consider, you have two "routines", one for "pick up
| ammo and take a shot" and one for "evade attack". Without a
| "yield", then your recovery time after an action is the
| longest chain of actions you have encoded. With the yield,
| it is the longest of a particular "command" that you have.
| taberiand wrote:
| Only one action can run per robot loop. The first code
| isn't bad exactly - it just doesn't work for how the robot
| needs to operate.
|
| Essentially the first code block has the issue mentioned in
| the article: Unfortunately, we can't do
| this because we need our autonomousPeriodic function to
| keep ticking. Loops like this will never finish and will
| cause the robot program to hang. So you can't use loops!
|
| The second code block yields every command back to the
| robot until the robot is ready for the next command - it's
| not actually the same as the article code but I think it's
| better design to return a command to the robot than to run
| a mutable operation in the brain code, it makes it clearer
| to the user that it is necessary to let the robot do the
| work.
|
| The article's point is there are many ways to write the
| state machine code that makes this possible - and the
| coroutine approach is the easiest for students to
| understand. It's also quite clean and practical for real
| world use too.
| isanjay wrote:
| Dots instead of colons ?
| taberiand wrote:
| Yes good catch; oversight from posting on mobile. I've fixed
| it up
| [deleted]
| quantified wrote:
| 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.
| flanked-evergl wrote:
| > Java frameworks like Quasar became focused on other goals
| besides that basic capability and lost their way (IMHO).
|
| A bit of an aside maybe, but the guy that made Quasar is behind
| project loom to add light weight threads to JVM which will
| become available in JVM 21 https://openjdk.org/jeps/444
| shadowgovt wrote:
| For FIRST robotics in particular, Java and LabView are the most
| commonly used languages because they are the two for which a
| robust library is maintained by Worcester Polytechnic Institute
| for use in the competition.
|
| But I have long been of the opinion that both languages are
| kind of an ill fit for the application. Python is making some
| headway and I think it might be a better fit; It has some
| advantages on legibility, it has an optional static typing
| system, and it does support coroutines.
| niederman wrote:
| Thankfully, very few teams are still using LabView at this
| point and WPILib primarily targets C++ and Java, with first-
| party Python support coming next season.
| moffkalast wrote:
| Unfortunately high schools are forced to continue teaching
| Java, or else Oracle starts executing hostages.
| galangalalgol wrote:
| In my day Borland had all the hostages, and every time you
| used something that wasn't orange text turbo pascal another
| one got fed to a gru.
|
| The right intro language is an interesting study in itself
| and the intuitiveness of coroutines is an good data point. Go
| seems like a decent first one. It jas dark corners, but at
| least they are in the corner. I'd love to argue for rust as a
| first language, and maybe it isn't a bad one. But I'm not
| sure where I'd start the argument. C was my second language,
| and I'm not sure it would have made sense as qyuckly before
| commodore basic.
| gorjusborg wrote:
| I feel bad for autonomous participants, Java is almost a
| uniquely poor choice for the size of project FIRST teams
| would be creating.
|
| Java has its domains, but small, algorithm driven code worked
| on by a small, inexperienced group isn't one.
| bvisness wrote:
| Especially when people try to impose Enterprise Quality
| Java on the students, and your project gets bloated with
| interfaces and dependency injection and config files...all
| for a single platform with fixed hardware :(
|
| When we were still using Java, I had the students just make
| everything static. We don't need more than one instance of
| our IntakeSubsystem. We only have one intake. But the OOP
| boilerplate persists.
| happymellon wrote:
| I don't know Quasar, but a lot of projects seem to want to add
| functionality rather than be a library for that purpose, and
| create a new one for the completely unrelated feature that you
| want.
| TheAlchemist wrote:
| 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.
| dools wrote:
| 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.
| shadowgovt wrote:
| Having taught FRC students to use Java: when you're talking
| about people with very little experience programming before,
| the multiple class abstraction is itself an obstacle to
| accomplishing the goal.
|
| I can't tell you how many times students have gotten frustrated
| trying to understand why you have to pass arguments into the
| constructor or, for that matter, Why the constructor is
| different from other method calls. "But we already said
| 'drivetrain' in the constructor, and over here in this other
| class. Why do we have to say m_drivetrain in the class also and
| do m_drivetrain = drivetrain?" And there isn't actually a
| better answer than "in other languages that learned from Java's
| mistakes, you don't. But we happen to be using a language that
| dates back to when Animaniacs was teaching kids the names all
| the countries, so some parts are just bad."
| dools wrote:
| > the multiple class abstraction is itself an obstacle to
| accomplishing the goal.
|
| Not if your goal is to learn about object orientated
| programming!
|
| > I can't tell you how many times students have gotten
| frustrated trying to understand why you have to pass
| arguments into the constructor or, for that matter, Why the
| constructor is different from other method calls. "But we
| already said 'drivetrain' in the constructor, and over here
| in this other class. Why do we have to say m_drivetrain in
| the class also and do m_drivetrain = drivetrain?" And there
| isn't actually a better answer than "in other languages that
| learned from Java's mistakes, you don't. But we happen to be
| using a language that dates back to when Animaniacs was
| teaching kids the names all the countries, so some parts are
| just bad."
|
| Do they also have nervous breakdowns every time the spell or
| read the word "knight"? The etymology of a language can be
| interesting, and they're welcome to look it up on their own
| time, but the sooner they learn that all of these decisions
| are arbitrary, the better.
|
| When I was first learning how to use FreeBSD I was equally
| confounded by trying to apply logic and reason to how the
| commands looked and worked. Once I just accepted the fact
| that it was no different to questioning why the buttons were
| on the left side of the toaster rather than right side, or
| why sought and sort are two different words pronounced the
| same my life got a whole lot easier.
|
| When I'm teaching kids about code and they ask "why ... "
| it's an opportunity for them to learn that oh-so-important
| lesson that in almost all cases the design decisions in
| software are totally arbitrary or of such obscure
| etymological origin that, unless you actually want to be a
| computer science historian, the best answer is "because".
| [deleted]
| [deleted]
| jezzamon wrote:
| It's addressed in the article... you sort of end up with a
| meta-programming language when you do that, which ends up being
| less ergonomic than your initial code. Then you have questions
| like how do you do control flow within that list? Can you
| branch or loop over multiple objectives? If you add support for
| that you end up even closer to a meta programming language,
| with worse syntax than if things were directly in the base
| language.
| dools wrote:
| > you sort of end up with a meta-programming language when
| you do that, which ends up being less ergonomic than your
| initial code
|
| I would say that what you end up with is a program.
|
| > Then you have questions like how do you do control flow
| within that list
|
| You don't. Each objective has an "isDone" method. That method
| returns true if done, false if not. If unable to complete its
| objective for some reason, throw an exception. Seems like
| pretty canonical object orientation to me.
|
| > Can you branch or loop over multiple objectives?
|
| No, but you could easily have an objective that groups
| together multiple smaller discrete objectives.
|
| > If you add support for that you end up even closer to a
| meta programming language, with worse syntax than if things
| were directly in the base language.
|
| Again, I just think you end up with a program, using the
| syntax of the language in which you're writing.
|
| Like if you write a story, you end up with a story, written
| using the language in which you wrote it.
| vanderZwan wrote:
| As always when this topic comes up, I have to plug Ceu[0]
| (formerly Ceu) 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
| mgaunard wrote:
| 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.
| lhecker wrote:
| > They're equivalent except that asynchronous callbacks is what
| actually happens [...]
|
| Neither stackful nor stackless coroutines work like this
| practice. The former suspends coroutines by saving and
| restoring the CPU state (and stack) and the latter compiles
| down to state machines, as mentioned in the article. Coroutines
| are functionally not equivalent to callbacks at all.
| omnicognate wrote:
| Which is exactly what happens when you use asynchronous
| callbacks except that you have to do the storing of state
| explicitly. Stackless coroutines even typically compile to
| (or are defined as equivalent to) callback based code.
| mgaunard wrote:
| Stackless coroutines are literally the same thing.
|
| Stackful coroutines are just a poor man's threads.
| pie_flavor wrote:
| Callbacks are no more 'what actually happens' than coroutines
| are; what actually happens involves a lot of jumping to memory
| addresses, and closure state is just as much a compiler
| invention as async/await. Blocking-style code, by comparison,
| is how we actually _think_ about the business logic; language
| features that abstract over callback hell to let you write it
| make code inherently more clear. People always say it because
| it 's true.
| mgaunard wrote:
| Closure state is a serious matter, especially if you're not
| in a garbage-collected language.
| armitron wrote:
| I imagine you would get a lot of blank stares with that POV, at
| least from folks with working bullshit detectors (like young
| kids that haven't been conditioned to "modern" industry
| practices).
|
| I found the article a great example of the kind of crap that
| passes for programming these days.
| shadowgovt wrote:
| In the context of the first robotics competition, teaching
| the command hierarchy is a good opportunity to teach students
| what a state machine is... And that can be a good opportunity
| to talk about what a computer does, because the computer is
| basically a hardware implementation of a state machine.
|
| But that isn't the kind of lesson that you want to be
| cramming into the middle of the competition season.
| cturner wrote:
| The coroutine approach shines for complex business-logic.
|
| Consider this example algorithm, of several async steps,
|
| 1. Download a file into memory
|
| 2. Email a link to a review web page.
|
| 3. Wait for the user to review.
|
| 4. Upload the file to a partner.
|
| 5. Update a database.
|
| You could implement this as callbacks. Callback from each step
| leads to the next being triggered. Downside - your business
| logic is spread across all the callbacks. You could mitigate
| this somewhat by defining a class with one method for each
| step, with those methods being defined in the same visual order
| as the algorithm. Then have each callbacks call a method. (The
| article shows something different but similar with its Command
| autoCommand pattern.)
|
| Tricks like this only go so far. Imagine if the reviewer user
| had a choice of pressing 'approve' or 'reject' on the webserver
| interface, with the algorithm changing depending on their
| answer. How do you now represent the business logic so the
| programmer can follow it?
|
| Such changes are easy in coroutines. Here is the algorithm with
| that variation in coroutine code, async def
| review(review_id, url): file_content = await
| download_large_file(url, review_id) ws_review_id =
| await create_webserver_review_page(file_content)
| await email_the_user(ws_review_id) result = await
| get_webserver_user_response(ws_review_id) if result:
| await upload_to_partner(file_content) else:
| await alert_failure(review_id, file_content) await
| update_database(review_id, ws_review_id, result)
|
| You state that callback code gives you easy visibility to what
| actually happens - yes, they do. When you read callback code,
| it is natural to follow business-logic to system calls.
| Coroutine tends towards code of layered business-logic and
| abstraction.
| mgaunard wrote:
| Just use lambdas, that makes the sequence local.
| jezzamon wrote:
| If you want to see the callbacks, an alternative middle ground
| is promises, e.g. code that looks like doSomething().then(() =>
| doSomethingElse()).then(() => doLastTask());
|
| I currently work on a project that involves Java code with a
| promise library, and Unreal Engine C++ code which does not and
| uses callbacks (and do async JS stuff in my personal projects),
| and both have to do asynchronous logic. The Unreal code is just
| so much harder to deal with.
|
| Specific problems the Unreal code has:
|
| - There's no "high level" part of the code that you can look at
| to see what the logic flow is.
|
| - Many functions are side-effecty, triggering the next part of
| the sequential logic without it being clear that that's what
| they're doing. Like the handleFetchAccount() callback kicks off
| another httpRequest for the next step, but you wouldn't know
| that it does that just from the name.
|
| I'd admit some of these problems might be mitigatable in a
| better written codebase though.
| brooke2k wrote:
| 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.
| Dudester230602 wrote:
| So many similarities with game development: central loop,
| coroutines, character state machine etc.
| ralphc wrote:
| An autonomous robot is just a NPC in the real world.
| calf wrote:
| 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.
| cryptonector wrote:
| Yes, threads and co-routines == sequential code, while callbacks
| == continuation passing style code. Our minds like sequential
| thinking.
| pas wrote:
| 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.
| ptr wrote:
| Coroutines are an abstraction though.
| giovannibonetti wrote:
| Related discussion: "Notes on structured concurrency, or: Go
| statement considered harmful"
|
| https://news.ycombinator.com/item?id=16921761
| greatgib wrote:
| If you want to make it easy for high schoolers, just don't use
| java in the first place...
| rootlocus wrote:
| We learned borland pascal and borland c in high school. Java is
| perfectly fine.
| shadowgovt wrote:
| I learned assembly in high school, and programmed my first
| FRC machine in NBASIC (a variant of BASIC built specifically
| for the robot controller back in the day that, among its
| other delightful quirks, had if statements that were only
| allowed to be followed by a label and no else clause).
|
| Java is "fine" in the sense that NBASIC was "fine." There's
| definite room for improvement in matching the abstraction to
| the problem domain.
| bedobi wrote:
| I went to a university where Java was the main language for
| most of the basic programming courses
|
| I've been making a living coding in Java for 10+ years
|
| I'm a strong believer in types etc etc
|
| I still don't think Java is a great language to teach
| programming lol
|
| too much pointless boilerplate and abstraction to achieve the
| simplest things
|
| lots of footguns and objectively bad standard practices built
| into the language and the native libraries
|
| to me Python and Kotlin seem like probably better choices
| (though they too have massive flaws)
| shadowgovt wrote:
| I've written enough Java at this point to think of it as
| the assembly of OOP languages.
|
| You can write code in it, but in industry almost everything
| I see is basically a DSL built out of annotations that
| tries as hard as it can to _not_ involve writing actual
| Java.
|
| When more of your application is annotations than lines of
| Java code, the language is probably a bad fit for the
| problem domain.
| esafak wrote:
| What's Kotlin's massive flaw?
| bedobi wrote:
| To me the whole point of Kotlin is fixing all the things
| that are wrong with Java lol
|
| But idiomatic Kotlin is still too close to Java, they
| call it "direct programming style" and it's basically
| already obsolete
|
| Null safety is great but the way they've implemented it
| with weird syntax like ? and ?.let is not great, it would
| have been a lot better to just have a regular Maybe type
| in the native library from the start. Like sure the
| nullable type is in some ways functionally equivalent,
| but it doesn't implement map, flatMap, applicative etc
| like a Maybe does, and when it behaves like it does, it's
| mostly kind of by accident, not by any real understanding
| of those operations (and, fatally, it's completely
| disconnected from map, flatMap etc on lists etc - those
| should be _interfaces_ that everything that can be
| mapped, flatmapped etc on implements, not methods that
| get hurr durr added here and there, sometimes as a
| regular method called map, sometimes built into the
| compiler called let)
|
| They've completely dropped the ball on error handling -
| they don't even know themselves whether the language or
| people using it should use sealed classes, exceptions or
| their shitty Result class (which, again, this is a solved
| problem, they could have just included Maybe, Either, Try
| etc in the native library from the start)
|
| Coroutines and structured concurrency are powerful but
| their API is so extremely confusing and full of footguns
| that virtually no codebase using them ever uses them
| correctly to reap the benefits
|
| I could go on - don't get me wrong, I love a lot about
| Kotlin and it's a massive massive improvement on Java but
| it's not a great language unless you run it with Arrow
| and learn to completely sidestep large parts of the
| language (just like you had to do with Java)
| 62951413 wrote:
| Don't get me wrong, I miss Turbo Vision-based UIs and an IDE
| that fits onto a single floppy disk. But the JVM is a
| professional tool to begin with. So if you cannot start with
| a toy such as golang or python at least go with Kotlin.
|
| Borland Pascal already had reasonable static types, classes,
| pointers, modules with a public API, and blinding compilation
| speed in the early 90s. All the basic building blocks to
| grok. Golang seems to be the closest modern-day
| approximation.
| bvisness wrote:
| The only officially supported languages for the competition are
| C++, Java, and LabVIEW. When those are your educational
| options...you stick with Java.
|
| (We've now switched to Lua, integrated with the official C++,
| but it's a lot more work behind the scenes.)
| mcv wrote:
| Why not Python? I thought that was the popular language for
| learning these days. My son gets quite a bit of Python in
| school.
| heffer wrote:
| Python is slated to become a choice for the 2024 season:
| https://wpilib.org/blog/bringing-python-to-frc
| hgsgm wrote:
| If it is that flexible, can you have a C++ program that runs
| a python interpreter as a module?
| ralphc wrote:
| What's the hardware/OS stack you have for the autonomous
| part? Can you go wild and use unsupported software as long as
| it fits in the official hardware?
| bvisness wrote:
| We can use whatever software we want as long as we use the
| specified hardware (the NI roboRIO), FIRST's latest
| firmware, and comply with the match system and safety
| systems that enable and disable the bot at different times.
| Many teams have been using a community Python alternative
| called RobotPy for a long time despite it not being
| officially supported and FIRST had no problem with it.
| shadowgovt wrote:
| The largest constraint on this (in the context of the FIRST
| competition) is that community matters at least as much as
| technology.
|
| If you're using Java (or C++ or LabVIEW, to a lesser
| extent), you at least have a hope of posting to a forum and
| going "Hey, we think our robot should be doing X and it's
| doing Y, here's our code, any insights?"
|
| If you use <bespoke language X> and your own custom
| bindings, you're on your own.
| mkoubaa wrote:
| 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.
| jezzamon wrote:
| 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...
| actinium226 wrote:
| 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.
| shadowgovt wrote:
| It makes sense for students that can make the logical leap that
| "things the robot can do" are objects too (which is only a tiny
| step from the nicely-recursive "code can just be an object").
|
| ... but not everybody is ready for that step.
| taeric wrote:
| 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.
| samsquire wrote:
| 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.
| noobermin wrote:
| I mean, this is an interesting thing to think about but isn't
| at all what the article is about...coroutines have many uses
| may be and are often related to async work in general but OP is
| using coroutines just as a pauseable function (in good old asm,
| you know how computers actually just work, this is just jumping
| into a subroutine).
| samsquire wrote:
| I think the article implements stackful coroutines since the
| yield calls a function where control flow jumps to, it
| eventually returns to the instruction(s) after the yield
| statement.
|
| (That is, it's not a JMP)
| bluetomcat wrote:
| > 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.
|
| Years ago I attempted to build an experimental language with
| first-class resumable functions. Every function can be invoked
| by the caller through a special reference type called "quaint".
| The caller can resume or stop the execution of the function
| either after reaching a timeout, or after passing a "wait
| label":
|
| https://github.com/bbu/quaint-lang
|
| A typical CPU-intensive example where preemption is done by the
| caller after a certain timeout: entry
| { fibq: quaint(u32) = ~fibonacci(32 as u32);
| ps("At start: "), pu8(fibq@start), pnl(); iter: u32
| = 0:u32; do { wait fibq for
| 1000 msec; ps("Iteration "), pu32(iter++),
| pnl(); } while !fibq@end; ps("At
| end: "), pu8(fibq@end), pnl(); const value: u32 =
| *fibq; ps("Reaped value: "), pu32(value), pnl();
| } fibonacci(number: u32): u32 {
| if number == 0:u32 || number == 1:u32 { return
| number; } else { return
| fibonacci(number - 1:u32) + fibonacci(number - 2:u32);
| } }
|
| An example that uses "wait labels" to suspend execution of the
| callee at certain points: entry {
| q: quaint(u64) = ~pointless_function(); wait q
| until pointless_function::label_a; ps("At label_a:
| "), pu8(q@pointless_function::label_a), pnl();
| wait q until pointless_function::label_b; ps("At
| label_b: "), pu8(q@pointless_function::label_b), pnl();
| wait q until pointless_function::label_c; ps("At
| label_c: "), pu8(q@pointless_function::label_c), pnl();
| wait q; ps("At end: "), pu8(q@end), pnl();
| ps("Result: "), pu64(*q), pnl(); }
| pointless_function: u64 { i: u64 = 0 as
| u64; while ++i < 1000000:u64 {
| x: vptr = malloc(1024:usize); free(x);
| if i == 300000:u64 { [label_a]
| } elif i == 600000:u64 { [label_b]
| } elif i == 900000:u64 { [label_c]
| } } return i; }
| knome wrote:
| Ha. Amazing! Someone actually implemented a variant of COME
| FROM of C-INTERCAL infamy in another language.
|
| And moreover made it a reasonable and readable variant!
| samsquire wrote:
| I really like this.
|
| Thank you for your comment and sharing.
|
| I have a lightweight 1:M:N runtime (1 scheduler thread, M
| kernel threads, N lightweight threads) which preempts by
| setting hot loops to the limit.
|
| https://github.com/samsquire/preemptible-thread (Rust, Java
| and C)
|
| How do you preempt code that is running?
|
| Would you like to talk more about your idea?
| bluetomcat wrote:
| > How do you preempt code that is running?
|
| It runs in its own VM with custom instructions. The "wait"
| statement corresponds to a special instruction that is able
| to switch to a separate execution context and restore the
| parent context when necessary.
|
| To implement this in native code, some kind of runtime
| support would be needed. Perhaps a dedicated thread that is
| able to stop/resume the main thread, changing the contents
| of the stack and the registers.
| synergy20 wrote:
| or just use dds as ros2 does, or use something like zeromq?
| xigency wrote:
| I'd recommend exploring some with Scheme. Somewhere between
| writing code in continuation-passing style, using macros, and
| using call-with-current-continuation it should be able to build
| any of these mechanisms in a clean way. Maybe there's a
| prototype for a better construct there. Then it's on other
| language developers to support these capabilities as well.
|
| Because looking at all the examples listed in the article, none
| of them seem like very idioms.
| samsquire wrote:
| Thanks for your reply.
|
| I have looked into shift and reset to the point I think I
| understand it (and callCC but it actually changes the
| execution context or can be seen as an AST transformation)
|
| (This wiki page helped me understand delimited continuations:
| https://wiki.haskell.org/Library/CC-delcont )
|
| I am interested in algebraic effects too, but would like to
| understand them from a assembly point of view and such as
| exceptions.
|
| My Lisp-like experience is only with Clojure, and it is
| delightful applying methods so trivially. I just find other
| people's LISP hard to read!
| jcarrano wrote:
| 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)
| vlovich123 wrote:
| > For example, the KJ library's doc says
|
| Just to be clear, the audience for that tour is primarily
| Cloudflare engineers working on workerd / Workers runtime.
| Within that context, that's very much correct. The entire
| codebase is written using asynchronous I/O - synchronous I/O
| doesn't show up (or if it does, it's in weird parts I've never
| looked). Fibers are used sparingly in very specific contexts
| and we have very special code to make it memory efficient at
| our scale.
| DSingularity wrote:
| I interpreted that as "if you can go stackless prefer it for
| the coroutines-for-elegance use case".
| z3t4 wrote:
| I want to add to the callback vs coroutines and async/serial
| discussion that it all depends on how you treat errors. Are
| errors mere exceptions or do you want to handle errors in the
| control flow ?
|
| For example turndeg(90), Move(10), PickupItem(), turndeg(180),
| Move(10) you treat errors as exceptions, if the robot fail to
| pickup the item, or if it ends up at the wrong place it's an
| exception.
|
| Now if you put all these in try/catch, have the functions
| return error code (or 0 for success), or use callbacks, the
| code will be more "ugly" yes, but you then treat errors as
| "first class citizens", if the robot for example fail to pickup
| the item you want to try something else, maybe apply more vacum
| to the suction arm, or switch to a grip arm. And if the robot
| goes off course you want to make a course direction.
|
| async/await, coroutines, futures, promises, do make the code
| "look nice", but that nice look comes from treating errors as
| exceptions.
| taeric wrote:
| Ish. You can also have error recovery in the form of the
| "conditions/restarts" system of common lisp. Such that you
| can have "look nice" things if you want with many different
| setups.
| spc476 wrote:
| I have a gopher server written in Lua [1] that uses
| coroutines to handle each connection. This bit of code is
| executed as the main menu page [2] is being displayed (with
| comments) -- ------------------- --
| Load in some modules. -- The first makes a gopher
| menu item of type link -- The second allows us to do
| a TCP connection -- --------------------------
| local mklink = require "port70.mklink" local tcp =
| require "org.conman.nfl.tcp" --
| ------------------------------ -- tcp.connect()
| connects to the given address and port and timeout --
| (in seconds). This function will create a socket, set it
| -- to non-blocking, call connect, then yield. When the
| socket has -- connected, the coroutine is then
| resumed with the connection; if -- 1 second has
| passed before the connection is done, then nil is --
| returned when resumed. This just calls a local QOTD service.
| -- --------------------- local ios =
| tcp.connect("127.0.0.1",'qotd',1) if ios
| then local res = "" --
| -------------------------------- -- The following
| loop will yield the coroutine until a line -- of
| data has been accumulated from the network. The coroutine
| -- is then resumed with the line of text. --
| --------------------------------------- for line in
| ios:lines() do -- ----------- --
| We're just accumulating the text into one long blob of
| -- text that we'll return -- -----------------
| res = res .. mklink { type = 'info' , display = line }
| end ios:close() -- another yield point, when
| closed, resume return res else
| return mklink { type = 'info' , display = "Not Available" }
| end
|
| No exceptions here. If we can't connect, it's a simple 'if'
| test and do something else. The functions `tcp.connect()`,
| `ios:lines()` and `ios:close()` are all blocking points that
| cause the coroutine to yield. Yes, if I put something like
| `while true do end` that will block the entire process as
| there is no preemption, but aside from that detail, I find
| this code easy to read.
|
| [1] https://github.com/spc476/port70/blob/master/share/index.
| por...
|
| [2] gopher://gopher.conman.org/
| jcarrano wrote:
| I don't agree. You can handle errors how you like on both
| approaches. The callback code, however, will get pretty messy
| once you have a branched control flow.
|
| With coroutines you are using the language's control
| structures directly. The program counter is your state and a
| branch is an `if`. With minimal debugger support you will be
| able to see at which line is the execution on each coro.
|
| With callbacks you would have to inspect which ones are
| pending, unless you have made the state explicit in a
| variable.
|
| These is not a huge deal for those who have been programming
| for a while but for beginners coros will feel more like an
| extension of the language than something built on top.
| CyberDildonics wrote:
| _Yes._
|
| What question are you answering 'yes' to?
|
| _The callback is not a natural construct_
|
| According to who? It is very natural to anyone making a GUI or
| anything interactive for the firs time. Eventually I would try
| to move people to queueing up events and handling them all at
| the same time so that the order is easier to debug.
|
| Coroutines are the latest silver bullet syndrome. Fundamentally
| you still need to synchronize and order data and that's the
| hard part. I don't know why students would need to do more than
| the classic interactive loop of:
|
| 1. get data
|
| 2. update state
|
| 3. interactive output (drawing a frame, moving a robot etc.)
| ilyt wrote:
| > According to who? It is very natural to anyone making a GUI
| or anything interactive for the firs time.
|
| The people that do not make GUIs but apps with complex
| behaviours.
|
| Callbacks are nice and simple. Callbacks calling callbacks
| calling callbacks calling callbacks (because of one of worst
| ideas in programming ever, function coloring) stops being
| simple. Async/await is just a patch over that ugliness for
| languages that can't do any better easily
| CyberDildonics wrote:
| _The people that do not make GUIs but apps with complex
| behaviours._
|
| I don't think there is a difference here.
|
| _Callbacks are nice and simple. Callbacks calling
| callbacks calling callbacks calling callbacks (because of
| one of worst ideas in programming ever, function coloring)
| stops being simple. Async /await is just a patch over that
| ugliness for languages that can't do any better easily_
|
| I agree with all of this, but they said 'callbacks are not
| natural'. I don't think this is a language issue either and
| I don't think coroutines help. Just like I said in the
| first comment, I think the way to go is to have a queue of
| events/inputs and use that because the ordering and
| debugging is much better and you don't get the same web of
| jumping to different parts of the execution.
|
| I do think that callbacks are a 'natural' and
| straightforward idea that most people either think of
| independently or understand well the first time they see
| it. It doesn't mean that's the best way to do it, but to
| say it isn't 'natural' is bizarre.
| ilyt wrote:
| > I think the way to go is to have a queue of
| events/inputs and use that because the ordering and
| debugging is much better and you don't get the same web
| of jumping to different parts of the execution.
|
| If you just have stream of events that need to be decided
| upon in order you're in a very happy place; complexity
| strikes when you want to run handlers for them in
| parallel to cut on latency, and those handles also need
| to do multiple things that can be done asynchronously to
| cut on latency.
|
| Message passing works well here, you can just send a
| bunch of requests to various components then just wait
| for each at the moment they are needed; async/await is
| essentially a very bastardised version of it (and usually
| stuck in single thread in most implementations so no
| parallel computing)
| CyberDildonics wrote:
| I'm not sure what point you are making now. The original
| story is about coroutines somehow making robotic
| programming easier and the comment I replied to was
| saying 'callbacks are not natural'.
| ilyt wrote:
| >I'm annoyed when coroutines are reserved for use only in high
| performance,c10k-type of situations
|
| Go's pretty much that idea. Make the threads light as
| coroutines (I think it's like 4k or 8k per goroutine) and give
| some basic messaging (channels) to go with it. Works well but
| some of the simplicity ended up biting it in the arse so there
| can be quite a bit of boilerplate in some cases.
| foxbyte wrote:
| 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.
| nstbayless wrote:
| "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...
| bvisness wrote:
| In practice we actually do; I had to simplify for the article.
| We have a few utilities like a `runUntilDone` that make simple
| sequences easier to write. Example:
| https://github.com/frc-2175/2023RobotCode/blob/main/src/lua/...
|
| I suppose we could make more utilities for running coroutines
| "in parallel", but I haven't really felt the need. At that
| point we usually have to worry about exit conditions and it
| feels natural to just write a loop.
| Jtsummers wrote:
| For Python generators, you can yield from called functions by
| using _yield from_ as in this (quick, not stellar) example:
| def first(): yield 1 yield from second()
| yield 4 def second(): yield 2
| yield 3 print(list(first())) # collects all the
| results and prints them # Output: [1, 2, 3, 4]
|
| But yeah, it doesn't work on a direct function call you have to
| know it's going to return a generator (or an iterable, like if
| it returns a list): def something():
| yield from something_else() def something_else():
| return [1,2,3,4]
| [deleted]
| pcblues wrote:
| 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!"
| sesuximo wrote:
| Arguably this devalues the programming they are learning since
| Coroutines aren't as generally applicable
| rtpg wrote:
| In a world where most programming languages have async/await
| style abstractions this stuff is pretty applicable IMO
___________________________________________________________________
(page generated 2023-06-22 23:02 UTC)