- fun hacks
When I couldn’t fall asleep last night 1 my sleep-deprived brain suddenly went “I bet you can’t write a Makefile for FizzBuzz.” While I obviously should have responded with Homer Simpson’s classic “Shut up, brain, or I’ll stab you with a Q-tip.”, it was too late, I had already nerd sniped myself. I fully expected to waste way too much time on this but it only (?) took a little over an hour to come up with a solution.
#The Makefile
Without further ado, here’s the thing that should probably never have happened:
START := 1
END := 15
NUMS := $(shell seq $(START) $(END))
fizzbuzz: $(NUMS)
mod = $(shell echo $$(( $1 % $2 )))
define fb
$1:
ifeq "$(call mod, $1, 15)" '0'
@echo FizzBuzz
else ifeq "$(call mod, $1, 3)" '0'
@echo Fizz
else ifeq "$(call mod, $1, 5)" '0'
@echo Buzz
else
@echo $1
endif
endef
$(foreach _,${NUMS},$(eval $(call fb,$_)))
.PHONY: fizzbuzz $(NUMS)
Let’s see it in action:
$ make fizzbuzz
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
Since fizzbuzz is the first target we can leave it off. And we can also override START and END from the command line:
$ make START=19 END=30
19
Buzz
Fizz
22
23
Fizz
Buzz
26
Fizz
28
29
FizzBuzz
#Walkthrough
So how does this work? We start by defining a bunch of variables for the start and end values of the number range we want to check:
START := 1
END := 15
Next we use the shell function to construct a sequence of numbers with seq, so NUMS will contain 1 2 3 [...] 14 15.
Now we can set up a fizzbuzz target and list all the numbers in NUM as prerequisites:
fizzbuzz: $(NUMS)
Next up, a little helper function for modulo, since make itself doesn’t support arithmetic operations. This uses the shell’s arithmetic expansion mechanism and does not rely on external programs like bc.
mod = $(shell echo $$(( $1 % $2 )))
Now for the fun part :-) First, we use define to, well, define a sequence of make expressions that we will later eval to turn them into targets:
define fb
$1:
ifeq "$(call mod, $1, 15)" '0'
@echo FizzBuzz
else ifeq "$(call mod, $1, 3)" '0'
@echo Fizz
else ifeq "$(call mod, $1, 5)" '0'
@echo Buzz
else
@echo $1
endif
endef
The code should be relatively straightforward: we refer to this block of statement as fb when calling it and $1 will receive the value of the first argument. Then we do a couple of ifeq checks 2
Now we can use foreach in combination with eval and call to define targets for all the individual numbers:
$(foreach _, ${NUMS}, $(eval $(call fb, $_)))
Last but not least, let’s declare all targets as phony to indicate that they don’t relate to real files.
#Summary
While I probably need better hobbies, this was a fun way to kill some time and I definitely learned a bit more about make today. When a friend (hi, Joe 👋) asked me why I did this, my answer was clear: “Because I could.” Hack on and don’t forget to have fun with technology!
#Updates
- This being the Internet (see xkcd #386), it was brought to my attention that “[this] relies on a specific and nonportable shell builtin.” You must be fun at parties. Anyway, let’s look into this a bit:
make‘s default shell is/bin/sh. It’s true thatseqis not part of POSIX, but I tried thisMakefileon macOS, NixOS, Arch Linux, and OpenBSD (where you also need topkg_add gmake) and it worked on all of them. It also works when explicitly setting the shell to Bash, ZSH, and OpenBSD’s version of ksh. That’s portable enough for me. In all fairness though, it did NOT work with Fish, though not because ofseq(which is available) but because the POSIX-style arithmetic is not supported. - User “GeoffWozniak” on lobste.rs joined in the fun and made a version that doesn’t rely on the
shellfunction at all. That’s the spirit!
Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.