Skip to content

Instantly share code, notes, and snippets.

@Kavec
Last active October 29, 2015 07:06
Show Gist options
  • Save Kavec/90f4a00eb045cd16387f to your computer and use it in GitHub Desktop.
Save Kavec/90f4a00eb045cd16387f to your computer and use it in GitHub Desktop.
An overpowered, lazy FizzBuzz using macros and Elixir's Stream module.
#!/usr/bin/env elixir
defmodule BadFizz do
# See "Other Notes" section for more on this macro.
defmacrop automate_fizz(fizzers, n) do
# To begin, we need to process fizzers to produce the various components
# we're using in the final assembly. As told by Mickens telling as Antonio
# Banderas, first you must specify a mapping function:
build_parts = (fn {fz, n} ->
ast_ref = {fz |> String.downcase |> String.to_atom, [], __MODULE__}
clist = List.duplicate("", n - 1) ++ [fz]
cycle = quote do: unquote(ast_ref) = unquote(clist) |> Stream.cycle
{ast_ref, cycle}
end)
# ...and then a reducing function:
collate = (fn
({ast_ref, cycle}, {ast_refs, cycles}) ->
{[ast_ref | ast_refs], [cycle | cycles]}
end)
# ...and then, my love, when you are done your computation is ready to run
# across thousands of fizzbuzz:
{ast_refs, cycles} = fizzers
|> Code.eval_quoted([], __ENV__) |> elem(0) # Gotta unwrap this mystery code~
|> Enum.sort(fn ({_, ap}, {_, bp}) -> ap < bp end) # Sort so that Fizz < Buzz
|> Enum.map(build_parts)
|> Enum.reduce({[], []}, collate)
# Setup the anonymous functions used by Enum.reduce to build our AST components.
# This was previously handled by List.foldl, but ejected because reduce/2's
# default behavior reduces repetition.
#
# ...I was tempted to move these into a macro themselves, thought better of it.
build_zip = fn (varname, ast) ->
quote do: Stream.zip(unquote(varname), unquote(ast))
end
build_tuple = fn (varname, ast) ->
{:{}, [], [varname, ast]}
end
build_concat = fn (varname, ast) ->
{:<>,
[context: __MODULE__, import: Kernel], # May change; accurate to Elixir 1.1.1
[varname, ast]}
end
# Toss cycles into a block by hand, then smash ast_refs into
# a few different computations on the cycle block's result.
cycles = {:__block__, [], cycles}
zip = ast_refs |> Enum.reduce(build_zip)
tuple = ast_refs |> Enum.reduce(build_tuple)
concat = ast_refs |> Enum.reduce(build_concat)
# Finally-- Now that all our components are assembled, we can put
# together the fizzbuzz stream pipeline. After quote ends, this
# block is injected into the caller's context.
quote do
unquote(cycles)
unquote(zip)
|> Stream.with_index
|> Enum.take(unquote(n))
|> Enum.each(fn {unquote(tuple), i} ->
ccats = unquote(concat)
IO.puts if ccats == "", do: i + 1, else: ccats
end)
end
end
@doc ~S"""
A fizzing, and possibly buzzing, function. Somehow, you feel like you've
seen this before. An old friend, suddenly appearing in your waking
Kafkaesque nightmares...
...or worse, during a whiteboard interview.
"""
def fizz(n \\ 100) when is_number(n) do
# In reward for all that effort above, we now have the latest in
# programmer productivity:
#
# A DSL for building arbitrary fizzing, buzzing, bazzing, and more!
[{"Fizz", 3},
{"Buzz", 5},
{"Baz", 7},
{"Qux", 11},
{"Bar", 2310}]
|> automate_fizz(n)
end
# ------------------------------ Other Notes ------------------------------ #
# It's surprisingly hard to convert a stream-based generator into an easy-to-use
# fizzing and buzzing macro. Originally I tried to use the quote/unquote approach,
# but after a lot of finicking, it was really just easier (and more legible!) to
# roll the AST by hand.
# (With the caveat that I'm using a map and four reduce functions. They're pretty
# mathy [and also super cool].)
#
# Our original stream implementation looked like this:
# def fizz(n \\ 100) when is_number(n) do
# import Stream, only: [cycle: 1, zip: 2, with_index: 1]
# import Enum, only: [take: 2, each: 2]
# dup = &List.duplicate/2
#
# fizz = dup.("", 2) ++ ["Fizz"] |> cycle
# buzz = dup.("", 4) ++ ["Buzz"] |> cycle
# zip(fizz, buzz)
# |> with_index
# |> take(n)
# |> each(fn {{fizz, buzz}, i} ->
# IO.puts if fizz <> buzz == "", do: i+1, else: fizz <> buzz
# end)
# end
#
# And (with just Fizz and Buzz) the code here generates this:
# (
# (
# buzz = ["", "", "", "", "Buzz"] |> Stream.cycle()
# fizz = ["", "", "Fizz"] |> Stream.cycle()
# )
# Stream.zip(fizz, buzz) ## Newlines here added for clarity, not present in
# |> Stream.with_index() ## generated output.
# |> Enum.take(n)
# |> Enum.each(fn {{fizz, buzz}, i} ->
# ccats = fizz <> buzz
# IO.puts(if(ccats == "") do
# i + 1
# else
# ccats
# end)
# end)
# )
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment