@@ -90,15 +90,15 @@ Note that we haven't deadlocked our own thread by sending to and receiving
90
90
from its mailbox in this fashion. Sending messages is a completely
91
91
asynchronous operation - even if the recipient doesn't exist, no error will be
92
92
raised and evaluating ` send ` will not block the caller, even if the caller is
93
- sending messages to itself!
93
+ sending messages to itself.
94
94
95
- Receiving works the opposite way, blocking the caller until a message
96
- matching the expected type arrives in our (conceptual) mailbox. If multiple
97
- messages of that type are present in the mailbox, they'll be returned in FIFO
98
- order. If not, the caller is blocked until a message arrives that can be
99
- decoded to the correct type .
95
+ Each process also has a * mailbox * associated with it. Messages sent to
96
+ a process are queued in this mailbox. A process can pop a message out of its
97
+ mailbox using ` expect ` or the ` receive* ` family of functions. If no message of
98
+ the expected type is in the mailbox currently, the process will block until
99
+ there is. Messages in the mailbox are ordered by time of arrival .
100
100
101
- Let's spawn two processes on the same node and have them talk to each other.
101
+ Let's spawn two processes on the same node and have them talk to each other:
102
102
103
103
{% highlight haskell %}
104
104
import Control.Concurrent (threadDelay)
@@ -140,83 +140,135 @@ main = do
140
140
Just s -> say $ "got " ++ s ++ " back!"
141
141
{% endhighlight %}
142
142
143
- Note that we've used the ` receive ` class of functions this time around.
144
- These can be used with the [ ` Match ` ] [ 5 ] data type to provide a range of
145
- advanced message processing capabilities. The ` match ` primitive allows you
146
- to construct a "potential message handler" and have it evaluated
147
- against received (or incoming) messages. As with ` expect ` , if the mailbox does
148
- not contain a message that can be matched, the evaluating process will be
149
- blocked until a message arrives which _ can_ be matched.
143
+ Note that we've used ` receiveWait ` this time around to get a message.
144
+ ` receiveWait ` and similarly named functions can be used with the [ ` Match ` ] [ 5 ]
145
+ data type to provide a range of advanced message processing capabilities. The
146
+ ` match ` primitive allows you to construct a "potential message handler" and
147
+ have it evaluated against received (or incoming) messages. Think of a list of
148
+ ` Match ` es as the distributed equivalent of a pattern match. As with ` expect ` ,
149
+ if the mailbox does not contain a message that can be matched, the evaluating
150
+ process will be blocked until a message arrives which _ can_ be matched.
150
151
151
152
In the _ echo server_ above, our first match prints out whatever string it
152
- receives. If the first message in our mailbox is not a ` String ` , then our second
153
- match is evaluated. This , given a tuple ` t :: (ProcessId, String) ` , will send
154
- the ` String ` component back to the sender's ` ProcessId ` . If neither match
155
- succeeds, the echo server blocks until another message arrives and
156
- tries again.
153
+ receives. If the first message in our mailbox is not a ` String ` , then our
154
+ second match is evaluated. Thus , given a tuple ` t :: (ProcessId, String) ` , it
155
+ will send the ` String ` component back to the sender's ` ProcessId ` . If neither
156
+ match succeeds, the echo server blocks until another message arrives and tries
157
+ again.
157
158
158
159
### Serializable Data
159
160
160
- Processes may send any datum whose type implements the ` Serializable ` typeclass,
161
- which is done indirectly by deriving ` Binary ` and ` Typeable ` . Implementations are
162
- provided for most of Cloud Haskell's primitives and various common data types.
161
+ Processes may send any datum whose type implements the ` Serializable `
162
+ typeclass, defined as:
163
163
164
- ### Spawning Remote Processes
164
+ {% highlight haskell %}
165
+ class (Binary a, Typeable) => Serializable a
166
+ instance (Binary a, Typeable a) => Serializable a
167
+ {% endhighlight %}
165
168
166
- In order to spawn processes on a remote node without additional compiler
167
- infrastructure, we make use of "static values": values that are known at
168
- compile time. Closures in functional programming arise when we partially
169
- apply a function. In Cloud Haskell, a closure is a code pointer, together
170
- with requisite runtime data structures representing the value of any free
171
- variables of the function. A remote spawn therefore, takes a closure around
172
- an action running in the ` Process ` monad: ` Closure (Process ()) ` .
169
+ That is, any type that is ` Binary ` and ` Typeable ` is ` Serializable ` . This is
170
+ the case for most of Cloud Haskell's primitive types as well as many standard
171
+ data types.
173
172
174
- In distributed-process if ` f : T1 -> T2 ` then
173
+ ### Spawning Remote Processes
174
+
175
+ We saw above that the behaviour of processes is determined by an action in the
176
+ ` Process ` monad. However, actions in the ` Process ` monad, no more serializable
177
+ than actions in the ` IO ` monad. If we can't serialize actions, then how can we
178
+ spawn processes on remote nodes?
179
+
180
+ The solution is to consider only * static* actions and compositions thereof.
181
+ A static action is always defined using a closed expression (intuitively, an
182
+ expression that could in principle be evaluated at compile-time since it does
183
+ not depend on any runtime arguments). The type of static actions in Cloud
184
+ Haskell is ` Closure (Process a) ` . More generally, a value of type ` Closure b `
185
+ is a value that was constructed explicitly as the composition of symbolic
186
+ pointers and serializable values. Values of type ` Closure b ` are serializable,
187
+ even if values of type ` b ` might not. For instance, while we can't in general
188
+ send actions of type ` Process () ` , we can construct a value of type `Closure
189
+ (Process ())` instead, containing a symbolic name for the action, and send
190
+ that instead. So long as the remote end understands the same meaning for the
191
+ symbolic name, this works just as well. A remote spawn then, takes a static
192
+ action and sends that across the wire to the remote node.
193
+
194
+ Static actions are not easy to construct by hand, but fortunately Cloud
195
+ Haskell provides a little bit of Template Haskell to help. If ` f :: T1 -> T2 `
196
+ then
175
197
176
198
{% highlight haskell %}
177
199
$(mkClosure 'f) :: T1 -> Closure T2
178
200
{% endhighlight %}
179
201
180
- That is, the first argument to the function we pass to ` mkClosure ` will act
181
- as the closure environment for that process. If you want multiple values
182
- in the closure environment, you must "tuple them up".
202
+ You can turn any top-level unary function into a ` Closure ` using ` mkClosure ` .
203
+ For curried functions, you'll need to uncurry them first (i.e. "tuple up" the
204
+ arguments). However, to ensure that the remote side can adequately interpret
205
+ the resulting ` Closure ` , you'll need to add a mapping in a so-called * remote
206
+ table* associating the symbolic name of a function to its value. Processes can
207
+ only be successfully spawned on remote nodes of all these remote nodes have
208
+ the same remote table as the local one.
183
209
184
210
We need to configure our remote table (see the documentation for more details)
185
211
and the easiest way to do this, is to let the library generate the relevant
186
212
code for us. For example:
187
213
188
214
{% highlight haskell %}
189
- sampleTask :: (TimeInterval, String) -> Process String
190
- sampleTask (t, s) = sleep t >> return s
215
+ sampleTask :: (TimeInterval, String) -> Process ()
216
+ sampleTask (t, s) = sleep t >> say s
191
217
192
- $( remotable [ 'sampleTask] )
218
+ remotable [ 'sampleTask]
193
219
{% endhighlight %}
194
220
195
- We can now create a closure environment for ` sampleTask ` like so:
221
+ The last line is a top-level Template Haskell splice. At the call site for
222
+ ` spawn ` , we can construct a ` Closure ` corresponding to an application of
223
+ ` sampleTask ` like so:
196
224
197
225
{% highlight haskell %}
198
226
($(mkClosure 'sampleTask) (seconds 2, "foobar"))
199
227
{% endhighlight %}
200
228
201
- The call to ` remotable ` generates a remote table and a definition
202
- ` __remoteTable :: RemoteTable -> RemoteTable ` in our module for us.
203
- We compose this with other remote tables in order to come up with a
204
- final, merged remote table for use in our program:
229
+ The call to ` remotable ` implicitly generates a remote table by inserting
230
+ a top-level definition ` __remoteTable :: RemoteTable -> RemoteTable ` in our
231
+ module for us. We compose this with other remote tables in order to come up
232
+ with a final, merged remote table for all modules in our program:
205
233
206
234
{% highlight haskell %}
235
+ {-# LANGUAGE TemplateHaskell #-}
236
+
237
+ import Control.Concurrent (threadDelay)
238
+ import Control.Monad (forever)
239
+ import Control.Distributed.Process
240
+ import Control.Distributed.Process.Closure
241
+ import Control.Distributed.Process.Node
242
+ import Network.Transport.TCP (createTransport, defaultTCPParameters)
243
+
244
+ sampleTask :: (Int, String) -> Process ()
245
+ sampleTask (t, s) = liftIO (threadDelay (t * 1000000)) >> say s
246
+
247
+ remotable [ 'sampleTask]
248
+
207
249
myRemoteTable :: RemoteTable
208
250
myRemoteTable = Main.__ remoteTable initRemoteTable
209
251
210
252
main :: IO ()
211
253
main = do
212
- localNode <- newLocalNode transport myRemoteTable
213
- -- etc
254
+ Right transport <- createTransport "127.0.0.1" "10501" defaultTCPParameters
255
+ node <- newLocalNode transport myRemoteTable
256
+ runProcess node $ do
257
+ us <- getSelfNode
258
+ _ <- spawnLocal $ sampleTask (1 :: Int, "locally")
259
+ pid <- spawn us $ $(mkClosure 'sampleTask) (1 :: Int, "remotely")
260
+ liftIO $ threadDelay 2000000
214
261
{% endhighlight %}
215
262
216
- Note that we're not limited to sending ` Closure ` s - it is possible to send data
217
- without having static values, and assuming the receiving code is able to decode
218
- this data and operate on it, we can easily put together a simple AST that maps
219
- to operations we wish to execute remotely.
263
+ In the above example, we spawn ` sampleTask ` on node ` us ` in two
264
+ different ways:
265
+
266
+ * using ` spawn ` , which expects some node identifier to spawn a process
267
+ on along for the action of the process.
268
+ * using ` spawnLocal ` , a specialization of ` spawn ` to the case when the
269
+ node identifier actually refers to the local node (i.e. ` us ` ). In
270
+ this special case, no serialization is necessary, so passing an
271
+ action directly rather than a ` Closure ` works just fine.
220
272
221
273
------
222
274
0 commit comments