Re: Factor

Factor: the language, the theory, and the practice.

Intersecting Ranges

Thursday, May 23, 2024

#language #math

After Factor 0.99, we have continued to add new features to what is likely to be an upcoming release. One of those that I would like to discuss today was a patch to add set methods to ranges that was contributed by Keldan Chapman. It was a really nice addition that makes our ranges support efficient set operations.

And that brings us to the topic of today’s blog post: intersecting ranges.

Specifically, a discussion that occurred over code style and I thought it would be interesting to see 3 different approaches to writing a reasonably complex intersecting ranges word in Factor.

You can see this in action, where intersect returns a range in the latest development version:

! Factor 0.99
IN: scratchpad 2 40 2 <range>
               1 40 3 <range> intersect .
V{ 4 10 16 22 28 34 40 }

! Factor 0.100 (git)
IN: scratchpad 2 40 2 <range>
               1 40 3 <range> intersect .
T{ range { from 4 } { length 7 } { step 6 } }

Some inspiration came from looking at the Julia programming language and their intersect function.

Local Variables

The first version used local variables and after a smidge of cleanup, looked pretty good:

:: intersect-range ( range1 range2 -- range3 )
    range1 empty? range2 empty? or [ empty-range ] [
        range1 >forward-range< :> ( start1 stop1 step1 )
        range2 >forward-range< :> ( start2 stop2 step2 )
        step1 step2 gcd :> ( x g )
        start1 start2 - g /mod :> ( z y )
        y zero? not [ empty-range ] [
            start1 x z step1 * * - :> b
            step1 step2 lcm :> a
            start1 start2 [ b over - a rem + ] bi@ max :> m
            stop1  stop2  [  dup b - a rem - ] bi@ min :> n
            m n a <range>
        ] if
    ] if ;

Stack Shuffling

Due to a bug with using local definitions in a bootstrap image, we temporarily switched to a version that used stack shuffling. As you can see – and is often the case when you are dealing with a large number of items on the stack – it isn’t particularly readable.

: intersect-range ( range1 range2 -- range3 )
    2dup [ empty? ] either? [ 2drop empty-range ] [
        [ >forward-range< ] bi@ [ -rot ] [ swap ] [ ] tri* [
            [ reach reach - ] 2dip gcd swap [ /mod ] dip swap
        ] 2keep rot zero? not [
            4drop 4drop empty-range
        ] [
            [ reach ] 4dip dupd lcm [ * * - ] dip [
                [ '[ [ _ over - _ rem + ] bi@ max ] 2dip ]
                [ '[ dup _ - _ rem - ] bi@ min ] 2bi
            ] keep <range>
        ] if
    ] if ;

Small Words

Usually when stack shuffling doesn’t help, sometimes extracting out pieces of logic into words and naming them can often result in a simpler or cleaner version. It did turn out a bit easier to read, but is still overly shuffly.

: explode-ranges ( range1 range2 -- start1 start2 stop1 stop2 step1 step2 )
    [ >forward-range< ] bi@ [ -rot ] [ swap ] [ ] tri* ;

: compute-z-y-x ( start1 start2 step1 step2 -- z y x )
    gcd [ - ] 2dip swap [ /mod ] dip ;

: compute-b-a ( start1 x z step1 step2 -- b a )
    dupd lcm [ * * - ] dip ;

: intersected-range ( start1 start2 stop1 stop2 b a -- range )
    [
        [ '[ [ _ over - _ rem + ] bi@ max ] 2dip ]
        [ '[ dup _ - _ rem - ] bi@ min ] 2bi
    ] keep <range> ;

: intersect-range ( range1 range2 -- range3 )
    2dup [ empty? ] either? [ 2drop empty-range ] [
        explode-ranges [
            [ reach reach ] 2dip compute-z-y-x swap
        ] 2keep rot zero? not [
            4drop 4drop empty-range
        ] [
            [ reach ] 4dip compute-b-a intersected-range
        ] if
    ] if ;

Well, likely the first version was the nicest, but it’s an interesting exercise to think about readability, verbosity, and concatenative thinking. Sometimes you need to puzzle through a piece of code to find the one you like the best.