raku: Home on the Range

After some controversy about the use of operators on Ranges in raku, I am inspired to write a new post.

An insightful comment by vrurg mentioned https://en.wikipedia.org/wiki/Interval_arithmetic and a re-skim of the raku docs made me realise that I was missing a chunk of the design intent of a raku Range – in bold

Ranges serve two main purposes: to generate lists of consecutive numbers or strings, and to act as a matcher to check if a number or string is within a certain range.

https://docs.raku.org/type/Range
Tolerance function (turquoise) and interval-valued approximation (red)

Here’s the first part of BMI example from wikipedia:

Consider the calculation of a person’s body mass index (BMI). BMI is calculated as a person’s body weight in kilograms divided by the square of their height in meters. Suppose a person uses a scale that has a precision of one kilogram, where intermediate values cannot be discerned, and the true weight is rounded to the nearest whole number. For example, 79.6 kg and 80.3kg are indistinguishable, as the scale can only display values to the nearest kilogram. It is unlikely that when the scale reads 80kg, the person has a weight of exactly 80.0kg. Thus, the scale displaying 80kg indicates a weight between 79.5kg and 80.5kg, or the interval {\displaystyle [79.5,80.5)}.

And here’s is a cod example of how that may look, first the class definitions (don’t worry if this is not 100% clear, see below for explanatory notes):

class Measure {
    has $.units;
    has $.value;
    has $.error = 0;    # ie ± half the precision

    method Numeric { +$!value }                    # note[1]

    method Str { "$!value ± $!error $!units" }     # note[2]

    method Range {                                 # note[3]
        ($!value - $!error) .. ($!value + $!error)
    }   

    multi method ACCEPTS( Measure:D \other ) {     # note[4]
        self.Range.ACCEPTS: other.Range
    }   
}

class Weight is Measure {
    method new( $value ) {                         # note[5]
        nextwith( :units<kg>, :$value );           
    }   
}

class Instrument{ 
    has Str  $.units;            # eg. 'kg', 'm/s', etc 
    has Real $.precision;        # the smallest gradation on the scale

    #| $x is the true value to artibrary precision
    method measure( Real:D $x ) {                  # note[6]
        Measure.new(
            :$!units,
            :value( $x.round: $!precision ),
            :error( $!precision / 2 ),  
        )   
    }   
}

class Scales is Instrument {
    #| these are digital scales with only 'whole' units
    method new {                                  # note[7]
        nextwith( :units<kg>, :precision(1.0) );
    }   

    method weigh(Real:D $x) { self.measure: $x }  # note[8]
}

And then the code:

my $scales = Scales.new;

my @weights = [                                   # note[9]
    $scales.weigh( 79.6 ),
    $scales.weigh( 80.3 ),
    Weight.new( 79.88 ),  
    Weight.new( 79.4 ),  
];

my @ranges = @weights>>.Range;                    # note [10]

for ^4 -> $i {                                    # note [11]
    say "the range of {~@weights[$i]} is " ~ @ranges[$i].gist; 
}

sub say-cb( $i, $j) {                            
    print "Is @weights[$i] contained by @weights[$j]?   ";  
    say @weights[$i] ~~ @weights[$j];             # note[12]
}

say-cb(2,0);                  
say-cb(3,0);

Which outputs:

the range of 80 ± 0.5 kg is 79.5..80.5
the range of 80 ± 0.5 kg is 79.5..80.5
the range of 79.88 ± 0 kg is 79.88..79.88
the range of 79.4 ± 0 kg is 79.4..79.4

Is 79.88 ± 0 kg contained by 80 ± 0.5 kg?   True
Is 79.4 ± 0 kg contained by 80 ± 0.5 kg?   False

And, now for the notes:

  1. Our Measure class carries the key attributes of a physical measurement. The method .Numeric will return just the value for example if you use with a numeric operator or prefix such as ‘+’ (addition)
  2. The method .Str fulfills a similar function when a Measure is used with a stringy operator or prefix such as ‘~’ (concatenation). Here we use it to make a nicely readable version of the measure with value, ± error and units.
  3. The method .Range is provided so that a Measure can be coerced to a Range.
  4. The method .ACCEPTS is a special method (thus the caps) that raku gives us so that a coder can customize the use of the smartmatch operator ‘~~’. We need this since Ranges use ‘~~’ to determine if one range is contained by another.
  5. Now we can make a child class Weight is Measure and here we ‘hardwire’ the Weight class to have units of ‘kg’ (yes this should be Newtons [or Pounds] really, but I am trying to follow the wikipedia example). By defining a custom .new method, we can adjust the attributes passed in to the parent Measure class and then use nextwith to call the vanilla, built in new constructor on the parent which returns the finished object. So here we can take a positional true value only. [Here you can see how cool the raku Pair type is for passing named arguments in a concise and understandable way :units<kg>, :$value]
  6. We use a similar inheritance pattern for Instrument and Scales. Here the key is the method .measure which applies the precision limitations of the Instrument to the true value and returns a real world Measure object.
  7. This is the same game we played in note [5] with a custom new constructor in this case the Scales are hardwired to units of ‘kg’ and the precision is set to 0.1.
  8. The Scales can offer method .weigh as a child specialization of the parent method .measure.
  9. — and now in the code part — you can see we can make a new Scale object and use it to weigh some true values and we can make Weight objects directly
  10. The hyper operator ‘>>’ helps us make all the Ranges in one pass.
  11. Here we now use the ‘~’ operator which calls .Str on our Measures
  12. And, in a utility function ‘say-cb()’ that saves a bit of typing you can see that we can test whether one weight contains another (or not) using the ‘“’ test and custom ACCEPTS method.

Phew!

Sorry for the code intensity of this post, but for me raku is very good at explaining itself (if you care what you write) and has some great concepts that can be dovetailed together to reflect what (in this case) wikipedia describes.

Until I did this analysis, I was wondering why Ranges needed to have Real endpoints since I had only seen them in the use case of integer or character sequences, specifically in array indexing. But the containment aspect as relates to real world numbers is now (at least to me) laid bare.

As the author of the Physics::Unit and Physics::Measure raku modules, I have taken a parallel approach to the released module code here (you are welcome to peek at the source). Look out for a future release of these with the Range semantics shown here (tuit dependent of course).

You may have tried the raku App::Crag module (calculator using raku grammars) which is a command line wrapper for these modules, something like this works today:

crag 'say (c**2 * 10kg ±1%).norm'   #898.76PJ ±8.99  via E=mc²

As feedback to the controversy, this shows where the use of operators to scale and offset Ranges is a very natural. In this case, the constant ‘c’ (speed of light) can then be used as the scale factor on the Range of the mass in kg and provide a build in way to propagate the error via interval arithmetic.

On the other hand, I am glad that the language stops here for now. We will need to override the operators for Range * Range and so on (see the continuation of the wikipedia BMI example) and I think that this would be a good fit for a new raku module.

Let me know if you would like to build it and we can collaborate so the Physics::Measure can use that rather than re-invent the wheel!

I’ll leave to the reader to comment on my deliberate errors 😉 and any other feedback is welcome!

~ lbrasteve

4 Comments

  1. librasteve says:

    I like yours – much more succinct than my ramble!

    Like

Leave a Comment