samber / mo

đŸ¦„ Monads and popular FP abstractions, powered by Go 1.18+ Generics (Option, Result, Either...)
https://pkg.go.dev/github.com/samber/mo
MIT License
2.61k stars 85 forks source link

Add Fold function to Result monad #42

Closed taman9333 closed 3 months ago

taman9333 commented 4 months ago

This pull request introduces Fold function to the Result monad, enhancing its functionality and making it more versatile for handling both success and failure scenarios.

Changes Made:

Benefits:

Example:

func main() {
    result := maySucceed(5)

    foldResult := result.Fold(onSuccess, onFailure)
    fmt.Println(foldResult)
}

func maySucceed(value int) mo.Result[int] {
    if value < 0 {
        return mo.Err[int](errors.New("Value must be greater than 0"))
    }

    return mo.Ok(value)
}

func onSuccess(value int) interface{} {
    return fmt.Sprintf("Success: %v", value)
}

func onFailure(err error) interface{} {
    return fmt.Sprintf("Failure: %v", err)
}
taman9333 commented 4 months ago

@samber

samber commented 3 months ago

I would use the same prototype as other methods: Fold(onSuccess[T], onFailure[T]) T

Feel free to add this method to the other types. Or I will do it by myself.

taman9333 commented 3 months ago

@samber Okay I think you are asking to have something like that

func (r Result[T]) Fold(successFunc func(T) T, failureFunc func(error) T) T {
    if r.err != nil {
        return failureFunc(r.err)
    }
    return successFunc(r.value)
}

However, I was thinking that the Fold function could map the result to a new type. This would allow the client (AKA the caller) to use custom functions that return any value based on their specific logic needs. For example, if the result monad encounters an error, instead of returning a fallback integer value, I would like to return a string containing an error message. So what do you think about that

samber commented 3 months ago

To date, Go does not allow type parameters on methods: https://github.com/golang/go/issues/49085 .

I don't really like returning interfaces, since it is not very typesafe.

In your case, I would suggest an additional helper: func Fold[T any, U any, R any](Foldable[T, U], func(T) R, func(U) R) R

IMO Option, Result and Either should implement the following interface:

type foldable interface[T, U] {
   left() T
   right() U
}
taman9333 commented 3 months ago

@samber Here are the changes. Are you okay with these changes before implementing the Foldable interface in the remaining types (Either & Option)?

Also, I have a question: I've noticed different implementations of the fold function in various languages/packages. For instance, for the Result<TValue, TError> monad, the fold function always returns a value of type TValue, as you mentioned here. However, in other packages, I see that the fold function returns a new type.

So, I'm unsure which approach makes more sense. Should we make the fold function consistently return the same type TValue? If so, would it be acceptable to implement another function that returns a new type?"

samber commented 3 months ago

Looks good.

I think we can return a different type in the case of mo.Fold, but it must be the same type if you also implement option.Fold

taman9333 commented 3 months ago

@samber I have changed method names for Foldable interface to make sure method names will not conflict with field names in the Either struct. Are you okay with that đŸ‘€

samber commented 3 months ago

Looks good!

Let's merge ;)