genkami / dogs

Make Go functional with dogs
Apache License 2.0
38 stars 5 forks source link

Examples needed. #1

Closed screwyprof closed 2 years ago

screwyprof commented 2 years ago

Hello @genkami!

First of all thank you for this nice project. I've recently discovered it and find it interesting.

I wanted to send you a letter, but then thought, that this issue maybe useful to some other enthusiasts, and also I can use the markdown formatting. Apologies for a long read and for the fact that I haven't had the chance to push the project to GitHub yet.

Before the generics were available I was playing around Monoids in Golang. My favourite example is FizzBuzz!

So I implemented the solution which was highly inspired by a couple of articles mentioned bellow. The first approach looked something like this:

// FizzBuzzFunctional
//
// https://web.archive.org/web/20130511210903/http://dave.fayr.am/posts/2012-10-4-finding-fizzbuzz.html
// https://medium.com/@iopguy/fizzbuzz-can-finally-be-implemented-in-stable-rust-87649a882f2d
// https://www.parsonsmatt.org/2016/02/27/an_elegant_fizzbuzz.html
func FizzBuzzFunctional(n int) string {
    m := monoid.ForString("").
        Append(monoid.ForString("Fizz").Filtered(func() bool {
            return n%3 == 0
        })).
        Append(monoid.ForString("Buzz").Filtered(func() bool {
            return n%5 == 0
        }))

    return m.UnwrapOr(strconv.Itoa(n))
}

I had to implement an Optional type (an equivalent of Maybe, the name seemed more conventional for Golang). UnwrapOr is basically FromMaybe in Haskell. After that I turned that Optional into a monoid. The API is not ideal, but the solution looks pretty simple. Given that we don't have the monadic list comprehensions I used Filtered function to add the predicated. So basically the structure that implemented the monoid, also had a Filtered method. Not very elegant, but better than nothing.

The beauty of this solution is that we can easily add more predicates: Fizz, Buzz, Bizz, etc... However in order for it to work we need to be able to create a Monoid which is basically a function, and not a concrete a simple type.

My Optional Monoidlooked like this:

package monoid

type OptionString struct {
    op option.String
}

// SomeString wraps the s into an optional string.
func SomeString(s string) OptionString {
    return OptionString{
        op: option.SomeString(s),
    }
}

// NoneString returns an empty optional string.
func NoneString() OptionString {
    return OptionString{
        op: option.NoneString(),
    }
}

func (m OptionString) Empty() OptionString {
    return NoneString()
}

func (m OptionString) Append(other OptionString) OptionString {
    switch {
    case m == NoneString():
        return other
    case other == NoneString():
        return m
    default:
        return SomeString(m.op.Append(other.op).UnwrapOrDefault())
    }
}

func (m OptionString) Filtered(fn func() bool) OptionString {
    if fn() {
        return m
    }

    return NoneString()
}

func (m OptionString) UnwrapOr(s string) string {
    return m.op.UnwrapOr(s)
}

It was using a String Option which supported the Append for convenience, however it was not a Monoid, it didn't respect the Monodic laws, however the monoid.OptionsSting did:

func TestOptionString(t *testing.T) {
    t.Parallel()

    // A set S equipped with a binary operation S × S → S,
    // which we will denote •, is a monoid if it satisfies the following two axioms:

    t.Run("it has valid identity", func(t *testing.T) {
        t.Parallel()

        m := monoid.SomeString("test")

        // There exists an element e in S such that for every element a in S,
        // the equations e • a = a and a • e = a hold.
        AssertEqual(t, m, m.Append(m.Empty()))
        AssertEqual(t, m.Empty().Append(m), m.Append(m.Empty()))
    })

    t.Run("it has valid associativity", func(t *testing.T) {
        t.Parallel()

        a := monoid.SomeString("foo")
        b := monoid.SomeString("bar")
        c := monoid.NoneString()

        // For all a, b and c in S, the equation (a • b) • c = a • (b • c) holds.
        AssertEqual(t, a.Append(b).Append(c), a.Append(b.Append(c)))
    })
}

The new iteration was changed a little to use foldable:

func FizzBuzzFunctional(n int) string {
    fizz := func(n int) bool {
        return n%3 == 0
    }

    buzz := func(n int) bool {
        return n%5 == 0
    }

    filters := foldable.RuleFoldable{
        monoid.ForFizzBuzzPredicate(fizz, "Fizz"),
        monoid.ForFizzBuzzPredicate(buzz, "Buzz"),
    }

    rules := fold(filters, monoid.NewEmptyFizzBuzzRuleset()) // a monoid which combines all the filters together

    return monoid.FromStringOption(strconv.Itoa(n), rules(n))
}

// fold :: (Foldable t, Monoid m) => t m -> m
func fold(filters foldable.RuleFoldable, m monoid.FizzBuzzRuleset) monoid.FizzBuzzRuleset {
    rules := filters.Foldl(filters.Init(), func(result foldable.T, next foldable.T) foldable.T {
        rule, ok := next.(func(int) monoid.OptionString)
        mustOk(ok, "cannot cast result foldable.T to func(int) monoid.OptionString")

        m = m.Append(rule)

        return m
    })

    ruleSet, ok := rules.(monoid.FizzBuzzRuleset)
    mustOk(ok, "cannot cast result foldable.T to monoid.FizzBuzzRuleset")

    return ruleSet
}

func mustOk(ok bool, msg string) {
    if !ok {
        panic(msg)
    }
}

Now that we have the generics, and your library we probably can come up with a better solution. I tried to use some of your ideas but at the moment you're changing the API faster than I have the time to follow :-D Nonetheless, I was almost able to implement the monoids I need from your wonderful library, however I stuck at the moment with a monoid to combine two functions. It got pretty ugly on my side.

Could you please add an ExampleTest for this FizzBuzz case to demonstrate the usage of your library the way you were intended? What I really would like to achieve is the following translated into Go without being too ugly :)

{-# LANGUAGE MonadComprehensions #-}

module Main where
import Data.Monoid (mappend)
import Data.Maybe (fromMaybe, listToMaybe, maybe)
import System.Environment (getArgs)

fizzbuzz i = fromMaybe (show i) $ mappend ["fizz" | i `rem` 3 == 0]
                                          ["buzz" | i `rem` 5 == 0]

main = mapM_ putStrLn [ fizzbuzz i | i <- [1..100] ]

Thank you very much.

genkami commented 2 years ago

Thank you for submitting such a detailed issue with great examples and references! I am so glad that you are interested in my project. I have done some experiments on Go generics these days and APIs are almost fixed. So I will be able to start writing some examples soon. Please wait for a moment!

Your functional FizzBuzz example can be written as something like this with dogs (but there's still lack of some functions):

var monoid algebra.Monoid[option.Option[string]] = option.DeriveMonoid[string](algebra.DeriveMonoid[string]())

func FizzBuzz(i int) string {
    return option.UnwrapOr(
        monoid.Combine(
            option.Filter(option.Some[string]("fizz"), func (_ string) bool { return i % 3 == 0 }),
            option.Filter(option.Some[string]("buzz"), func (_ string) bool { return i % 5 == 0 }),
        ),
        fmt.Sprint(i),
    )
}

func main() {
    iterator.ForEach[string](
        iterator.Map[int, string](iterator.Range[int](1, 100), FizzBuzz),
        func (x string) { fmt.Println(x) },
    )
}

It's still a bit ugly compared to Haskell, but I think It's acceptable. I hope you will like this 😄

genkami commented 2 years ago

Sorry for being late, but I added the example above to the repo! https://github.com/genkami/dogs/tree/main/examples