[OCaml] Clever Messaging

Started by
2 comments, last by ToohrVyk 16 years, 7 months ago
I am facing the issue of implementing a message system between game components in OCaml. The problem here is the Open/Closed principle: I want to be able to create a basic component without specifying all the messages that it can send or receive. In short, I need to write the following in a class:
type message = (* clever definition *)

class abstract component = 
object
  method virtual react : message
end
And never worry about changing the definition of a message again. Intended behaviour is that bjects can send each other the messages of their choice (I can live with some constraints on what messages actually are, as long as I can create new kinds of messages without altering the above definition). If an object doesn't know how to interpret a message, it raises an exception. My first approach was to use an exception-throwing approach: a message is fundamentally an unit -> unit function which raises an exception, perhaps with some additional generic information around it (such as the sender). In short:
(* The basic definition *)
type message = unit -> unit

class abstract component = 
object
  method virtual react : message -> unit
end

(* An example class with a custom message *)
exception MyPrintMessage of string
let myPrintMessage str () -> raise (MyPrintMessage str)

class printer = object
  inherit component
  method react msg = 
    try msg () with
      | MyPrintMessage s -> Printf.printf "Message: %s\n" s
      | n -> raise n
end

(* Sending a message to a 'component' *)
let c = 
  (new printer) :> component
in
c # react (myPrintMessage "Hello, world!")
The messages are clean, and so is their syntax, and they can also be easily serialized and sent over a network. However, I'm worried about the fact that this isn't very elegant (it's using exceptions at cross-purpose) and might incur performance penalties because of the exception 'walk back up the stack' process. Any ideas on insights about either this method or an alternate open-closed method?
Advertisement
Let's see if I understand correctly...

  • You want to be able to add messages easily without having to change one central definition, meaning you can't use variants.

  • You want your messages to carry information, so you can't use ints (problems with choosing unique values for separate messages) of strings (well you could use strings and parse them, and no type safety), but these are hardly robust solutions.

  • You can't use records because AFAIK there's no subtyping relationship between them in O'Caml, i.e. { x:int } and { x:int; s:string } are not related.

  • You could implement a kind of dynamic record yourself, i.e. type dynamic_record = (string * object) list with object a variant enumerating all possible types for you to use, but a number of problems come with it (no type safety, objects definition m.

  • Using classes as objects should work, but you'd need to emulate multiple dispatch (visitors? I'm not familiar enough with classes/objects in O'Caml to know whether or not that's possible).

  • So, that leaves exceptions, which are in a way perfect: they can carry their own extra baggage, subtyping relations don't matter, there's a matching-construct already waiting for you, uniqueness is guaranteed, everything is type-safe... But using them for this kind of thing is icky, and they're possibly slow.



If I were in that kind of trouble, I would start thinking metaprogramming (it seems that's my answer for everything these days...). With CamlP4 (or whatever, you could use Perl too in theory) I would try to build new abstractions for messages: a message declaration (message print_message of string) and a message-matching constructor (msg_match msg | print_message str -> ...). As a first implementation, message might simply be translated into exception and msg_match into a try-block. A better solution would probably be to use variants: the preprocessor collects all message declarations and puts them into one big variant, etc. To me, it seems that this would be the most natural O'Caml solution.

Overkill? :)
Wouldn't polymorphic variants work?
After some more experiments, it seems that there is a possible usage of polymorphic variants, as:

class virtual ['message] component =   object    method virtual send : 'message -> unit  endclass ['message] printer =  object    inherit 'message component    method send = function      | `Print s -> print_endline s      | _ -> raise UnknownMessage  end(new printer) # send (`Print "Hello, world!")


This has the ugly side-effect of not having any namespacing (that is, once someone somewhere defined `Print, nobody else is allowed to use that name ever) which does not exist with exceptions (which can be disambiguated using module names). Plus, it needs the 'message type variable everywhere for resolution purposes.

Ultimately, I remembered that exceptions can be manipulated and matched without requiring a throw, so:
type message = { sender : id; payload : exn }class virtual component =   object    method virtual send : message -> unit  endmodule Print = struct  exception MsgPrint of string  class printer =     object      inherit component      method send msg =        match msg.payload with           | MsgPrint s -> print_endline s          | _ -> raise UnknownMessage    endend(new Print.Printer) # send {   sender = world;   payload = Print.MsgPrint "Hello, world"}

This topic is closed to new replies.

Advertisement