Skip to content

Instantly share code, notes, and snippets.

@wimvanderbauwhede
Last active October 15, 2020 09:14
Show Gist options
  • Select an option

  • Save wimvanderbauwhede/19cc1e8d04e9a477f58cfe7288a6172e to your computer and use it in GitHub Desktop.

Select an option

Save wimvanderbauwhede/19cc1e8d04e9a477f58cfe7288a6172e to your computer and use it in GitHub Desktop.

Revisions

  1. wimvanderbauwhede revised this gist Oct 4, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion raku-junctions-reconstructed.md
    Original file line number Diff line number Diff line change
    @@ -316,4 +316,4 @@ my \p4r = ( sub (\x){ &m.assuming(*,x) } ●○ all_ [33,44] ) ○○ any_ [11,2

    ## Conclusion

    Starting from the hypothesis that the magic typing behaviour of Raku's junctions is actually syntactic sugar, I reconstructed the Junction type and its actions using a polymorphic algebraic data type and showed that the interpretation of Raku's behviour as syntactic sugar holds for the presented implementation. In other words, Raku's Junctions do follow static typing rules.
    Starting from the hypothesis that the magic typing behaviour of Raku's junctions is actually syntactic sugar, I reconstructed the Junction type and its actions using a polymorphic algebraic data type and showed that the interpretation of Raku's behaviour as syntactic sugar holds for the presented implementation. In other words, Raku's Junctions do follow static typing rules.
  2. wimvanderbauwhede revised this gist Oct 4, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion raku-junctions-reconstructed.md
    Original file line number Diff line number Diff line change
    @@ -316,4 +316,4 @@ my \p4r = ( sub (\x){ &m.assuming(*,x) } ●○ all_ [33,44] ) ○○ any_ [11,2

    ## Conclusion

    Starting from the hypothesis that the magic typing behaviour of Raku's junctions is actually syntactic sugar, I reconstructed the Junction type and its actions using a polymorphic algebraic data type and showed that the interpretation of Raky's behviour as syntactic sugar holds for the presented implementation. In other words, Raku's Junctions do follow static typing rules.
    Starting from the hypothesis that the magic typing behaviour of Raku's junctions is actually syntactic sugar, I reconstructed the Junction type and its actions using a polymorphic algebraic data type and showed that the interpretation of Raku's behviour as syntactic sugar holds for the presented implementation. In other words, Raku's Junctions do follow static typing rules.
  3. wimvanderbauwhede revised this gist Oct 3, 2020. No changes.
  4. wimvanderbauwhede revised this gist Oct 3, 2020. 1 changed file with 8 additions and 4 deletions.
    12 changes: 8 additions & 4 deletions raku-junctions-reconstructed.md
    Original file line number Diff line number Diff line change
    @@ -132,7 +132,7 @@ Finally we have `collapse`, which returns the value from a junction, provided th
    collapse :: (Show a,Eq a) => Junction a -> a
    ```

    This may seem like a strange function but it is necessary because of the behaviour of junctions. As we will see, the above semantics imply that junctions are greedy: if a single argument of a function is a junction, then all other arguments also become junctions, but all values in the junction are identical. I have discussed this in ["The strange case of the greedy junction"]({{site.url}}/greedy-junctions), but we can now formalise this behaviour.
    This may seem like a strange function but it is necessary because of the behaviour of junctions. As we will see, the above semantics imply that junctions are greedy: if a single argument of a function is a junction, then all other arguments also become junctions, but all values in the junction are identical. I have discussed this in ["The strange case of the greedy junction"](https://gist.github.com/wimvanderbauwhede/85fb4b88ec53a0b8149e6c05740adcf8), but we can now formalise this behaviour.

    ### Revisiting the strange case of the greedy junction

    @@ -142,7 +142,7 @@ Now, let's consider the particular case where the type `c` is `forall d . (a ->

    Let's assume we apply this function (call it `p`) to `fst :: a->b->a`, using `p ○• fst`, then we get `Junction a`. But if we apply it to `snd :: a->b->b`, using `p ○• snd`, then we get `Junction b`. Recall that we applied the original function `f` to a `Junction a` and a non-Junction `b`. Yet whatever we do, we can't recover the `b`. The result is always wrapped in a junction.

    This is the formal type-based analysis of why we can't return a non-Junction value from a pair as explained in ["The strange case of the greedy junction"]({{site.url}}/greedy-junctions). And this is why we need the `collapse` function.
    This is the formal type-based analysis of why we can't return a non-Junction value from a pair as explained in ["The strange case of the greedy junction"](https://gist.github.com/wimvanderbauwhede/85fb4b88ec53a0b8149e6c05740adcf8). And this is why we need the `collapse` function.

    ## Reconstructing junctions part 2: Raku implementation

    @@ -239,7 +239,7 @@ our sub so (\jv) {
    }
    ```

    And finally we have `collapse`, as defined in [the article on greedy junctions]({{site.url}}/greedy-junctions). `collapse` returns the value form a junction provided they are all the same:
    And finally we have `collapse`, as defined in [the article on greedy junctions](https://gist.github.com/wimvanderbauwhede/85fb4b88ec53a0b8149e6c05740adcf8). `collapse` returns the value form a junction provided they are all the same:

    ```perl6
    our sub collapse( \j ) {
    @@ -287,7 +287,7 @@ if (so ($j ○==● 42)) {...}

    and similar for other Boolean contexts.

    If we look closer at [the pair example from the greedy junctions article]({{site.url}}/greedy-junctions), then applying a junction to a function with multiple arguments
    If we look closer at [the pair example from the greedy junctions article](https://gist.github.com/wimvanderbauwhede/85fb4b88ec53a0b8149e6c05740adcf8), then applying a junction to a function with multiple arguments

    ```perl6
    my Junction \p1j = pair R,(42^43);
    @@ -313,3 +313,7 @@ sub m(\x,\y){x*y}
    my \p4 = ( sub (\x){ &m.assuming(x) } ●○ any_ [11,22] ) ○○ all_ [33,44];
    my \p4r = ( sub (\x){ &m.assuming(*,x) } ●○ all_ [33,44] ) ○○ any_ [11,22];
    ```

    ## Conclusion

    Starting from the hypothesis that the magic typing behaviour of Raku's junctions is actually syntactic sugar, I reconstructed the Junction type and its actions using a polymorphic algebraic data type and showed that the interpretation of Raky's behviour as syntactic sugar holds for the presented implementation. In other words, Raku's Junctions do follow static typing rules.
  5. wimvanderbauwhede created this gist Oct 3, 2020.
    315 changes: 315 additions & 0 deletions raku-junctions-reconstructed.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,315 @@
    # Reconstructing Raku's Junctions

    Junctions in Raku are cool but at first glance they do not follow the rules for static typing. I was curious about their formal typing semantics, so I deconstructed and then reconstructed junctions from a functional, static typing perspective.

    ## Junctions in Raku

    [Raku](https://raku.org/) has this neat feature called [Junctions](https://docs.raku.org/). A junction is an unordered composite value. When a junction is used instead of a value, the operation is carried out for each junction element, and the result is the junction of the return values of all those operators. Junctions collapse into a single value when used in a Boolean context. Junctions can be of type _all_ (`&`), _any_ (`|`), _one_ (`^`) or _none_ (empty junction).

    For example,

    ```perl6
    my $j = 11|22; # short for any(11,22)
    if 33 == $j + 11 {
    say 'yes';
    }

    say so 3 == (1..30).one; #=> True
    say so ("a" ^ "b" ^ "c") eq "a"; #=> True
    ```

    The function `so` forces the Boolean context.

    Junctions have type _Junction_, and I was curious about the typing rules, because at first sight there is something strange. Let's say we have a function `sq` from _Int_ to _Int_ :

    ```perl6
    sub sq(Int $x --> Int) { $x*$x }

    my Int $res = sq(11); # OK
    say $res; #=> 121
    ```

    Now let's define a junction of type _any_ of _Int_ values:

    ```perl6
    my Junction $j = 11 | 22;
    ```

    When we apply `sq` to `$j`, we do not get a type error, even though the functions has type `:(Int --> Int)` and the junction has type `Junction`. Instead, we get a junction of the results:

    ```perl6
    say sq($j); #=> any(121, 484)
    ```

    If we assign this to a variable of type _Int_ as before, we get a type error:
    ```perl6
    my Int $rj = sq($j); #=> Type check failed in assignment to $rj; expected Int but got Junction (any(121, 484))
    ```
    Instead, the return value is now of type _Junction_:

    ```
    my Junction $rj = sq(11|22); # OK
    ```

    So the _Junction_ type can take the place of any other type but then the operation becomes a junction as well.

    On the other hand, junctions are implicitly typed by their constituent values, even though they seem to be of the opaque type _Junction_. For example, if we create a junction of _Str_ values, and try to pass this junction value into `sq`, we get a type error:

    ```perl6
    my $sj = '11' | '22';
    say $sj.WHAT; #=>(Junction)

    my Junction $svj = sq($sj); #=> Type check failed in binding to parameter 'x'; expected Int but got Str ("11")
    ```

    ## Junctions do not follow static typing rules

    Although this _kind of_ makes sense (we don't want it to work with _Str_ if the original function expects _Int_), this does flout the rules for static typing, even with subtyping. If an argument is of type _Int_ then any type below it in the type graph can be used instead. But the simplified type graph for Int and Junction is as follows:

    Int -> Cool -> Any -> Mu <- Junction

    So a Junction is never a subtype of anything below _Any_. Therefore putting a junction in a slot of type _Any_ or subtype thereof should be a type error.

    Furthermore, because the _Junction_ type is opaque (i.e. it is not a parametrised type), it should not hold any information about the type of the values inside the junction. And yet it does type check against these invisible, unaccessible values.

    So what is happening here?

    ## A working hypothesis

    A working hypothesis is that a _Junction_ type does not really take the place of any other type: it is merely a syntactic sugar that makes it seem so.

    ## Reconstructing junctions part 1: types

    Let's try and reconstruct this. The aim is to come up with a data type and some actions that will replicate the observed behaviour of Raku's junctions.
    First we discuss the types, using Haskell notation for clarity. Then I present the implementation in Raku. This implementation will behave like Raku's native junctions but without the magic syntactic sugar. In this way I show that Raku's junctions do follow proper typing rules after all.

    ### The Junction type

    A _Junction_ is a data structure consisting of a junction type _JType_ and a set of values.
    I restrict this set of values to a single type for convenience and also because a junction of mixed types does actually not make much sense. I use a list to model the set, again for convenience. Because a _Junction_ can contain values of any type, it is a polymorphic algebraic data type:

    ```haskell
    data JType = JAny | JAll | JOne | JNone

    data Junction a = Junction JType [a]
    ```

    ### Applying junctions

    Doing anything with a junction means applying a function to it. We can consider three cases, and I introduce an ad-hoc custom operator for each of them:

    - Apply a non-_Junction_ function to a _Junction_ expression
    ```haskell
    (•○) :: (a -> b) -> Junction a -> Junction b
    ```
    - Apply a _Junction_ function to a non-_Junction_ expression
    ```haskell
    (○•) :: Junction (b -> c) -> b -> Junction c
    ```
    - Apply a _Junction_ function to a _Junction_ expression, creating a nested junction
    ```haskell
    (○○) :: Junction (b -> c) -> Junction b -> Junction (Junction c)
    ```

    For convenience, we can also create custom comparison operators between _Junction a_ and _a_:

    ```haskell
    -- and similar for /-, >, <, <=,>=
    (○==•) :: Junction a -> a -> Bool
    ```

    ### Collapsing junctions

    Then we have `so`, the Boolean coercion function. What it does is to collapse a junction of Booleans into a single Boolean.

    ```haskell
    so :: Junction Bool -> Bool
    ```

    Finally we have `collapse`, which returns the value from a junction, provided that it is a junction where all stored values are the same.

    ```haskell
    collapse :: (Show a,Eq a) => Junction a -> a
    ```

    This may seem like a strange function but it is necessary because of the behaviour of junctions. As we will see, the above semantics imply that junctions are greedy: if a single argument of a function is a junction, then all other arguments also become junctions, but all values in the junction are identical. I have discussed this in ["The strange case of the greedy junction"]({{site.url}}/greedy-junctions), but we can now formalise this behaviour.

    ### Revisiting the strange case of the greedy junction

    Suppose we have a function of two arguments `f :: a -> b -> c`, and we apply a junction `j :: Junction a` to the first argument, `f •○ j`. Then the result is a partially applied function wrapped in a Junction: `fp :: Junction b -> c`. If we now want to apply this function a non-Junction value `v :: b` using `fp ○• v`, the result is of type `Junction c`. So already we see that the non-Junction value `v` is assimilated into the junction.

    Now, let's consider the particular case where the type `c` is `forall d . (a -> b -> d) -> d`, so we have `Junction (forall d . (a->b->d) -> d)`. This is a function which takes a function argument and returns something of the return type of that function. We use the `forall` so that `d` can be anything, but in practive we want it to be either `a` or `b`.

    Let's assume we apply this function (call it `p`) to `fst :: a->b->a`, using `p ○• fst`, then we get `Junction a`. But if we apply it to `snd :: a->b->b`, using `p ○• snd`, then we get `Junction b`. Recall that we applied the original function `f` to a `Junction a` and a non-Junction `b`. Yet whatever we do, we can't recover the `b`. The result is always wrapped in a junction.

    This is the formal type-based analysis of why we can't return a non-Junction value from a pair as explained in ["The strange case of the greedy junction"]({{site.url}}/greedy-junctions). And this is why we need the `collapse` function.

    ## Reconstructing junctions part 2: Raku implementation

    We start by creating the Junction type, using an enum for the four types of junctions, and a role for the actual _Junction_ data type:

    ```perl6
    # The types of Junctions
    enum JType <JAny JAll JOne JNone >;

    # The actual Junction type
    role Junction[\jt, @vs] {
    has JType $.junction-type=jt;
    has @.values=@vs;
    }
    ```

    Next, the constructors for the four types of junctions (underscore to avoid the name conflict with the builtins):

    ```perl6
    our sub all_(@vs) {
    Junction[ JAll, @vs].new;
    }

    our sub any_(@vs) {
    Junction[ JAny, @vs].new;
    }

    our sub one_(@vs) {
    Junction[ JOne, @vs].new;
    }

    our sub none_(@vs) {
    Junction[ JNone, @vs].new;
    }
    ```

    To apply a (single-argument) function to a junction argument

    ```perl6
    sub infix:<●○>( &f, \j ) is export {
    my \jt=j.junction-type;
    my @vs = j.values;

    Junction[ jt, map( {&f($_)}, @vs)].new;
    }
    ```

    To apply a function inside a junction to a non-junction an argument

    ```perl6
    sub infix:<○●>( \jf, \v ) is export {
    my \jt=jf.junction-type;
    my @fs = jf.values;

    Junction[ jt, map( {$_( v)}, @fs)].new;
    }
    ```

    To apply a function to two junction arguments is equivalent to applying a function inside a junction to a junction. There is a complication here: Raku imposes an ordering on the nesting such that `all` is always the outer nest. Therefore we must check the types of the junctions and swap the maps if required.

    ```perl6
    sub infix:<○○>( \jf, \jv ) is export {
    my \jft= jf.junction-type;
    my @fs = jf.values;
    my \jvt = jv.junction-type;
    my @vs = jv.values;
    if (jvt == JAll and jft != JAll) {
    Junction[ jvt, map( sub (\v){jf ○● v}, @vs)].new;
    } else {
    Junction[ jft, map( sub (&f){ &f ●○ jv}, @fs)].new;
    }
    }
    ```

    For completeness, here is the definition of `○==●`. Definitions of `○!=●`, `○>●`. etc are analogous.

    ```perl6
    sub infix:<==>( \j, \v ) is export {
    sub (\x){x==v} ●○ j
    }
    ```

    Next we have `so`, which turns a junction of Booleans into a Boolean:

    ```perl6
    our sub so (\jv) {
    my @vs = jv.values;
    given jv.junction-type {
    when JAny { elems(grep {$_}, @vs) >0}
    when JAll { elems(grep {!$_}, @vs)==0}
    when JOne { elems(grep {$_}, @vs)==1}
    when JOne { elems(grep {$_}, @vs)==0}
    }
    }
    ```

    And finally we have `collapse`, as defined in [the article on greedy junctions]({{site.url}}/greedy-junctions). `collapse` returns the value form a junction provided they are all the same:

    ```perl6
    our sub collapse( \j ) {
    my \jt=j.junction-type;
    my @vvs = j.values;
    my $v = shift @vvs;
    my @ts = grep {!($_ ~~ $v)}, @vvs;
    if (@ts.elems==0) {
    $v
    } else {
    die "Can't collapse this Junction: elements are not identical: {$v,@vvs}";
    }
    }
    ```


    ## Junctions desugared

    Let's now look at our working hypothesis again, the interpretation of actions on Raku's junctions as syntactic sugar for the above type and operators.

    ```perl6
    sub sq(Int $x --> Int) { $x*$x }
    my Junction $j = 11 | 22;
    my Junction $rj = sq($j);
    ```

    Desugared this becomes:

    ```haskell
    my Junction $j = any_ [11,22];
    my Junction $rj = &sq ●○ $j;
    ```

    Similarly,

    ```perl6
    if ($j == 42) {...}
    ```

    becomes

    ```
    if (so ($j ○==● 42)) {...}
    ```

    and similar for other Boolean contexts.

    If we look closer at [the pair example from the greedy junctions article]({{site.url}}/greedy-junctions), then applying a junction to a function with multiple arguments

    ```perl6
    my Junction \p1j = pair R,(42^43);
    ```

    is desugared as

    ```perl6
    my Junction \p1j = &pair.assuming(R) ●○ one_ [42,43];
    ```

    We use `.assuming()` because we need partial application. It does not matter whether we apply first the non-junction argument or the junction argument:

    ```perl6
    my \p1jr = ( sub ($y){ &pair.assuming(*,$y) } ●○ one_ [42,43] ) ○● R;
    ```

    Finally, an example where both arguments are junctions. Because of the definition of `○○`, the order of application does not matter.

    ```perl6
    sub m(\x,\y){x*y}

    my \p4 = ( sub (\x){ &m.assuming(x) } ●○ any_ [11,22] ) ○○ all_ [33,44];
    my \p4r = ( sub (\x){ &m.assuming(*,x) } ●○ all_ [33,44] ) ○○ any_ [11,22];
    ```