Implicit over Explicit - maybe?
Published on
Hello once again, I have been away too long. Between work, experiments in Zig, writing Racket code for work, and mostly goofing off fielding new projects, I am back again with yet another blog post. This time about the hottest topic in all computer science - for loops.
I was writing some code in Racket earlier, and it started bugging me about why I was doing things one way when I should really maybe consider doing it in a different way. There are a ton of different ways of looping Racket, all which boil down to being very similar, but in this case it... sort of got to me, I guess.
For the longest time I have been the proponent of using simple tools to get the job done. I stick to map
, filter
, and for-each
and I try not to make things too overly complex. Sometimes foldl
for advanced types of iteration as well.
In a new project I am working on, I wrote a for-each
expression that made me do a double-take and think "wait a minute".
(for-each
(λ (town)
(define name (town-name town))
(define state (town-state town))
(displayln (format "~a is in ~a" name state)))
Towns)
I map a function over a list of towns (pretend it's a two-value struct) and print out the town name and American state it belongs to.
But... it can also be written like this.
(for ([town Towns])
(define name (town-name town))
(define state (town-state town))
(displayln (format "~a is in ~a" name state)))
There's a two line reduction in this alone. The above for-each
was kind of sloppy because the data list is dangling on it's own, which doesn't look very appealing and can get lost very easily in code.
Granted, a for
macro isn't as appealing looking when doing simple one-liners, simply because it boils back down into an explicit style call.
(for ([x (range 10)]) (displayln x))
; versus
(for-each displayln (range 10))
Note that we have to bind the symbol for each entity versus for-each
which acts as the point-free style function that we have come to love back in one of my many posts. Point-free style rules, but in some cases it may not be as awesome.
The cool stuff is that you can use point-free functions like for-each
and map
and such in ways that enables you to combine multiple functions. But sometimes you don't really want to combine or write separate definitions, only because code would then start looking scrambled across multiple files. Sometimes it pays off when application logic exists in a loop so you can better keep track of it.
(define (program-loop)
(for ([job (get-jobs)])
(if (not (job-complete? job))
(do-job job)
(displayln (format "~a is complete" job)))))
Here the logic is contained, but in an uncontained for-each
edition:
(define (app-logic job)
(if (not (job-complete? job))
(do-job job)
(displayln (format "~a is complete" job))))
(define (program-loop)
(for-each app-logic (get-jobs)))
It almost seems... redundant to have to split up all this logic. You define one function to work on something, then another to work across a range of somethings. The for
macro version is appealing because it looks like any other language for the most part, while this version is kind of... not the prettiest.
My issue with for-each
, and frankly most of the Racket library, is that sometimes I feel the arguments for things are completely backwards. Literally, when I say out loud for each displayln things
, it doesn't really make sense. It would make more sense to say for each things displayln
, as that converts to "for every thing in things, do displayln". The other way makes less sense, "for each displayn, thing". Rearrange that all you want, it's still not the most appealing format.
Which is why the for
macro makes more sense, because it follows English almost one-for one.
; For every fruit in a fruit stall
; Print out the name of the fruit
(for ([fruit (get-fruits-in-stall)])
(displayln (fruit-name fruit)))
This is what I would say is good code, because it's easy to describe your algorithm in plain English, then write it out in a way that follows plain English almost perfectly. Doing something like this the other way results in more heavier, thicker code.
; For every fruit in a fruit stall,
; Print out the name of the fruit
(for-each (compose displayln fruit-name)
(get-fruits-in-stall))
Here we have to compose the displayln
and fruit-name
functions together to make it more seamless. But still, notice how it doesn't flow as nicely? I blame that on the Racket library's position of arguments.
If we switched it around, we would get very close to something more human readable.
; our for-every implementation
(define (for-every listof-things func)
(if (empty? listof-things)
(void) ; for-each returns no value
(begin
(func (car listof-things))
(for-every (cdr listof-things) func))))
; now it seems a bit more human readable
(for-every (get-fruits-in-stall)
(compose displayln fruit-name))
; Bonus: the *smart person* rewrite
(define (for-every listof-things func)
(for-each func listof-things))
A lot of things in the Racket standard library end up being this way for me. But I think somewhere on the road they had to pick a standard and stick with it, even if it leads to some ugly code.
The tricky part of any iteration is when you want to get extra variables involved. Let's say you have a list of three numbers that you wish to multiple by another list of three numbers. Representing this in Racket can be a pain without the for
macro, as the for
macro provides us some cool utilities for writing these out.
(for ([x (list 1 2 3)]
[y (list 4 5 6)])
(displayln (format "~a x ~a = ~a"
x y (* x y))))
; output
; 1 x 4 = 4
; 2 x 5 = 10
; 3 x 6 = 18
The cool part is that this advances both x
and y
through their bindings both incrementally, it does not iterate for every single combination of pairs of x
and y
.
Doing this with for-each
requires you writing it in a format that may not be super appealing.
(for-each
(lambda (pair)
(define x (car pair))
(define y (cdr pair))
(displayln (format "~a x ~a = ~a"
x y (* x y))))
'((1 . 4) (2 . 5) (3 . 6)))
; output
; 1 x 4 = 4
; 2 x 5 = 5
; 3 x 6 = 18
The difference is that for
is able to generate on-the-fly iterator-like bindings, whereas using for-each
, doing something like this would technically be impossible.
(for ([x '(1 2 3)]
[y '(4 5)])
(displayln (* x y))
; output
4
10
Because of the uneven length of both x
and y
, the loop short-circuits when there are no more possible states that can be created using those two lists. The above code where we created pairs won't work, because there's no way we can attach a null value to a pair and say "stop here". A for-each
wants to iterate over every single element, there's no bail-out clause in effect. So this kind of code is technically impossible to recreate using for-each
, unless you went to great lengths to do some crazy kind of shit.
for
macro is definitely an important step-up in terms of writing code. The ability to bind variables, create iterators, and can even collect results using for/list
, simplify application code, there's a lot of advantage to using a for
macro in place of regular list traversal functions.
; a multiplication table for fun
(for ([x (range 1 13)])
(for ([y (range 1 13)])
(display (format "~a " (* x y))))
(displayln ""))
; output:
; 1 2 3 4 5 ...
; 2 4 6 8 10 ...
; 3 6 9 12 15 ...
for
loops are pretty synonomous across all programming languages and even native human languages, but they are certainly a key way of understanding what computers do. Heck, it's an important concept even in mathematics, the idea of "for-every" with the ∀ symbol if you've ever seen it.
My early Racket days it was never clear why for
loops were used, as I'm not used to seeing a lot of Racket code from others on a regular basis, but I enjoyed discovering the power of it now. Anyways, that's all I had for now. Time to go re-write some code using for
instead of for-each
(where it makes sense to, at least).