P6SGI: Revising and Reviewing
After my previous post I received quite a bit of really good, constructive feedback. Thank you all who responded to my request for comments! I would say the gist of the feedback was this:
- Make the interface simpler for middleware and to a lesser extent, servers.
- Consider always requiring the Promise interface.
- Consider using Supply instead of Channel as it is non-blocking and likely to perform better.
After wrangling and playing around with various things and learning more about Perl 6 than I knew before, I have come up with what I think will be a stable interface that satisfies all of these suggestions.
First, since Promises are easy to do, I agree, let's just always require them. This means that the basic Hello World app now looks like this:
sub app(%env) {
start {
200, [ Content-Type => 'text/plain' ], [ 'Hello World' ]
};
}
After much research and playing around with the Supply type, I have decided that it is generally superior to Channel. Therefore, we will use Supply to generate the message body, not a Channel. The main problem with Channel is that it will block the server on receive. You can get around this, but it is either a headache or requires a dedicated thread. I originally chose Channel over Supply, though, because Supply is flexible enough to cause trouble if the application developer is sloppy. The benefits, however, far outweigh the risks. Besides, since when has Perl ever shied away from letting sloppy developers shoot themselves in the foot?
Finally, I have unified the interface so that middleware and servers can process the application output without much effort, but in a way that allows the application flexibility to respond in just about any way it pleases. This is possible because of the way Perl 6 coercions work. Let's examine the definition of an application and then explore a couple examples.
The application must return a Promise or it must return an object that coerces into a Promise. In Perl, an object coerces to another if it provides a method with the name of the coercion to perform. Therefore, an application could return a Supply as it can be turned into a Promise automatically.
The Promise must be kept with a Capture that contains three positional arguments, or it must provide something that coerces into such a Capture, such as a 3-element List or Array.
The first argument of the Capture is the status code, which must be an Int or (you guessed it) something that coerces into an Int. That means it could be Enum or some other kind of object representing each of the possible HTTP status codes.
The second argument of the Capture is the list of headers. As before, this is a list of Pairs mapping header names to header values. Again, it could be any object that coerces to such a list.
The third argument of the Capture is a Supply that emits Str and Blob values, or any object that coerces into such a Supply. Both List and Array coerce into such a Supply safely, so we can just return an Array or List in simple cases.
This means that we have the simplicity for this to work:
sub app(%env) {
start {
my $n = %env.Int;
my $acc = 1.FatRat;
200,
[ Content-Type => 'text/plain' ],
[ do for 1..$n { "{$acc *= $_}\n" } ]
};
}
and the flexibility to make something that outputs it's first byte faster like this:
sub app(%env) {
start {
my $n = %env.Int;
200,
[ Content-Type => 'text/plain' ],
Supply.on-demand(-> $content {
my $acc = 1.FatRat;
for 1..$n {
$content.emit("{$acc *= $_}\n");
}
$content.done;
});
};
}
and still have middleware that processes each like this:
sub add-one(%env) {
callsame().then(-> $p {
my ($s, @h, Supply(Any) $body) = $p.result;
$s, @h, $body.map(* + 1);
});
}
&app.wrap(&add-one);
I think that's pretty awesome.
Cheers.
Edit: The folks in #perl6 corrected a misunderstanding I had about the start { }
deprecation. I have adjusted the code here accordingly.
Leave a comment