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 discussions on Hacker News and on Lobsters. 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()

Someone 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

However, it would be better if we only check against zero if the first check passes:

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

And, despite still being three passes, it is better if we only check negative if the positive check fails:

[| line |
    line [ abs 1 3 between? ] all? [
        line [ 0 > ] all? [ t ] [
            line [ 0 < ] all?
        ] if
    ] [ f ] if
] count

We can do these short-circuiting checks using a short-circuit combinator:

[
    {
        [ [ abs 1 3 between? ] all? ]
        [ { [ [ 0 > ] all? ] [ [ 0 < ] all? ] } 1|| ]
    } 1&&
] count

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

[| line
    line empty? [ t ] [
        line [ abs 1 3 between? ] all?
        line unclip sgn '[ sgn _ = ] all? and
    ] if
] count

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

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

We often encourage writing combinators to do algorithmic things:

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

Which makes for a satisfyingly simple version:

[ [ { [ 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 {
            { [ 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 { 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?