Skip to content

Commit

Permalink
Add Kaiepi's "Junction Transformers" article
Browse files Browse the repository at this point in the history
  • Loading branch information
Kaiepi committed Dec 7, 2022
1 parent 15a8139 commit 012d0d2
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 0 deletions.
119 changes: 119 additions & 0 deletions raku-advent-2022/articles/junction-transformers.md
@@ -0,0 +1,119 @@
Junction Transformers
=====================

Consider a junction of digits:

```raku
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 against any
object that can smartmatch against any digit. The list of eigenstates is not
exposed, but there is a means of processing them individually.

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 introducing side effects into the smartmatch operation:

```raku
sub sum(Mu $topic is raw) {
my $sum is default(0);
sub sum($topic) { $sum += $topic }
&sum.ACCEPTS: $topic;
$sum
}

say sum 2; # OUTPUT: 2
say sum 2 | 2; # OUTPUT: 4
say sum 0 & 4 & 0 & 4 & 0; # OUTPUT: 8
```

In this case, it forwards a single argument (its "topic") to an invokation 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`, it will leave both junctions
and containers on input alone.

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. Like `Mu.ACCEPTS` can
*thread* its topic over `Mu` given a type object invocant (instances are NYI;
they default to `&[===]` on `Any`), `Junction.CALL-ME` threads its invocant
over `Mu`. Because this will *not* shortcircuit, these can be chained to
traverse each of the eigenstates of a junction in sequence:

```raku
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)
}
}

given JMap(any 0..9) -> &digits {
say 0 ~~ &digits; # OUTPUT: True
say 10 ~~ &digits; # OUTPUT: False
say digits 2 ** *; # OUTPUT: any(1, 2, 4, 8, 16, 32, 64, 128, 256, 512)
}
```

`JMap` gets its one chance to thread a `Junction:D` as a type object, so we
forward the junction to map before its `Callable`. Should more junctions be
considered, more type objects or a means of typing them for `ACCEPTS` would be
necessary. 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. Though `ACCEPTS` makes matters complicated, we can drop the `Any` bound
autothreading enforces this way.

`JMap` can process the eigenstates of a junction generically, but carries
overhead in its threading as a result. If you have a particular `Callable` in
mind with which to map, this `JTransformer` template can be followed:

```raku
class JTransformer does Callable is repr<Uninstantiable> {
multi method ACCEPTS(Mu --> Code:D) { ... }

method CALL-ME(Mu $topic is raw) 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:

```raku
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`](https://raku.land/zef:Kaiepi/Kind) subsets'
refinements (`where` clauses) as of
[v1.0.2](https://github.com/Kaiepi/ra-Kind/blob/v1.0.2/lib/Kind.pm6#L45-L74).
Because it's dabbling in metaobjects, `Any` cannot be assumed (e.g. `Mu`,
`Junction`), but 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.
1 change: 1 addition & 0 deletions raku-advent-2022/authors.md
Expand Up @@ -31,4 +31,5 @@ reminders.
16. melezhik: SparrowCI pipelines cascades of fun
17. p6steve: Day XX [Santa CL::AWS](https://wordpress.com/post/raku-advent.blog/2047) --- ready to go ... please change the Day XX in the title
18. smokemachine: RedFactory
19. Kaiepi: Junction Transformers
<!-- add your presentation here -->

0 comments on commit 012d0d2

Please sign in to comment.