Re: Factor

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

Left to Right

Tuesday, August 19, 2025

#language

An article about Left to Right Programming was posted a few days ago with a good discussion on Hacker News. It’s a nice read with some syntax examples in different languages looking at some code blocks that are structured left-to-right or right-to-left.

We can look at a few of the shared examples and think about how they might look like naturally in Factor, which inherits a natural data flow style due to the nature of being a concatenative language.

The Challenge

The blog post discusses Graic’s 2024 Advent of Code solution, written in Python:

len(list(filter(lambda line: all([abs(x) >= 1 and abs(x) <= 3 for x in line]) and (all([x > 0 for x in line]) or all([x < 0 for x in line])), diffs)))

And compares it to an equivalent improved form in JavaScript:

diffs.filter(line => 
    line.every(x => Math.abs(x) >= 1 && Math.abs(x) <= 3) &&
    (line.every(x => x > 0) || line.every(x => x < 0))
).length;

There’s nothing quite like syntax wars – the nerd version of the linguistic wars – to get people interested in a topic. It is only one dimension, but perhaps the most visible one, to evaluate a programming language on.

I usually get excited for any code that solves a problem, and I give kudos to Graic for their efforts solving the Advent of Code! It’s often only working code that we can make iterative improvements upon, and that should be appreciated.

The Response

On Hacker News, someone shared a version using Python’s list comprehensions:

len([line for line in diffs
     if all(1 <= abs(x) <= 3 for x in line)
     and (all(x > 0 for x in line) or all(x < 0 for x in line))])

As well as a direct translation in Rust:

diffs.iter().filter(|line| {
    line.iter().all(|x| x.abs() >= 1 && x.abs() <= 3) &&
    (line.iter().all(|x| x > 0) || line.iter().all(|x| x < 0))
}).count()

And a single pass version in Rust with improved performance:

diffs.iter().filter(|line| {
    let mut iter = line.iter();
    let range = match iter.next() {
        Some(-3..=-1) => -3..=-1,
        Some(1..=3) => 1..=3,
        Some(_) => return false,
        None => return true,
    };
    iter.all(|x| range.contains(x))
}).count()

Some else shared a version using numpy arrays:

sum(1 for line in diffs
    if ((np.abs(line) >= 1) & (np.abs(line) <= 3)).all()
       and ((line > 0).all() or (line < 0).all()))

And another comment shared a version in Kotlin:

diffs.countIf { line -> 
    line.all { abs(it) in 1..3 } and ( 
        line.all { it > 0} or
        line.all { it < 0}
    )
}

There was also a shared version in Python perhaps a bit more idiomatic:

sum(map(lambda line: all(1 <= abs(x) <= 3 for x in line)
                     and (all(x > 0 for x in line) or all(x < 0 for x in line)),
        diffs))

What about Factor?

As you might imagine, I was also curious about what this would look like in Factor.

Directly translating using local variables does up to three passes through the line:

[| line |
    line [ abs 1 3 between? ] all?
    line [ 0 > ] all?
    line [ 0 < ] all? or and
] count

Checking that the sign of all the numbers are the same does two passes through the line:

[| line |
    line [ abs 1 3 between? ] all?
    line [ [ sgn ] same? ] monotonic? and
] count

Using a short-circuit combinator prevents the second check if the first one fails:

[
    {
        [ [ abs 1 3 between? ] all? ]
        [ [ [ sgn ] same? ] monotonic? ]
    } 1&&
] count

Comparing the first value to subsequent values does a single pass through the line:

[
    [ t ] [
        unclip-slice { [ abs 1 3 between? ] [ sgn ] } 1&& [
            '[ { [ abs 1 3 between? ] [ sgn ] } 1&& _ = ] all?
        ] [ drop f ] if*
    ] if-empty
] count

But, we often encourage factoring code into reusable small blocks:

: valid? ( value -- sgn/f )
    { [ abs 1 3 between? ] [ sgn ] } 1&& ;

[
    [ t ] [
        unclip-slice valid? [ '[ valid? _ = ] all? ] [ drop f ] if*
    ] if-empty
] count

But, we also often encourage writing combinators to do algorithmic things:

:: all-same? ( seq quot: ( elt -- obj/f ) -- ? )
    seq [ t ] [
        unclip-slice quot call
        [ '[ quot call _ = ] all? ] [ drop f ] if*
    ] if-empty ; inline

[ [ { [ abs 1 3 between? ] [ sgn ] } 1&& ] all-same? ] count

We could even do something like the Rust version above, getting the endpoints from the first value to check that the subsequent ones match:

[
    [ t ] [
        unclip-slice {
            { [ dup 1 3 between? ] [ drop 1 3 ] }
            { [ dup -3 -1 between? ] [ drop -3 -1 ] }
            [ drop f f ]
        } cond [ '[ _ _ between? ] all? ] [ nip ] if*
    ] if-empty
] count

And that simplifies even more if we use range syntax:

[
    [ t ] [
        unclip-slice
        { 1 ..= 3 -3 ..= -1 } [ in? ] with find nip
        [ '[ _ in? ] all? ] [ drop f ] if*
    ] if-empty
] count

As usual, there is more than one way to do it, and that’s okay.

Are any of these best? How else might we write this better?