Last active
October 29, 2015 07:06
-
-
Save Kavec/90f4a00eb045cd16387f to your computer and use it in GitHub Desktop.
An overpowered, lazy FizzBuzz using macros and Elixir's Stream module.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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