Re: Factor

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

Intersecting Ranges

Thursday, May 23, 2024

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.