-
Notifications
You must be signed in to change notification settings - Fork 18
True Modularity
Depending on who you are, the question of why you might want to use roshask can be answered in several ways. Some people may just want to use Haskell to interoperate with services mediated by ROS because they like Haskell, or want to make use of some library already written in Haskell. But the primary benefit of using roshask is what it provides for software design.
Most software frameworks providing architectural support similar to what ROS provides -- discrete processes or nodes loosely coupled by message-based communication channels -- make the production of messages very opaque. One might write something like this in Python (from a ROS tutorial):
while not rospy.is_shutdown():
str = "hello world %s"%rospy.get_time()
rospy.loginfo(str)
pub.publish(String(str))
rospy.sleep(1.0)
The problem with this block of code is that it is hard to give it a descriptive type. The issue is that the publishing action effectively has a void
return type, which makes it extremely difficult to pass around what might otherwise be seen as generators.
Compare that with the corresponding roshask definition (from Examples/PubSub/src/Talker.hs
),
sayHello :: Topic IO S.String
sayHello = repeatM (fmap mkMsg getCurrentTime)
where mkMsg = S.String . ("Hello world " ++) . show
Here, sayHello
is a first-class value that we can pass to other functions. Not only that, but its type is now descriptive enough that the type checker can ensure that our uses of sayHello
are type safe.
The main benefit of being able to pass Topics around as first-class values is that we can now work with operations defined on those values. If we have two Topics whose values should be paired together, we can simply write everyNew topic1 topic2
to get a new pair of most recent values every time either Topic produces a new value. If we instead had to register a callback function when subscribing to a Topic, we would have to manage the requisite concurrency ourselves.
The way a Node interfaces with the rest of the world may be thought of as its plumbing: there are input pipes, there are output pipes, and they each flow in or out of particular sections of code. This is primarily a declarative aspect of ROS component design that is defined almost entirely in terms of advertise
and subscribe
parts. By separating this plumbing declaration from any notion of an executable program, these interconnection specifications may be combined with other specifications to build up a larger specification.
Consider the roshask example package Examples/NodeCompose
. This package nominally defines two nodes: the telescope
Node that produces images, and the detectUFO
Node that consumes images. These nodes can each be compiled into their own executables as in NodeCompose/src/JustScope.hs
,
module Main (main) where
import Ros.Node
import Telescope
main = runNode "Scope" telescope
and, from NodeCompose/src/JustDetect.hs
,
module Main (main) where
import Ros.Node
import DetectUFO
main = runNode "Detect" detectUFO
Running the two Node values, telescope
and detectUFO
, as separate executables means that data is passed from telescope
to detectUFO
via the "video"
Topic in the standard ROS way. But sometimes the expense of this modularity due to the copying of large values like Images tempts us to break down the barriers and stuff telescope
and detectUFO
into the same Node. This is a horrible temptation to give in to! By keeping the two concerns separate, we are buying ourselves the flexibility to swap out the telescope
back-end without touching detectUFO
, or move one Node onto a different machine if that would offer some benefit.
So must abstraction cost us at runtime? No! Since we have isolated the declarative aspects of our Node definitions as (from Telescope.hs
),
telescope :: Node ()
telescope = advertise "video" $ (topicRate 60 (runTopicState' images 0))
and (from DetectUFO.hs
)
detectUFO :: Node ()
detectUFO = subscribe "video" >>= runHandler findPt >> return ()
we can simply compose the two Nodes when defining the executable process we want to run (from Main.hs
),
module Main (main) where
import Ros.Node
import Telescope
import DetectUFO
main = runNode "NodeCompose" $ telescope >> detectUFO
This composition has the benefit that message passing between telescope
and detectUFO
is simply pointer copying, rather than data copying. In this way, we have preserved the modularity of separate Nodes with concise, focused definitions that can be run as separate processes on different machines, but we have also left the door open for super-efficient single-process compositions of Nodes.
The key point here is that the same abstractions of Nodes and Topics are used in all cases!