From 012d0d2f7facb6b736c59970bf362bb8d9c27035 Mon Sep 17 00:00:00 2001 From: Ben Davies Date: Tue, 6 Dec 2022 17:27:41 -0400 Subject: [PATCH] Add Kaiepi's "Junction Transformers" article --- .../articles/junction-transformers.md | 119 ++++++++++++++++++ raku-advent-2022/authors.md | 1 + 2 files changed, 120 insertions(+) create mode 100644 raku-advent-2022/articles/junction-transformers.md diff --git a/raku-advent-2022/articles/junction-transformers.md b/raku-advent-2022/articles/junction-transformers.md new file mode 100644 index 0000000..c384622 --- /dev/null +++ b/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 { + 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 { ... } +``` + +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. diff --git a/raku-advent-2022/authors.md b/raku-advent-2022/authors.md index 2400262..0902721 100644 --- a/raku-advent-2022/authors.md +++ b/raku-advent-2022/authors.md @@ -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