Sometimes you want to get things done to a piece of data, but you don't know in advance what that piece of data is made of. It can be an array, it can be a string, it can be a more complicated arrangement of data. Happens all the time in dynamically typed languages: you want to do something to a list of things, such as printing it or simply converting it to a string. You can jump to hoops and do a switch
clause and check type and dispatch the corresponding operation. Or you can use multiple dispatch.
Many languages use multiple dispatch
And they use it to bind a function to a type. For instance, Elixir, a Rubyfication of Erlang that turns it into something that can be understood by mere humans:
defprotocol Stringifier do
@doc "Can convert to string using tostring"
def tostring( whatever)
end
defmodule Span do
@doc "A few words"
defstruct words: []
end
defimpl Stringifier, for: Span do
def tostring(span), do: Enum.join(span.words," ")
end
defmodule Quoted do
@doc "A few words and a quote"
defstruct words: [], quote: "em"
def i(string, quote) do
"<#{quote}> #{string} </#{quote}>"
end
end
defimpl Stringifier, for: Quoted do
def tostring(quoted), do: Quoted.i(Enum.join(quoted.words," "),quoted.quote)
end
Some languages require the programmer to declare, in advance, the function we intend to use for multiple dispatch; in elixir parlance, they are called protocos. We need to declare a protocol
with the name of the function and the function signature. In this case, it takes a single artument, which we are going to be calling whatever
. We will call the protocol Stringifier
.
We will use the marked text model here as we have done so far. We have a Span
which is a simple list of words, and a Quoted
which adds some kind of quoting construct around it. Modules in Elixir are packaging constructs, but are in no way objects: they are structs and stuff you can do to them, but don't get instantiated or anything like that. But they at least define a structure we can use later on to define the multiple dispatching:
defimpl Stringifier, for: Span do
def tostring(span), do: Enum.join(span.words," ")
end
Since a protocol is kind of an abstract interface, we define a function that follows that interface by declaring an implementation using defimpl
; defimpl Stringifier
defines an implementation of that protocol. Applied to what? for: Span
says this is going to be applied to that specific data struct
. The actual implementation of the function follows, and it wraps around the join
ed words the quote we have defined. The other implementation follows the same, but when we want to use it:
defmodule Main do
def main do
words = %Span{words: ["A","few","words"]}
IO.puts(Stringifier.tostring(words))
quoted = %Quoted{words: ["More","words"], quote: "em"}
IO.puts(Stringifier.tostring(quoted))
end
end
Main.main
No matter what we use, IO.puts(Stringifier.tostring(quoted))
will detect the data type and will dispatch to the corresponding code the message so that it gets printed alright.
And we know that Perl6 throws everything and the kitchen sink in. Let's see
Multiple dispatch in Perl6
We have seen a bit of that already. Grammars use multiple dispatch to deal with symbols that have pretty much the same function, as in quoting constructs. Since grammars are classes, multiple-dispatching rules which are methods make sense. But let's go back to the actual data structures that would represent paragraphs with enrichment:
role stringifiable {
method to-string( Str $ligature ) { ... }
}
# Thanks to https://perl6advent.wordpress.com/2009/12/18/day-18-roles/
role Span does stringifiable {
has @.words;
method to-string( Str $ligature ) {
return @!words.join( $ligature );
}
}
class Quoted does Span {
has $.quote;
method to-string( Str $ligature ) {
return self.þ ~ self.Span::to-string( $ligature ) ~ self.ø;
}
method þ () {
return "<" ~ $!quote ~ ">";
}
method ø () {
return "</" ~ $!quote ~ ">";
}
}
class Heading does Span {
submethod BUILD( :@!words ) {
}
method to-string() {
return "\n<h1>" ~ self.Span::to-string( " " ) ~ "</h1>\n";
}
}
It's rather longish because we are throwing a bit of everything we have done so far in. We define a lightweight role which is just a function, with no implementation. The { ... }
takes care of that. We defer the implementation to the role users, and we actually force them to do so. We have another role, Span
, which is consumed by two classes, Quoted
for enriched words and Heading
for header class. This is a more full-fledged class with its BUILD
function and all, but it needs that since it's got to tie the object instantiation function (new
) to the instance variables. Been there, done that. No big deal. But now we might need to convert to string some object whose class we do not know in advance. Multiple dispatch to the rescue:
# Stringifier for stuff above
proto stringify-chunk( | ) {*}
multi stringify-chunk( $chunk where Quoted | Span ) {
return $chunk.to-string( " " );
}
multi stringify-chunk( Heading $heading ) {
return $heading.to-string();
}
We use exactly the same syntax we used before in grammars. A proto
says "Hey, this is the way the multiply dispatched functions are going to look". Proto as in protocol
, or maybe prototype, but protocol suits me just fine since it's how they are called in Elixir and we have seen before. However, Elixir used a placeholder to say that the implementations were going to use a single word. Perl6 is more flexible: | is a Capture
which says "I don't care what you throw in here, I'll take care of it".
The instantiations of the proto(type|col) use multi
, as in multi
ple dispatch. They do not need to declare that they are part of that protocol, just the general shape of it takes care of that. But the first one use again |
in a different way. We got Span
and Quoted
, but they are pretty much the same, so they can use the same implementation. $chunk where Quoted | Span
uses two features in Perl6. Quoted | Span
is a Junction whose value is one or the other. And Perl6 allows to add constrains to type declarations via where
. $chunk
can be either a Quoted
or a Span
, and we declare it in a very compact fashion.
Another proof that Perl6 gets stuff done. We have here a
multi
function that deals with multiple types too.
We will use this function from here:
class Paragraph {
has stringifiable @!chunks;
method new( *@chunks ) {
return self.bless(:@chunks);
}
submethod BUILD( :@!chunks ) {}
method to-string() {
return "\n" ~ (@!chunks.map: { stringify-chunk( $^þ ) }).join(" " );
}
}
Which is a class with new
and all. This kind of function is a method (can be inherited) and not a submethod (which can't) and allows us to make a more compact definition that does not use named parameters. *@chunks
is a slurpy parameters (uses *) which takes everything and puts it into an array @chunks
, which we then bless and turn into the instance variable. But @chunks
does stringifiable
. We don't know what every piece of the text will be in advance, but we do know we can string
it. That is why we call { stringify-chunk( $^þ ) }
on it later on.
How can we use it?
As usual, the full code is in GitHub
my $heading = Heading.new( words => "Here we are".comb(/\w+/) );
my $span = Span.new( words => "This goes first");
my $quoted = Quoted.new( words => "This is last",
quote => "em" );
my $sentence = Paragraph.new( $heading, $span, $quoted );
say $sentence.to-string();
We define three different kinds of objects, which get slurped when defining a Paragraph
, and eventually are printed by calling the multi stringify-chunk
on them using map
. The result is a not incredibly beautiful but serviceable
<h1>Here we are</h1>
This goes first <em>This is last</em>
To be continued
Grammars are kind of the nexus of everything that is new and beautiful about Perl6. We will continue expanding the grammar with new features to make it almost, but not quite, a Markdown grammar.
Top comments (0)