Robotics, though a multi-disciplinary area of engineering and science, shares a unique symbiotic relationship with computer science. As robots are increasingly put to solve more complex tasks, the more important it becomes to look carefully into the software that runs inside them, and in particular to the programming languages that bring them to life. At the same time, programming a robot can grant computer science an often missing practical appeal, with a pedagogical potential long recognized by computer science educators [Blank2006].
Various studies corroborate that robotics can indeed mitigate the abstract nature of programming [oddie2010introductory] and provide a more creative environment for teaching programming concepts focused on hands-on problem solving [druin2000robots] and for coding rich behavior using basic code structures [gandy2010use], while still requiring reasoning about relevant computational concepts such as software modularity and communication [LawheadDBGSBH:03].
Yet, the richness of robot programming is synonym of its real-world complexity, which renders standard robotic frameworks unfeasible for use in a pedagogical setting and has been motivating the proposal of various educational robot programming frameworks. As the synergy between robotics and programming deepens, education is the perfect place to experiment novel approaches that can shape the robot programming languages of the future [cleary2015reactive]. It also carries a timely opportunity to transmit good design practices to new generations of programmers, that are receptive to novel languages as long as these allow to quickly build applications [felleisen2004teachscheme].
As a step towards realising this vision, we advocate that languages for teaching robotics to novice programmers shall:
be compatible with standard robotic practices and seamlessly connect to existing robotic infrastructures;
adopt a simple declarative programming style and provide a pure cause-and-effect interface emphasizing the essence of what it means to program a reactive system;
rely on a general-purpose programming language with good tool support, so that advanced programming features can be gradually introduced and acquired programming skills can naturally transfer to other domains.
In Section II, we argue that state-of-the-art languages fail, to some extent, to exhibit these characteristics. This justifies the proposal of rosy, a simple yet powerful reactive programming language, presented in Section III. To ease adoption by novice programmers, rosy is supported by a browser-based development environment, described in Section IV. As an embedded domain-specific language, rosy supports the full power of higher-order functional programming offered by the host Haskell language, and is connected to ROS, one of the most popular robotic middlewares. The mechanics behind its implementation is presented in Section V. Section VI concludes the paper and leaves directions for future work.
Ii The pedagogy of robot programming languages
Programming robots is a particularly complex task, where one has to deal not only with the continuous and real-time aspects of the physical world, but also with heterogeneous architectures and complex communication paradigms. To minimize frustration of novice programmers, several pedagogical languages and approaches have been proposed over the years. This section reviews and discusses such related work.
Ii-a The Robot Operating System
Robotic middlewares have been developed to ease the programming of robots, abstracting hardware and communication details and promoting modularity. The Robot Operating System (ROS) [QuigleyCGFFLWN:09] is one such middleware, possibly the most popular, and defines an architecture through which components, called nodes in ROS, can communicate with each other by publishing to or subscribing from data sources, called topics. Other than that, individual nodes are programmed in general-purpose languages, typically C++ or Python. The popularity of ROS
is fueled by a very dynamic community and an open-source policy that encourages code re-use, and a large package database ranging from educational to industrial applications.
As a pedagogical example, consider the well-known TurtleBot2 robot, whose controller is programmed in ROS-powered C++. A main node controls its Kobuki mobile base publishing odometry information and data collected from a set of sensors (collision, cliff and wheel drop sensors, plus a few buttons), and subscribing to commands that control its (linear and angular) velocity and the color of a set of LEDs. ROS allows the definition of custom message types, from which source code is automatically generated, and Kobuki defines such message types for the relevant events. Below is a minimal example of a ROS application that subscribes to bumper events and plays an error sound when a bumper is pressed:
Snippet 1 (Play a sound on collision).
[escapeinside=@@]cpp @ros::Publisher@ pub;
void cb (const @kobuki_msgs::BumperEventConstPtr@& b) if (b-¿@state@==@kobuki_msgs::BumperEvent::PRESSED@) @kobuki_msgs::Sound@ s; s.@value@ = @kobuki_msgs::Sound::ERROR@; pub.@publish@(s);
int main(int argc, char** argv) @ros::init@(argc, argv, ”play”); @ros::NodeHandle@ nh; @ros::Subscriber@ sub = nh.@subscribe@( ”/mobile_base/events/bumper”, 10, cb); pub = nh.@advertise@¡@kobuki_msgs::Sound@¿( ”/mobile_base/commands/sound”, 10); @ros::spin@(); return 0;
Even this minimal snippet is clearly non-trivial for novice programmers, obfuscating the truly reactive nature of the controller. Besides having to get around advanced linguistic features such as pointers, namespaces or templates, to manipulate topics the programmer must first explicitly [escapeinside=@@]cpp@subscribe@ to bumper events and [escapeinside=@@]cpp@advertise@ that sound commands will be published, considering the buffer size for incoming and outgoing messages. The programmer must also reason about how topics are processed, by registering a callback on the [escapeinside=@@]cpp@ros::Subscriber@ that will [escapeinside=@@]cpp@publish@ an error sound command if a bumper pressed event is read, and when topics are processed, in this simplest case using the ROS [escapeinside=@@]cpp@spin@ primitive that periodically processes callbacks for the queued messages.
Ii-B Visual robot programming languages
To tame the complexity of programming robots in fully-fledged general-purpose programming languages, a myriad of frameworks aimed at novice robot programming using visual programming languages have been proposed [DiproseMH2011]. Many of these follow a block-based approach [CasanCMAM2015, angulo2017roboblock, masum2018framework, WeintropASFLSF:18, marghitu2015robotics], where predefined blocks with varied colors and edges can be put together like pieces of a puzzle to define a robotic controller.
Aside from discussions on whether block-based approaches constitute “real” programming or their programming experience transfers to “real” textual languages [weintrop2019block, xu2019block], and with due exceptions such as [marghitu2015robotics], the vast majority of block-based robot programming approaches adopt an imperative mindset: primitive blocks execute individual predefined tasks, such as moving forward during a certain period of time or for a certain distance; and combining blocks amounts to performing sequences of tasks. For example, the TurtleBot3 can be programmed via a block-based interface in which we can write a block similar to the one from Figure 1. Albeit simple, this example block hides away the reactive nature of the robotic system – all the sensor and command behavior is encapsulated within the black-box primitive blocks.
Ii-C Functional reactive programming languages
Reactive programming [bainomugisha2013survey] is a programming paradigm organised around information producers and consumers, that can naturally bring out the intrinsically reactive nature of cyber-physical systems. A particularly active line of research, known as functional reactive programming (FRP) [perez2018functional], focuses on streams of information as the central reactive abstraction, and advocates a declarative approach to manipulate streams at a high-level of abstraction supported by the pure equational reasoning of functional languages such as Haskell.
Functional languages, as a form of algebra, are a good fit for introductory programming [felleisen2004teachscheme], and many pedagogical FRP approaches have been proposed for programming interactive games and animations [felleisen2009functional, codeworld, almeida2018teaching, cleary2015reactive]. Much classical work has also proposed FRP for programming robots [peterson1999lambda, hudak2002arrows, pembeci2002functional].
In FRP, time is typically explicit and conceptually continuous; the execution of the system is then carried out by sampling all streams synchronously at the rate of an external and global clock. The roshask [roshask] Haskell ROS library promotes the modularity and expressiveness of FRP, while remaining faithful to the asynchronous nature of ROS, by adopting a more pragmatic approach centered on manipulating asynchronous topics in their entirety.
Ii-D Towards a pedagogical robot FRP language
Despite the declarative nature of functional programming and the focus on events of reactive programming, the combinatorial FRP style is not beginner friendly, as it tends to swallow entire programs and resorts to advanced higher-order features to separate reactive code (referring to entire topics) from non-reactive code (referring to individual events).
In this paper, we advocate that a toned-down FRP language, focused on individual events, represents a sweet spot of introductory robot programming. Making time implicit is a big win, as it liberates novice programmers from specific FRP syntax and invites them to simply write pure functions. To retain some of the expressiveness of FRP, these functions have then an intuitive interpretation as operations on data streams.
Iii The rosy language
The rosy language presents itself as a natural dialect for bringing robots alive using nothing more than plain mathematics, while promoting good software design practices. In this section, we make a case for how its declarative nature allows creating robotic controllers in an intuitive and painless way, and informally present its defining features through a collection of examples of increasing complexity that illustrate how to control a TurtleBot2 robot. The types and associated fields used in the examples will therefore spell the standard Kobuki ROS message types. The rosy website111https://haskell.lsd.di.uminho.pt/rosy offers a modern integrated development environment, including an editor, help guides, and executable versions of all the examples shown in this section and more.
For readers not familiar with Haskell syntax, all the functions and operators not defined in this paper are standard and their definition can be found at https://hoogle.haskell.org. The documentation for respectively colored rosy-specific functions and types is available at the rosy website.
Iii-a A rosy primer
In rosy, we can control a robot by writing pure functions that receive sensor information from the robot and react by sending commands back to the robot. To make a robot move forward at a constant velocity of 0.5 m/s, we can simply write:
Example 1 (Move forward).
[escapeinside=@@]haskell move :: @Velocity@ move = @Velocity@ 0.5 0
main = @simulate@ move
Thehaskell move function models our controller (where the [escapeinside=@@]haskell@Velocity@ of the robot is separated into its linear and angular components), and thehaskell main function is rosy-specific syntax to[escapeinside=@@]haskell @simulate@ our controller (that will be elided from now on). Still, the astute reader may fittingly ask “for how long are we telling the robot to move forward?” Being rosy a reactive programming language, the answer is not “once” or “for a certain period of time”, but actually “forever”. The intuition is that a controller is a function that unceasingly listens for inputs, and for each received input produces an output, what grants rosy programs an implicit notion of time. In this case, sincehaskell move receives no inputs, it will produce [escapeinside=@@]haskell@Velocity@ outputs at a fixed rate.
To make things a bit more interesting, imagine that we want the robot to accelerate forward with non-constant velocity. We can achieve this behavior by making sure to increase the robot’s velocity at each point in time:
Example 2 (Accelerate forward).
[escapeinside=@@]haskell accelerate :: @Velocity@ -¿ @Velocity@ accelerate (@Velocity@ vl va) = @Velocity@ (vl+0.5) va
Note that the same [escapeinside=@@]haskell@Velocity@ type has different input and output meanings. This secondhaskell accelerate controller is a function that repeatedly asks the robot for its current linear velocity, and commands the robot to increase it by 0.5 m/s.
As our robot is moving forward, what if it hits a wall? We can naturally express multiple rosy controllers that react to distinct events. For instance, we can make the robot play an error sound when one of its bumpers is pressed, what will happen on contact with a wall:
Example 3 (Accelerate and play a sound on collision).
[escapeinside=@@]haskell play :: @Bumper@ -¿ Maybe @Sound@ play (@Bumper@ _@Pressed@) = Just @ErrorSound@ play (@Bumper@ _@Released@) = Nothing
accelerateAndPlay = (accelerate,play)
In this example, we define a compositehaskell accelerateAndPlay controller by simply pairing togetherhaskell accelerate andhaskell play. Note that these two functions are not required to execute at the same time:haskell accelerate runs on periodic robot information, thoughhaskell play waits for [escapeinside=@@]haskell@Bumper@ events. The two controllers will execute in parallel, effectively combining both behaviors. To be able to play a sound only when a bumper is [escapeinside=@@]haskell@Pressed@, and not [escapeinside=@@]haskell@Released@, thehaskell play function may or may not produce a [escapeinside=@@]haskell@Sound@ command. Thehaskell play rosy controller displays the same behavior as the ROS one encoded in Snippet 1, but here its reactive nature is clear from the type declaration.
Even though the controller from Example 3 detects when the robot hits a wall, it will continue to push against the wall, likely reaching a deadlock. With controllers as pure functions that react to events as they happen, there is no easy way to make a controller remember some event in the past. This contrasts with traditional imperative robot programming languages, where we could use a global variable to, e.g., memorize when the robot has hit a wall and, from then on, change its behavior. Like other pedagogical functional languages [felleisen2009functional], we grant rosy controllers a notion of [escapeinside=@@]haskell@Memory@, that can be used to, e.g., remember the robot’s moving direction:
Example 4 (Accelerate forward and backwards on collision).
[escapeinside=@@]haskell type Hit = Bool
reverseDir :: @Bumper@ -¿ @Memory@ Hit reverseDir _= @Memory@ True
accelerate :: @Memory@ Hit -¿ @Velocity@ -¿ @Velocity@ accelerate (@Memory@ hit) (@Velocity@ vl va) = if hit then @Velocity@ (vl-0.5) va else @Velocity@ (vl+0.5) va
forwardBackward = (reverseDir,accelerate)
In the above example, the controller hands over its memory tohaskell accelerate, that uses it to determine in which direction to move. To reconcile memory with pure functional programming, controllers that wish to change the memory are expected to return it explicitly as an output. The haskellHit boolean memory will be false by default, and changed to true byhaskell reverseDir when a bumper event occurs.
Iii-B Revisiting Ros controllers
For a more complete and realistic example, we now encode the popular Kobuki random walker controller in rosy, while trying to stay faithful to the C++ ROS implementation222https://github.com/yujinrobot/kobuki/tree/devel/kobuki_random_walker:
On a bumper or cliff event, the robot blinks one of its LEDs orange and decides to change its direction;
On a wheel drop event, the robot blinks both LEDs red and decides to stop moving while the wheel is in the air;
When changing direction, it randomly decides on an angle between and
and on a left/right direction. Depending on a fixed angular velocity, it estimates how many seconds it shall turn;
A sequential loop routinely performs the adequate action depending on the state of the controller. It commands the robot to either: go forward, stop moving, or turn in a given direction for a number of seconds.
Example 5 (Random walker).
[escapeinside=;;]haskell data Mode = Go — Stop — Turn Double ;Seconds; data ChgDir = ChgDir – change direction
vel_lin = 0.5 vel_ang = 0.1
bumper :: ;Bumper; -¿ (;Led1;,Maybe ChgDir) bumper (;Bumper; _st) = case st of ;Pressed; -¿ (;Led1; ;Orange;,Just ChgDir) ;Released; -¿ (;Led1; ;Black;,Nothing)
cliff :: ;Cliff; -¿ (;Led2;,Maybe ChgDir) cliff (;Cliff; _st) = case st of ;Hole; -¿ (;Led2 Orange;,Just ChgDir) ;Floor; -¿ (;Led2 Black;,Nothing)
wheel :: ;Wheel; -¿ (;Led1;,;Led2;,;Memory; Mode) wheel (;Wheel; _st) = case st of ;Air; -¿ (;Led1; ;Red;,;Led2; ;Red;,;Memory; Stop) ;Ground; -¿ (;Led1; ;Black;,;Led2; ;Black;,;Memory; Go)
chgdir :: ChgDir -¿ StdGen -¿ ;Seconds; -¿ ;Memory; Mode chgdir _r now = ;Memory; (Turn dir time) where (b,r’) = random r (ang,_) = randomR (0,pi) r’ dir = if b then 1 else -1 time = now + ;doubleToSeconds; (ang/vel_ang)
spin :: ;Memory; Mode -¿ ;Seconds; -¿ (;Velocity;,;Memory; Mode) spin m@(;Memory; Stop) _= (Velocity 0 0,m) spin m@(;Memory; (Turn dir t)) now — t ¿ now = (;Velocity; 0 (dir*vel_ang),m) spin m _= (Velocity vel_lin 0,;Memory; Go)
randomWalk = (bumper,cliff,wheel,chgdir,spin)
Thehaskell randomWalk controller encodes each part of the above specification as a separate function, sharing a memory haskellMode that encodes the different robot states. The ROS-style computation graph is illustrated in Fig. 2, where nodes amount to the defined reactive functions, and topics are either user-defined events or those provided for the Kobuki controller. Thehaskell bumper andhaskell cliff functions change the LED colors, and set in motion a change in direction. For greater modularity, they both emit a new event of type haskellChgDir. Thehaskell wheel function also changes the LEDs’ colors and sets the memory mode to the haskellStop state. Thehaskell spin function reads the memory mode, sets the respective velocity depending on the mode, and returns an updated mode. It receives the current time in [escapeinside=@@]haskell@Seconds@ to determine if the estimated turning time has elapsed.
The most complicated behavior is left to functionhaskell chgdir, that resolves haskellChgDir events to concrete haskellTurn actions. To implement random behavior, it resorts to a Haskell standard randomness generator of type haskellStdGen to generate a random direction and angle, and reads the current time to calculate the time limit for the haskellTurn action. This is a good example of how the power of the full Haskell language can be gradually unleashed as students tackle more advanced problems.
Another popular Kobuki controller is the safety controller333https://github.com/yujinrobot/kobuki/tree/devel/kobuki_safety_controller, that imposes stricter conditions on dangerous events. Its C++ ROS implementation can be encoded in rosy as a single controller that reacts to bumper, cliff or wheel drop events and cautiously decides on a new velocity to escape danger:
Example 6 (Safety controller).
[escapeinside=@@]haskell safetyControl :: Either (Either @Bumper@ @Cliff@) @Wheel@ -¿ Maybe @Velocity@ safetyControl = …
The code for haskell safetyControl is conceptually simple, yet verbose as it explores multiple combinations of sensor inputs. We omit it in the paper, but it can be found at the rosy website.
The safety controller does not do much by itself, and it is typically deployed together with the random walker to limit its actions as the robot roams around. Since both controllers publish possibly conflicting [escapeinside=@@]haskell@Velocity@ commands to the robot, the traditional ROS solution is to use a multiplexer that remaps topics and allows one controller at a time to command the robot, according to a fixed set of priorities.
We can define a general multiplexer in rosy as follows:
Example 7 (Binary multiplexer, haskellM1 with priority over haskellM2).
[escapeinside=@@]haskell data M = Start — Ignore @Seconds@ data M1 a = M1 a data M2 a = M2 a
mux :: @Seconds@ -¿ @Memory@ M -¿ Either (M1 a) (M2 a) -¿ Maybe (a,@Memory@ M) mux t _(Left (M1 a)) = Just (a,@Memory@ (Ignore(t+d))) mux t (@Memory@ (Ignore s)) _— s ¿ t = Nothing mux t _(Right (M2 a)) = Just (a,@Memory@ Start)
This binary multiplexer reads the current timehaskell t in [escapeinside=@@]haskell@Seconds@ and reacts to events either marked as haskellM1 or haskellM2, giving higher priority to haskellM1 events by setting a time interval starting athaskell t and ending athaskell t+d (for a fixed durationhaskell d) during which all haskellM2 events are ignored.
We can then instantiate a safe random walker by remapping output velocities of the safety and random walker controllers with haskellM1 and haskellM2 tags and running a multiplexer in parallel:
Example 8 (Random walker with safety controller).
[escapeinside=@@]haskell safetyControl :: … -¿ Maybe (M1 @Velocity@) spin :: … -¿ (M2 @Velocity@,…) muxVel :: … -¿ Either (M1 @Velocity@) (M2 @Velocity@) -¿ Maybe (@Velocity@,…)
safeRandomWalk = (randomWalk,safetyControl,muxVel)
The complete refactored code for thehaskell safeRandomWalk controller is available at the rosy website.
Iii-C Revisiting block-based languages
Because controllers in rosy run forever, they do not directly lend themselves to performing sequences of instructions. For concreteness, imagine that we want to command the robot to draw a square on the floor with its movement. In a visual robot programming language, this can be done by assembling a block like the one from Fig. 1. Even though such a block can be expressed in rosy as a multi-stage controller that explicitly encodes a state machine and reacts differently depending on the state444Classical FRP frameworks tackle this general problem by designing advanced higher-order switching combinators over reactive functions, that are expressively powerful but even less novice-friendly., it is useful to lend more structure to the language as the complexity of the controller grows.
Like other FRP languages [peterson1999lambda], rosy introduces the concept of tasks, as a combination of an initialization step and a continuous controller. The initializer sets up the stage for the controller, that runs continuously until a predefined terminating event occurs. For example, we can make the robot turn sideways by a fixed amount of degrees by writing a task:
Example 9 (Task: Turn left or right).
[escapeinside=@@]haskell type Side = Either @Degrees@ @Degrees@
turn :: Side -¿ @Task@ () turn s = @task@ (startTurn s) runTurn
startTurn :: Side -¿ @Orientation@ -¿ @Memory@ @Orientation@ startTurn s o = case s of Left a -¿ @Memory@ (o+@degreesToOrientation@ a) Right a -¿ @Memory@ (o+@degreesToOrientation@ a)
runTurn :: @Memory@ @Orientation@ -¿ @Orientation@ -¿ Either (@Velocity@) (@Done@ ()) runTurn (@Memory@ to) from = if abs d ¡= err then Right (@Done@ ()) else Left (@Velocity@ 0 (@orientation@ d)) where d = @normOrientation@ (to-from)
At initialization, thehaskell startTurn reads the robot’s [escapeinside=@@]haskell@Orientation@ from its odometry information, and writes the desired final [escapeinside=@@]haskell@Orientation@ to memory by adding or subtracting the received angle to the current orientation. Thehaskell runTurn controller will rotate the robot towards the desired orientation until the desired and current orientations are equal with a small error marginhaskell err, signalling when it is [escapeinside=@@]haskell@Done@. The [escapeinside=@@]haskell@Done@ type allow returning additional information on task termination. Returning nothing is achieved with the empty type haskell().
A similar task makes the robot move a fixed distance:
Example 10 (Task: Move forward or backwards).
[escapeinside=@@]haskell data Direction = Forward @Centimeters@ — Backward @Centimeters@
move :: Direction -¿ @Task@ () move = …
Since tasks can end, in contrast to controllers, we can now mimic the block from Fig. 1 in rosy by using Haskell’s monadic notation to sequence tasks in an imperative style:
Example 11 (Task: Draw a square).
[escapeinside=@@]haskell drawSquare :: @Task@ () drawSquare = replicateM_4
As well as striving to allow students to learn a “real” programming language while freeing them from inessential technical language details outside of how to control a robot, rosy comes with a fully integrated development environment. To really allow students to concentrate on the meaning of their programs, ignoring deployment details, the rosy environment runs as a web application inside any modern web browser.
Figure 3 shows the rosy environment in action. It is powered by Codeworld [codeworld], a modern educational environment for writing graphical Haskell programs such as games and animations. CodeWorld has been used in K12 schools for years, and supports a code editor with features such as syntax highlighting, improved on-the-fly compiler error messages, extensive documentation or easy sharing of projects.
Another vital component of the environment is a visualizer that simulates, directly in the browser, how the controller programmed by the student interacts with a robot in a fictitious 2D world. At the moment, this is tailored for a TurtleBot2 placed in a tiled world made of floor, walls and cliffs. Inrosy training sessions for K12 students that we have hosted at the University of Minho, students could also deploy their same code to control a real robot and perceive the differences between a simulated and a real world. Enabling students to simultaneously test their examples in simulated and real scenarios played an important role in teaching them the importance of these differences and their influence on the design of approximate, event-driven robotic controllers.
V Under the hood
Despite their simplicity, rosy programs are fully compatible with the ROS infrastructure. Under the hood, the rosy language is implemented as an embedded domain-specific language that provides an additional abstraction layer over the existing Haskell ROS library. The rosy environment is also implemented in Haskell, by extending and specially tailoring the CodeWorld [codeworld] environment to the rosy language. This includes a custom prelude that is imported by default, a custom code pre-processor to automatically derive necessary Haskell type class instances, and a custom graphical simulation of the robot. All the development source code is open-source and freely available555https://github.com/hpacheco/codeworld-rosy.
V-a Haskell Ros library
The roshask library [roshask] enables ROS programming within the Haskell ecosystem by supporting the deployment of ROS client nodes that are compatible with the rostcp communications protocol. roshask fades the architectural boundaries between ROS system and software components, by lifting topics to first-class values and designing a collection of FRP-style combinators to split, fuse and generally manipulate topics in a more expressive, modular and compositional way.
At its core, the library provides functions for subscribing and publishing topics: [escapeinside=@@]haskell @subscribe@ :: String -¿ @Node@ (@Topic@ m a) @publish@ :: String -¿ @Topic@ m a -¿ @Node@ () The type variablehaskell m is a monad for actions with effects, typically haskellIO for interacting with a non-pure outside world, andhaskell a is the type of the subscribed or published event derived from standard ROS message type definition files. Topics are modelled as infinite monadic streams [perez2018functional], i.e., monadic actions that produce the next value and a new topic: [escapeinside=@@]haskell newtype @Topic@ m a = @Topic@ (m (a,@Topic@ m a)) The [escapeinside=@@]haskell@Node@ monad manages TCP connections and internal buffers of published and subscribed messages. The following example node subscribes to two sensors, fuses them using the[escapeinside=@@]haskell @bothNew@ combinator (subsampling the faster topic), maps an actionhaskell act :: (Sense1,Sense2) -¿ Cmd over each pair of sensor values, and publishes the resulting commands: [escapeinside=@@]haskell n1 = do t1 ¡- @subscribe@ ”sense1” t2 ¡- @subscribe@ ”sense2” @publish@ ”cmd” t1 ‘@bothNew@‘ t2 Internally, the asynchronous roshask behavior is implemented on top of Haskell user-space threads, that are managed by the Haskell runtime and much more efficient than system threads. For instance, a typical[escapeinside=@@]haskell @publish@ implementation launches a thread that infinitely samples values from a topic and communicates them to the ROS master; similarly, merging two topics is done by launching two threads that independently consume each topic and write to a common channel.
Nodes can also be easily composed, for instance, we can simultaneously install a handler that listens to and prints the published commands to the command line: [escapeinside=@@]haskell n2 = @subscribe@ ”cmd” ¿¿= @runHandler@ putStrLn In the style of[escapeinside=@@]haskell @publish@,[escapeinside=@@]haskell @runHandler@ is a general combinator that launches a thread that will consume values from a topic and execute some user-defined haskellIO action: [escapeinside=@@]haskell @runHandler@ :: (a -¿ IO b) -¿ Topic IO a -¿ @Node@ () The composite nodehaskell n1 ¿¿ n2 will communicate the commands produced byhaskell n1 both via rostcp and locally tohaskell n2.
Alternatively to web-based simulation, rosy programs can also be executed on a local machine (right side of Fig. 4), by connecting to a ROS master server and operate a more realistic Gazebo simulator or a real Kobuki robot. We have tested both scenarios on Ubuntu 14.04 with ROS Indigo and a TurtleBot2.
V-C Implicit stream programming
The greatest design decision of the rosy language is that controllers have an implicit notion of time. This favors a simpler declarative style focused on which commands are issued when events happens, without specifying how often subscribers and publishers interact with the ROS world, and freeing programmers from common robotic programming details such as clocks, sampling rates or synchronization. Therefore, unlike roshask, that exposes a full-fledged API to manipulate topics as a whole, rosy controllers are less expressive in that they only consider a single point in time.
In rosy, events are identified by their type. We define two type classes that internally bind rosy types and ROS message namespaces to streams of subscribed sensors or published commands: [escapeinside=@@]haskell class @Sensor@ a where @sensor@ :: @Node@ (@Topic@ IO a) class @Command@ a where @command@ :: @Topic@ IO a -¿ @Node@ () For example, the same [escapeinside=@@]haskell@Velocity@ type can simultaneously express the act of getting the current velocity from the robot’s periodic odometry data and the act of setting the desired velocity by sending a command to the robot’s base: [escapeinside=@@]haskell instance @Sensor@ @Velocity@ where @sensor@ = @subscribe@ ”odom” ¿¿= return . fmap (@_twist@ . @_twist@)) instance @Command@ @Velocity@ where @command@ = @publish@ ”/mobile_base/commands/velocity” The rosy language currently supports a fixed set of events offered by the kobuki_node API. Extending support for other robots simply requires defining new boilerplate [escapeinside=@@]haskell@Sensor@ and [escapeinside=@@]haskell@Command@ instances, as roshask already supports many standard ROS message types and allows deriving Haskell types from custom ROS message files.
To conciliate whole topics with point-wise controllers, we define another type class that implicitly lifts a function on individual values to a controller on streams of values: [escapeinside=@@]haskell class @Controller@ a where @controller@ :: a -¿ @Node@ () The stream semantics of the lifted controller is then inferred from the function’s type signature: inputs correspond to subscribed sensors, and outputs to published commands, e.g.: [escapeinside=@@]haskell instance (@Sensor@ a,@Command@ b) =¿ @Controller@ (a -¿ b) where @controller@ f = @sensor@ ¿¿= @command@ . fmap f Instances for composite types perform implicit stream programming. For example, a [escapeinside=@@]haskell@Controller@ that receives an input pair is fusing data from two sensors: [escapeinside=@@]haskell instance (@Sensor@ a,@Sensor@ b) =¿ @Sensor@ (a,b) where @sensor@ = liftM2 @bothNew@ @sensor@ @sensor@ As another example, a [escapeinside=@@]haskell@Controller@ that returns possibly different commands is splitting the output stream (using the[escapeinside=@@]haskell @tee@ roshask combinator that duplicates a topic), and processing each type of commands independently: [escapeinside=@@]haskell instance (@Command@ a,@Command@ b) =¿ @Command@ (Either a b) where @command@ t = do (t1,t2) ¡- @tee@ t @command@ rights t2 Multiple [escapeinside=@@]haskell@Controller@s are executed in parallel threads, and can be composed in sequence: [escapeinside=@@]haskell instance (@Controller@ a,@Controller@ b) =¿ @Controller@ (a,b) where @controller@ (a,b) = @controller@ a ¿¿ @controller@ b
V-D Supporting user-defined events and memory
rosy also allows the declaration of user-defined data types for more modular intra-node communication. Since these need not be bound to ROS namespaces, every newly declared user-defined data type is by default a [escapeinside=@@]haskell@Sensor@ and a [escapeinside=@@]haskell@Command@. This is supported by a new [escapeinside=@@]haskell@UserNode@ monad that extends [escapeinside=@@]haskell@Node@ capabilities with local type-indexed event buffers for user-defined data types.
Since rosy programs are by design pure Haskell functions, there is no native support for common robotics design patterns that global memory. It is nonetheless possible to emulate global variables by publishing an initial event, and on every update subscribing to current event and re-publishing its updated value. Even so, this pattern can be error-prone as the programmer needs to be cautious about subscribing and publishing the “variable” the right number of times in order to keep it alive. This may also be problematic if more than one controller is manipulating the same “variable” in parallel.
To avoid these caveats, rosy supports node-specific memory, distinguishable through a type-level tag: [escapeinside=@@]haskell data @Memory@ a = @Memory@ a We implement memory by extending the [escapeinside=@@]haskell@UserNode@ monad with a transactional memory store, holding a global value for every distinct typehaskell a.
In order to support transactional controllers, or more specifically, be able to execute each controller thread as a single transaction, we must generalize our sensor and command interfaces to produce and consume topics of transactions777The haskellSTM monad stands for Haskell’s software transactional memory library. The haskellMaybe type is a technical requirement for filtering values inside a transaction, since instances must not change the periodicity of the topics.: [escapeinside=@@]haskell @sensor@ :: @UserNode@ (@Topic@ IO (STM a)) @command@ :: @Topic@ IO (STM (Maybe a)) -¿ @UserNode@ (@Topic@ IO (STM ())) A [escapeinside=@@]haskell@Sensor@ (@Memory@ a) returns a topic that repeatedly reads from the memory variable of typehaskell a, while a [escapeinside=@@]haskell@Command@ (@Memory@ a) appends memory writes to a topic of transactions, returning a new topic. The greatest change occurs on commands that, unlike before, must be published synchronously within the same transaction, meaning we can no longer fork a topic and publish each side independently. We can execute a controller thread by installing a handler thathaskell atomically executes each transaction as a side effect: [escapeinside=@@]haskell instance (…) =¿ @Controller@ (a -¿ b) where @controller@ f = @sensor@ ¿¿= @command@ . fmap f ¿¿= lift . @runHandler@ atomically
V-E Sequencing tasks
In roshask and other FRP approaches, topics are modelled as infinite streams and topic handlers are program-long threads continuously waiting on and reacting to events. The fact that the data flow graph, inferred from the wiring of stream combinators, is typically known statically, allows roshask to register all subscribers and publishers with the ROS master at node initialization, before starting to actually process data.
In rosy, a task is defined as an initialization action, a continuous controller, and a terminating event: [escapeinside=@@]haskell @task@ :: (@Command@ init,@Controller@ ctrl) =¿ init -¿ ctrl -¿ @Task@ end A controller issues termination via a special event type: [escapeinside=@@]haskell data @Done@ a = @Done@ a We also make [escapeinside=@@]haskell@Task@ a monad, so that programmers can use monadic notation to sequence tasks. For composing two smaller tasks in into a composite task, where the output of the first is passed on to the second, we may write: [escapeinside=@@]haskell task12 = do end1 ¡- @task@ init1 ctrl1 @task@ (init2 end) ctrl2 In this scenario, controllers no longer run forever: when the first task ends, we must uninstall the controllerhaskell ctrl1 and install a new controllerhaskell ctrl2 for the second task. We have extended roshask to support dynamic node configuration: each task runs within its own [escapeinside=@@]haskell@UserNode@; publishers and subscribers are registered at declaration time; tasks keep a fine-grained control of launched threads, and all children threads are killed when exiting the parent [escapeinside=@@]haskell@UserNode@888Note that messages are not lost when transitioning between tasks, since a haskellNode keeps global buffers of published and subscribed ROS topics..
In this paper we have presented rosy, a new pedagogical robot programming language that advocates a sweet-spot between the expressiveness of FRP and the needed simplicity of an educational setting. As part of a computing summer camp for held at the University of Minho999https://www.uminho.pt/veraonocampus, in July 2019, we have taught a 4-hour training session for K12 students on hands-on robot programming in rosy, with only two prior sessions of general programming in Haskell. From our perceptions, students were able to comprehend the concepts and quickly start programming the robot to perform simple tasks.
In the future, we plan to provide further rosy training sessions and undergo empirical studies that can corroborate its practical value for learning programming via robotics. We plan to improve the rosy environment with more advanced simulation scenarios using, e.g., the Gazebo web client101010http://gazebosim.org/gzweb.html or remote ROS support similar to [CasanCMAM2015]. We also plan to explore the design of novel novice-friendly interfaces that blend textual and visual representations, and graphically combine visual blocks typical of imperative robotic approaches with graphical data flow diagrams typical of advanced FRP approaches.