Consider a junction of digits:
say any 0..9; # OUTPUT: any(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
This carries 'any'
as a type of operation and 0..9
as a list of eigenstates internally. In a smartmatch context, this can match any object that can smartmatch against any of its digit eigenstates. While the list of these is not exposed, there is a means of traversing its contents.
The ~~
smartmatch operator delegates to the ACCEPTS
method on the RHS prior to a Bool
coercion on its result. For example, we’ll depend on Code.ACCEPTS
directly as a means of injecting behaviour into a smartmatch:
sub sum(Mu $topic is raw) { my $sum is default(0); sub sum($topic) { cas $sum, * + $topic } &sum.ACCEPTS: $topic; $sum } say sum any 0..9; # OUTPUT: # 45 say sum 0 & (9 ^ 9) & 0; # OUTPUT: # 18
In this case, it forwards a single argument (its “topic”) to an invocation of itself, allowing us to take a sum of eigenstates.
We give &sum
a closure to give us fresh $sum
for each call. Because the $topic
of the inner &sum
has no type, it carries a Mu
type like the outer $topic
, but will autothread a Junction:D
argument over Any
eigenstates. Because the outer $topic
is explicitly typed Mu
and is raw
however, it will leave both junctions and containers on input alone. Note that autothreading recurses over Junction:D
eigenstates, and while we could use +=
, a Junction
implementation may be parallel.
If we’re going to just accept one argument given a junction, it could just as well be a Mu
eigenstate instead of the junction itself. Mu.ACCEPTS
can thread its topic over Mu
instead of Any
given a Mu:U
invocant (Mu:D
is NYI; Any:D
defaults to &[===]
). Similarly, Junction.CALL-ME
threads its invocant over Mu
. Because a junction will not shortcircuit as it is threaded, these can be chained to traverse one’s eigenstates recursively:
class JMap does Callable { has &!transform is built(:bind); multi method ACCEPTS(::?CLASS:U: Mu $topic is raw) { self.bless: transform => { &^function($topic) } } multi method ACCEPTS(::?CLASS:D: Mu $topic is raw) { &!transform.ACCEPTS: -> Mu $thread is raw { $thread.ACCEPTS: $topic } } proto method CALL-ME(Mu) {*} multi method CALL-ME(::?CLASS:U: Mu $topic is raw) { self.ACCEPTS: $topic } multi method CALL-ME(::?CLASS:D: &function) is raw { &!transform(&function) } } say JMap(any 0..9)(*[]); # OUTPUT: # any(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) say JMap(any 0..9)(2 ** *); # OUTPUT: # any(1, 2, 4, 8, 16, 32, 64, 128, 256, 512) say JMap(0 & (9 ^ 9) & 0)(?*); # OUTPUT: # all(False, one(True, True), False)
JMap
gets its one chance to thread a Junction:D
as a type object, so we forward the junction to map before its Callable
. After a CALL-ME
, we wind up with either a callable or a new junction of callables that is invokable, but does not qualify as Callable
itself. Despite ACCEPTS
being typed with a Mu
topic over JMap
, the threading of junctions still wins in a dispatch.
JMap
can process the eigenstates of a junction generically, but carries overhead in its autothreading in CALL-ME
prior to any smartmatching against the result. If you have a particular Callable
in mind with which to map, this JTransformer
template can be followed:
class JTransformer does Callable is repr<Uninstantiable> { multi method ACCEPTS(Mu --> Code:D) { ... } method CALL-ME(Mu $topic is raw) { self.ACCEPTS: $topic } }
In general, a CALL-ME
would be used to thread its Mu $topic is raw
through ACCEPTS
. Instead of instantiating the invocant, this would return a bare block or anon sub
, which would perform a tailored smartmatch operation given the threaded Mu
in its context and a topic from any later smartmatch on said code object.
If a junction produced by CALL-ME
is cached, the ACCEPTS
candidate written can shoulder part of its resultant thunk’s work to make that cheaper to smartmatch, e.g. by preprocessing the path to a particular block. It can be cheapened further in this sense by subtyping Mu
directly in lieu of the default Any
for a simpler dispatch, though it becomes more difficult to work with in doing so:
class JTransformer is Mu does Callable is repr<Uninstantiable> { ... }
A practical example of a JTransformer
-like class is the internal class Refine
that backs my Kind
subsets’ refinements (where
clauses) as of v1.0.3. Because it’s dabbling in metaobjects (e.g. Mu
, Junction
), Any
cannot be assumed, but at the same time, junctions can allow for complex checks against multiple metaroles. If a metaobject threaded by its ACCEPTS
call cannot typecheck as Mu
for any reason, it will substitute a block wrapping a low-level typecheck against it, otherwise thunking a boolification of an ACCEPTS
call.
One thought on “Day 15: Junction transformers”