Semigroup Resonance FizzBuzz
blog.ploeh.dk
blog.ploeh.dk
from itertools import cycle
fizz = cycle(['', '', 'Fizz'])
buzz = cycle(['', '', '', '', 'Buzz'])
for n in range(1, 100):
fizzbuzz = next(fizz) + next(buzz)
print (fizzbuzz if fizzbuzz else n) <T> Iterator<T> cycle(List<T> list) {
return IntStream.iterate(0, x -> (x + 1) % list.size()).mapToObj(list::get).iterator();
}
<T, U, V> Iterator<V> zip(Iterator<T> it1, Iterator<U> it2, BiFunction<T,U,V> merger) {
return Stream.generate(() -> merger.apply(it1.next(), it2.next())).iterator();
}
var fizz = cycle(List.of("Fizz", "", ""));
var buzz = cycle(List.of("Buzz", "", "", "", ""));
var ints = IntStream.iterate(0, x -> x + 1).boxed().iterator();
var fizzbuzz = zip(
zip(fizz, buzz, String::concat),
ints, (f, i) -> f.isEmpty()? "" + i: f);
Stream.generate(fizzbuzz::next).skip(1).limit(100)
.forEach(System.out::println); from itertools import cycle
fizz = cycle([lambda x: ''] * 2 + [lambda x: 'Fizz'])
buzz = cycle([lambda x: ''] * 4 + [lambda x: 'Buzz'])
numb = cycle([lambda x: x] * 2 + [lambda x: '', lambda x: x, lambda x: ''])
for n in range(1, 100):
print(next(fizz)(n) + next(buzz)(n) + next(buzz)(n))That's my version to make it more "functional":
from itertools import cycle, islice
from operator import add
fizz = cycle(['', '', 'Fizz'])
buzz = cycle(['', '', '', '', 'Buzz'])
strings = map(add, fizz, buzz)
strings_or_nos = (s or i for i, s in enumerate(strings, start=1))
print(list(islice(strings_or_nos, 100))) from itertools import islice, cycle
print("\n".join(islice((
f + b if f or b else str(i+1)
for i, (f, b) in enumerate(
zip(cycle(["", "", "Fizz"]), cycle(["", "", "", "", "Buzz"]))
)), 100)))https://gist.github.com/kbob/f46776fdc950e6a948b04c76bc23e87...
https://projecteuler.net/problem=1
> If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.
> Find the sum of all the multiples of 3 or 5 below 1000.
I tried dual iterators at first but it turned out to be more elegant to just generate the multiples directly using the sequence:
3 2 1 3 1 2 3
It turn out that the differences between successive multiples always form a palindrome.http://joypy.osdn.io/notebooks/Developing.html
(In re: catamorphism et. al. see http://joypy.osdn.io/notebooks/Recursion_Combinators.html Cheers!)
Using (clojure.string/join “\n” coll) would be more idiomatic. And there’s probably a concise way to leave the nils in the sequence alone instead of mapping str—that would change the “if” into an “or”, which is closer to the original solution.
(defn fizzbuzz [n]
(doall (map
#(println (if (seq %1) %1 %2))
(map str (cycle [nil nil "fizz"])
(cycle [nil nil nil nil "buzz"]))
(range 1 (inc n))))
nil)Both solutions seem to deal with group theory to basically the same extent.
fib = 0 : 1 : zipWith (+) fib (tail fib)
Indeed, the way the author has written it would seem a little strange to most Haskell programmers, I think, who would just write (in this idiom): mapIdx f xs = map (uncurry f) $ zip [1..] xs
fizz = ["", "", "Fizz"] ++ fizz
buzz = ["", "", "", "", "Buzz"] ++ buzz
fizzBuzz = mapIdx f fbs
where f n "" = show n
f _ v = v
fbs = zipWith (++) fizz buzz
main = mapM_ putStrLn $ take 100 $ fizzBuzz
Though usually you would just write it a more typical way f :: Int -> String
f x | x `mod` 3 == 0 && x `mod` 5 == 0 = "FizzBuzz"
| x `mod` 3 == 0 = "Fizz"
| x `mod` 5 == 0 = "Buzz"
| otherwise = show x
main :: IO ()
main = mapM_ (putStrLn . f) [1..100]
But, yes, I don't find the semigroup connection particularly illuminating.I mean they ditectly put into the source code that the pattern is "", "", "fizz", etc..., and repeat (in a lazy fashion), instead of having the computer calculate which integers give a fizz/buzz. So the programmer is in a sense precalculating at the time of writing the program, instead of having the computer calculate the result of the if statements at runtime.
buzzes = cycle [Nothing, Nothing, Nothing, Nothing, Just "Buzz"]
You could instead write: buzzes = cycle $ replicate 4 Nothing <> [Just “buzz”]
It’s not a big difference though. Since everything is lazily evaluated, you’re not precalculating anything.Edit: for clarity, the reason i think this is kind of a cop out, is that in the article the author seems to call out not using if-then-else as a benefit of this solution, compared to the normal version that takes a list of integers and transforms it using if/then/else and mod operations. But i dont really think it counts as not using if/then/else if you literally do the same operation in your head and then just write down the transformed list explicitly, to save the computer the effort of transforming the list at runtime for you.
Your objection is confusing everyone else because these are both equally "calculations", and the meaning of the "divided evenly by three" calculation and the meaning of the FFTFFTFFT... sequence are the same, so it just two ways of expressing the same thing. Either way is just as "precalculated" as the other.
People who haven’t studied abstract algebra aren’t used to thinking of it this way though. To most people, the modulus operator is specifically an operator that returns the remainder of Euclidean division. To think of it as a bunch of equivalence classes that split up the integers is not something most people think of right away.
[1] https://www.amazon.com/Abstract-Algebra-3rd-David-Dummit/dp/...
To put it another way, the original version was explicitly calculating the function ℤ -> { "", "fizz" }. The new version had the results of this function directly embeded in the source code (and hence precalculated as opposed to calculated at runtime). I just don't see the two of these approaches really being all that different
i mod 3 and i mod 5 are just as much embedding the answer in the source code. Saying that the unrolled sequence embeds the answer more than the modulus is the same as saying that
for (i = 0; i < 10; i++)
does not embed the answer to iterating over the integers 0 to 9, but for (auto i : views::iota(0, 10))
does embed it. I don't think anyone would regard those as anything but equivalent, though.Also with a little more sympathetic eye, i suppose the new version is more evocative of the underlying group and such representations are much more in line with the mores of functional programming. At the same time, i cant help but think its a bit pretentious to talk about being isomorphic to a cyclic group and otherwise evoke high level math, just to say, you know this program whose output by definition repeats in a very obvious pattern, well guess what, its output is cyclic.
If you look at it as taking something simple and making it needlessly complicated, then it seems pretentious, but if you look at it as taking very abstract concepts and making them more understandable with a concrete, well-known example, it's not.
This might give you a good idea of where the author is coming from and trying to accomplish:
https://blog.ploeh.dk/2017/10/04/from-design-patterns-to-cat...
I've been telling people that design patterns were algebras glimpsed darkly for years, but it's nice to see someone finally write that up.
Another interesting way would be to not use any modulo operation, but only do string processing and substitution on the numbers to check their divisibility (easier for mod 5, a bit harder for mod 3 but not impossible)
numberString = "1578465465464570"
divFive = /[05]$/.test(numberString);
// Check if the digits of the number sum up to 0, 3, 6, or 9. If so it is divisible by 3.
while(numberString.length > 1)
{
total = 0;
for (const digit of numberString) {
total += parseInt(digit, 10);
}
numberString = total.toString(10);
}
divThree = /[0369]/.test(numberString)
divFifteen = divThree && divFiveReduce the number using string replacements based on patterns (for example, all digit pairs that sum to 0 mod 3 can be removed). The order of the digits doesn't matter.
One can do this as follows (edit: spot the bug; this algorithm is broken, but can be easily fixed):
- Take the number as a string : "13565172345474789047"
- remove all 0s, 3s, 6s, and 9s: “155172454747847”
- replace all 4s and 7s by 1s : "155112151111811"
- replace all 5s and 8s by 2s : "122112121111211"
- sort the string’s characters : "111111111122222"
- replace all ‘111’ by ‘’ : "122222"
- replace all ‘222’ by ‘’ : "122"
- replace ’12’ by ’’ repeatedly: "2"
(you have to do this at most two times)
(proving that none of these steps changes the sum of the digits of the number (mod 3) is easy, as each step changes the sum by a multiple of 3)⇒ that number is 2 (mod 3)
[1]: https://en.wikipedia.org/wiki/Divisibility_rule#Divisibility...
Though maybe the two approaches can be combined
console.clear();
/** naive python like range in JS */
function* range (beg, end) {
for(let i = beg; i < end; ++i) {
// stringify the number
yield '' + i;
}
}
function buzz (stringNum) {
const lastChar = stringNum.charAt(stringNum.length - 1);
return lastChar === '5' || lastChar === '0';
}
const mapping = [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1];
function fizz (stringNum) {
return (
stringNum
// get each character of the string
.split('')
// find out if total digits are divisible by three.
.reduce((acc, x) => (acc + mapping[x]) % 3, 0) === 0
);
}
function fizzBuzz (stringNum) {
const f = fizz(stringNum), b = buzz(stringNum);
if(f && b) {
return 'FizzBuzz';
} else if (f) {
return 'Fizz';
} else if (b) {
return 'Buzz';
} else {
return stringNum;
}
}
const nums = [...range(1,1001)];
const result = nums
// main algo
.map(fizzBuzz)
console.log(result.join('\n'));
EDIT: sorry I didn't see your other reply below another comment. I now see this does not fulfill the requirements.EDIT: hmm, unzip is probably the wrong term here.
The words catamorphism, semigroup and monoid barely make sense to most programmers.
Another angle to view it is what if I told you that you could eliminate an entire class of errors by employing this abstraction? Learn Option once and then never have to deal with NPE's. What about errors due to recursion? Would it be appealing to avoid those errors forever? Concurrency, state, error handling, all things that have appealing abstractions.
If the idea of mathematical connections to seemingly unrelated things appeals to you, or you are tired of dealing with the same bugs in different forms then these abstractions seem very appealing. The problem is that it's a technical solution to a human problem. That doesn't inherently make it bad, but it does imply a different set of tradeoffs.
I think it's not too far from practicality to describe the solution in OP as along the lines of "solving fizzbuzz by combining infinite streams". It's a quirky/cute solution.
I think there's a jump from "knowing these functions / structures can be useful" and "let's use terms like 'semigroup resonance' and 'catamorphism'".
Names like StateT and Reader can surely be improved but the names taken directly from mathematics are already the best we can do imo.
However, I completely agree that the word "functor" is useful for exactly the reason you imply. It's a kind of special container. It doesn't work exactly the way you imagine containers should work initially. So I guess it's 6 of 1 half a dozen of the other. I just wanted to speak out because I know there are others like me that find reasoning about functors as if they are containers very useful.
Over the last few years I have absorbed a lot of these terms via professional Haskell development and have come to the conclusion that it is quite unfortunate that most of us tend to shy away from this unfamiliar vocabulary. It turns out these are very precise (mathematical) terms which describe common things we see every day in programming.
Addition, multiplication, and string concatenation are all monoids (described as: an associative binary operation with an id element). This might not seem useful to know on the surface, however, once you discover that you can write generic functions over any monoid you can start eliminating large classes of errors that tend to turn up in software development due to the DRY principle.
> An extreme attempt to simplify?
It turns out that the more generic you make something, the harder it is to do the wrong operations on it.
id :: a -> a
If 'a' is a generic type value for all possible types, is it possible to return anything other than the initial value passed in?A family man might read a book about FP patterns to solve common issues, agile, QS+testing, concurrency, etc (actually seen tgese read by 50yo collegues) - but not a single one has a category theory book on their table.
Its a question of time invested vs benefits to current occupation.
FP terminology is the domain of the young and the driven, who also are of academic age. (Probably winy find any junior-high kids dabbling with semigroups anytime soon)
The linguistics issue here is real and should be addressed or we will end up into two camps, and one will eventually give way to the other.
And math-hardcorers be warned, Life prefers the simple.
My take is you should know all these, and choose the appropriate level for what you're doing. You don't need to crank it up to 13 everywhere just because it's there. I spend most of my time (web apis, business logic) around level 7. If you dial the abstraction level too far up, not only does the code seem confusing, but it's also easy to paint yourself into an abstraction corner, where a new requirement comes in that breaks the abstraction, so you end up either tearing it down and rebuilding it completely, or building up a terrible "spaghetti abstraction" which is the worst kind of spaghetti. It's a bit ironic to think that abstractions are exactly what's supposed to prevent you from being painted into a corner, but it can happen easily. (https://www.sandimetz.com/blog/2016/1/20/the-wrong-abstracti... is a good reference).
When writing things like compilers, the abstraction level can comfortably go higher. In fact it should. It makes challenging logic far easier to reason about and more consistent to specify and implement when you can think about it mathematically. Other things like rules engines and such can live comfortably in between the two. Conversely when writing timing-critical device code then the appropriate level of abstraction is much lower. A good reference for this style is the k8s PV code. Note the comments starting on line 55 (tldr: DO NOT SIMPLIFY THIS CODE) https://github.com/kubernetes/kubernetes/blob/ec2e767e593953...
So as developers we have to determine what the appropriate level of abstraction should be for our use cases. In fact I'd offer that's one of the most important parts of software design. But it's important to know all of them so we can choose accordingly.
The haskell model is much more intuitive and productive for me than the weird oo-esque models of “usual” languages like python, c++, and java.
I’m familiar with the theory behind popular styles like OO and have used them professionally but ultimately I find Haskells incredibly simple and constrained type system (closed data types, no subtyping, essentially total inference, purity, etc) easier to think in than the Wild West of python or java, say, since there are fewer modeling choices to make.
The gist is - by having many simple-but-powerful tools along with many simple-but-powerful ways of composing such tools, it makes programming real problems way less intense. The cost of course is the learning curve. So I think of it has shifting variable cost of development to fixed costs upfront.
The end result in my experience is being able to handle more complexity than otherwise (good for side-projects.) Or to handle the same complexity with less time and effort (good for FT engineering...to make room for said side-projects.)
(Those terms actually have very general and simple definitions that I've seen interns learn in one teaching session. But they are infinitely useful problem solving techniques that I use all the time.)