
Semigroup Resonance FizzBuzz - lelf
https://blog.ploeh.dk/2019/12/30/semigroup-resonance-fizzbuzz/
======
jamesdhutton
Nice idea. Here is the (rough) equivalent in Python.

    
    
      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)

~~~
kortilla
Conditionals are so 2019.

    
    
      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))

~~~
inetknght
tbqh that's a lot less readable and a lot less intuitive

~~~
kortilla
Yeah, not really going for readable. Just an exercise in eliminating the
conditional.

------
carapace
Reminds me of the time I was trying the first problem of Project Euler,
“Multiples of 3 and 5” in Joy.

[https://projecteuler.net/problem=1](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](http://joypy.osdn.io/notebooks/Developing.html)

(In re: catamorphism et. al. see
[http://joypy.osdn.io/notebooks/Recursion_Combinators.html](http://joypy.osdn.io/notebooks/Recursion_Combinators.html)
Cheers!)

------
moomin
Nice article, but I’d point out that “the maybe catamorphism” is fundamentally
just an if statement in disguise. You could, if you were feeling particularly
extra, use the Foldable instance...

------
filoeleven
Here’s an okay Clojure version.

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)

------
bawolff
Its kind of begging the question in a way. The only reason you can conclude
this is related to Z3 and Z5 is because the original problem statement said to
repeat fizz every 3 numbers, and thus its related to cycluc groups. But the
only aspect of group theory used seems to be that Z3 repeats cyclicly (which
was the fact we used to invoke group theory in the first place). So it seems
rather circular.

------
bawolff
I guess i dont see what the fuss is. Instead of looping through the numbers,
they precalculate the answers and loop through that instead. Personally this
seems about equivalent of replacing all instances of 2+2 in your program with
just 4, and then claiming the program doesnt use addition.

Both solutions seem to deal with group theory to basically the same extent.

~~~
madhadron
They actually don't precalculate, since Haskell is lazy. This kind of
construction of the infinite data structure and relying on laziness is a
really typical trick in Haskell. For example:

    
    
        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.

~~~
bawolff
> They actually don't precalculate, since Haskell is lazy.

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.

~~~
chongli
They’re only doing it once though, not for every multiple of 3 or 5. So if you
didn’t like the line:

    
    
        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.

~~~
bawolff
I think you misunderstand what i mean by "precalculated". I just mean the
answer is embeded in the source code and not calculated at runtime. It doesnt
matter how you embed it in the source code, whether that is a list literal or
constructed via functions or whatever

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.

~~~
inimino
I think what you are trying to say is that it's not using the modulo operator
to derive the pattern _from the integers_ , instead it is simply encoding the
pattern directly in the program as a pattern (false, false, true, ...).

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.

~~~
chongli
Yes, exactly. In group theory when we speak of a quotient group modulo one of
its normal subgroups, we’re referring to exactly this. Z3, the integers mod 3,
is isomorphic to (actually equal to) Z/3Z, the integers quotiented by all the
multiples of 3.

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.

~~~
elcritch
Do you have a good resource for an intro to group theory and abstract algebra?
I’ve never had a chance to study it in a course but it pops up a lot in
physics and other interesting phenomena.

~~~
chongli
The best resource I know of is the textbook I bought for the course I took on
group theory and ring theory [1]. It’s pretty expensive and the exercises are
very challenging but if you’re a self-motivated student, you can learn a TON
of abstract algebra from this one book. You may want to review some linear
algebra before you dive in, if you haven’t done so in a while. You can find
solutions to many of the exercises online though I can’t vouch for their
accuracy.

[1] [https://www.amazon.com/Abstract-Algebra-3rd-David-
Dummit/dp/...](https://www.amazon.com/Abstract-Algebra-3rd-David-
Dummit/dp/0471433349)

------
raverbashing
I love these "harder than necessary" solutions to FizzBuzz, it might be useful
to take questions about your ability out of the way.

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)

~~~
zamadatix
Assuming you wanted to keep it in base 10 this is the simplest to follow way I
could come up with:

    
    
        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 && divFive

~~~
Someone
That doesn’t satisfy the requirement “only do string processing and
substitution on the numbers”.

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)

------
egypturnash
This is so beautiful.

------
united893
Not sure I ever understood the haskell folks drive. Is it mathematical poetry?
An extreme attempt to simplify? Is this an intellectual equivalent of trying
to write poetry?

The words catamorphism, semigroup and monoid barely make sense to most
programmers.

~~~
aaron-santos
As a former Scala dev who dipped their toes a bit too far into
Cats/Matryoshka, it came from a combination of DRY + powerful abstractions. If
you have powerful abstractions then it's possible to see commonalities between
parts of the codebase that others can't see. "Oh that thing that we're doing
over here with lists is the same as that part with futures." That's where DRY
comes in and turns that observation of duplicity into unification. Less code
equals less bugs, right?

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.

~~~
rgoulter
I think there's a bit of a leap between "Option types are useful" and "using
terminology to describe simple and common patterns of computation", though.

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'".

~~~
tsss
The concepts really aren't that easy (though they are indeed simple). While I
disliked the names too at the beginning, I realize now that they are
necessary, because it's important to be precise with your words and the
concepts are so general that there simply isn't a better word for it. You can
try to call a functor a container since both `List a` and `Maybe a` are
functors and contain an `a`. But what about `Int -> a`? It is also a functor
and doesn't "contain" anything. Perhaps "producer" would be a better word?
`data Phantom a = Phantom` doesn't "produce" anything and is still a functor.
Eventually you may arrive at a name like "mappable" which is convention just
like "functor" and arguably worse because "functor" is more precise and was
there first.

Names like StateT and Reader can surely be improved but the names taken
directly from mathematics are already the best we can do imo.

~~~
mikekchar
I don't know. `Int -> a` is definitely a container in my mind. Imagine a list.
What does it contain? Well, it contains the contents of the list, of course!
What is it? We don't know until we look inside. What does `Int -> a` contain?
It contains an `a`. Which `a` does it contain? We don't know until we look
inside (by passing it an `Int`). The only real difference is that in a list I
put the value in first and look at it later. With a function, I look at the
value first and put the value in later ;-) I'm being silly, but I really do
find that treating it as a container makes it much, much easier for me to
reason about it.

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.

