Recently, there was a discussion asking why, if raku Ranges can be used in arithmetic operations with Real values …
(1..2) + 3 => (4..5)
And yet when it’s a calculation with two Ranges, the operator applies Numeric context which then does the calculation on the length .elems
and not the contents! …
(1..2) + (3..4) => 4 #not (4..6)!
Only when one of the participants pointed me to the excellent Wikipedia article on Interval Arithmetic did it dawn on me why. I encourage anyone who uses real-world measurements with errors and uncertainty to read that page before continuing.
Let’s take it one step at a time:
Addition
That seems simple enough, here it is in raku code from the Math::Interval module:
method add {
Interval.new: (x1 + y1) .. (x2 + y2)
}
---
use Math::Interval;
say (1..2) + (2..4); #3..6
Subtraction
Hmmm – that’s a tad counter intuitive, not quite as simple as making a new Range by subtracting start of one from the start of the other and the end of one from the end of the other. I had to reread the Wikipedia page to grok this. And this potential for confusion is why I suspect that the raku language designers decided to duck the idea of having Range op Range
in the language core.
It was about now that I thought – OK I think we need Range op Range operations as Interval op Interval, but not in the raku core – and so I started work on a new module – Math::Interval
Anyway, here’s the code for subtract:
method sub {
Interval.new: (x1 - y2) .. (x2 - y1)
}
---
use Math::Interval;
say (2..4) - (1..2); #0..3 (yes - this is weird!)
Multiply
The weirdness of Interval Arithmetic steps up another notch with multiply – now its a min, max of the cross product over the Range|Interval arguments endpoints:
method mul {
#| make cross product, ie. x0*y0,x1*y0...
@!xXy = (x1, x2) X* (y1, y2);
Interval.new: @!xXy.min .. @!xXy.max
}
---
use Math::Interval;
say (2..4) * (1..2); #2..8
Again, the Wikipedia page does a great job of explaining why.
Divide
And then I felt my brain explode:
So, we need to unpack this a little:
- the tricky part is the inverse (1/[y1..y2])
- things are OK until 0 appears in the span or endpoints of the divisor interval
- so this is like divide by zero on steroids
- if either endpoint y1 or y2 are 0 then we can use ±Inf
- if both are 0 that’s a divide by zero error
- but if they span 0, then the result is a disjoint multi-interval
A What?
I went to bed that night feeling afraid, very afraid… and then bingo I realised that this is a job for raku Junctions…
… I also realised that a variant of the built in raku class Range was needed – since a continuous class Interval should not have Positional and Iterator roles.
Junctions
8.6. Junctions
A junction is a logical superposition of values.
In the below example 1|2|3 is a junction.
my $var = 2;
https://raku.guide/#_junctions
if $var == 1|2|3 {
say “The variable is 1 or 2 or 3”
}
The use of junctions usually triggers autothreading; the operation is carried out for each junction element, and all the results are combined into a new junction and returned.
Junction of Intervals
So, to cut a long story short, here is the module code:
#| make inverse, ie. 1/[y1..y2]
sub inverse($y) {
my \ss = (y1.sign == y2.sign); # same sign
given y1, y2 {
# continuous
when !0, !0 && ss { Interval.new: 1/y2 .. 1/y1 }
when !0, 0 { Interval.new: -Inf .. 1/y1 }
when 0, !0 { Interval.new: 1/y2 .. Inf }
# disjoint
when !0, !0 && !ss {
warn "divisor contains 0, returning a multi Interval Junction";
Interval.new(-Inf..1/y1) | Interval.new(1/y2..Inf)
}
# div 0 error
when 0, 0 { die "Divide by zero attempt." }
}
}
method div {
Interval.new: $!x * inverse($!y)
}
Now, here is a naughty divide in action:
# a divisor that spans 0 produces a disjoint multi-interval
my $j1 = (2..4)/(-2..4);
ddt $j1; #any(-Inf..-1.0, 0.5..Inf).Junction
say 3 ~~ $j1; #True
# Junction[Interval] can still be used
my $j2 = $j1 + 2;
ddt $j2; #any(-Inf..1.0, 2.5..Inf).Junction
say 5 ~~ $j2; #True
# but this can only go so far...
my $j3 = $j1 * (-2..4);
ddt $j3; #any(-Inf..Inf, -Inf..Inf).Junction
say 3 ~~ $j3; #True (but meaningless!)
Conclusion
Anyway, for me, it was an amazing surprise that raku Junctions could be so helpful to represent these disjoint multi-intervals. I get the feeling that I am walking in the footsteps of some very wise language design (both to provide the tool of Junction but also not to overstep the remit of a general purpose coding language).
Here are a couple of extra links if you would like to learn a bit more about Junctions:
- https://limited.systems/articles/reconstructing-raku-junctions/
- https://perl6advent.wordpress.com/2009/12/13/day-13-junctions/
And, as ever, please do comment, like and feedback on this post.
~librasteve
1 Comment