# "99 Bottles of Beer" in Roc, by Jan Van Bruggen
#
# For https://99-bottles-of-beer.net
app "sing"
packages { pf: "../roc/examples/hello-world/platform/main.roc" }
imports []
provides [main] to pf
main = List.range 0 100
|> List.reverse
|> List.map \count ->
before = Num.toStr count
after = Num.toStr (count - 1)
when count is
0 -> "No more bottles of beer on the wall, no more bottles of beer.\nGo to the store and buy some more, 99 bottles of beer on the wall."
1 -> "1 bottle of beer on the wall, 1 bottle of beer.\nTake one down and pass it around, no more bottles of beer on the wall."
2 -> "2 bottles of beer on the wall, 2 bottles of beer.\nTake one down and pass it around, 1 bottle of beer on the wall."
_ -> "\(before) bottles of beer on the wall, \(before) bottles of beer.\nTake one down and pass it around, \(after) bottles of beer on the wall."
|> Str.joinWith "\n\n"
|> Str.concat "\n"
Any other approaches? :)
maybe List.range 100 0
should Just Work here
would be a lot more efficient than reversing in place!
List.range 0 100 |> List.map (\x -> 100 - x) |> ...
:sunglasses:
you could also do a List.walkBackwards
and build up the answer list one List.append
at a time
We should benchmark this against using Task.await (Stdout.line ...)
I'd expect the List
version to be faster - fewer syscalls, and Stdout is usually buffered (on a line boundary too), so probably fewer array writes too :big_smile:
but then again, performance is often surprising in practice!
I refactored my script to make it more s c a l a b l e
- any ideas for how to gracefully merge before1
and before2
?
# "99 Bottles of Beer" in Roc, by Jan Van Bruggen
#
# For https://99-bottles-of-beer.net
app "sing"
packages { pf: "../roc/examples/hello-world/platform/main.roc" }
imports []
provides [main] to pf
main = List.range 0 100
|> List.reverse
|> List.map verse
|> Str.joinWith "\n\n"
|> Str.concat "\n"
verse = \count ->
take = "Take one down and pass it around"
more = "Go to the store and buy some more"
when count is
0 -> format "No more bottles" "no more bottles" more "99 bottles"
1 -> format "1 bottle" "1 bottle" take "No more bottles"
2 -> format "2 bottles" "2 bottles" take "1 bottle"
_ ->
stringify = \n -> n |> Num.toStr |> Str.concat " bottles"
format (stringify count) (stringify count) take (stringify (count - 1))
format = \before1, before2, action, after ->
"\(before1) of beer on the wall, \(before2) of beer.\n\(action), \(after) of beer on the wall."
(the only reason for the split is No more
vs. no more
:distraught:)
... or a bigger idea for how to do dynamic verse formatting more gracefully in general?
You could pass a function on how to transform a capitalized string "No more bottles" into the non-capitalized form, which gives the caller a similar level of freedom.
A bigger idea could be defining functions like format
for specific uses (like this one) but a record with transformations on a string inside of it. Functions that format strings could then require specific formatting functions and make the rest optional (or enable defaulting, as it's entirely possible to default some of these operations). This reduces the number of arguments and enables modular formatting functions with similar design elements. Just an idea though, no idea how ergonomic that would be
I imagine something like the following:
format :
{
toLowerPhr : Str -> Str,
toUpperPhr : Str -> Str,
replace : Map [Action, After] Str -> [Action, After] -> Str ,
# ^ The idea behind this is to mitigate the action and after
arguments and instead enable ad hoc replacements
replacements : Map [Action, After] Str
}*,
# ^ This whole record would be a Formatter and all formatting functions would use it.
Str -> Str
format = \{ toLowerPhr, toUpperPhr, replace, replacements}, str ->
"\(toUpperPhr str) of beer on the wall, \(toLowerPhr str) of beer.\n\(replace replacements Action), \(replace replacements After) of beer on the wall"
This means you pick and choose which formatting functions you want and throw out the rest. The caller provides the formatters they want and the formatter applies them where necessary. Hope this makes sense, also note I haven't done much Roc coding aside from browsing discussions here so I'm unsure how ergonomic this may or may not be. One thing i don't know how to do is enable picking and choosing of record fields from an established record (Formatter
) in the Type declaration so new formatting functions can't add new functions not present in Formatter
to the record; it may be difficult to have a la carte Open Records, if that makes sense. (Apologies for the several edits, its way too late at night lol)
Is the 'No more bottles...' thing required? There seem to be many implementations that just use `0 bottles...'
I agree, that website probably would accept 0 bottles
, but I embrace this string formatting challenge!
@Thomas Dwyer interesting, I want to try that and see how it feels!
Last updated: Jul 06 2025 at 12:14 UTC