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

Translating types #11

Open sirfilip opened 2 years ago

sirfilip commented 2 years ago

At this moment we cant use monads to translate between different Result monad types.

Ex Ok(42).FlatMap(func(int) Result[string] { return Ok("wont work") })
because of the single type constraint introduced in the signature.

It wold be very useful if we can perform translations.

A way to do this is to detach FlatMap from result and make the signature like this

func [T, U any] FlatMap(func(T) Result[U]) Result[U]

Or maybe even

func [T, U any] FlatMap(func(T)(U, error)) Result[U]

I understand why this is not done here, it is because of the go generics restriction not to introduce new types in a struct methods. At the time being all types that are used in the struct methods must be declared in the struct definition.

Also func(T) Result[U] is not a correct functor interface, for go maybe func(t) (U, error) would be more appropriate but tbh returning result feels right. The cons is that it will be hard to define universal interface that will work across all of the monads.

samber commented 2 years ago

I 100% agree.

We are looking for a way to implement common interfaces. Any idea appreciated ;)

exchgr commented 2 years ago

As it is now:

func (o Option[T]) FlatMap(mapper func(value T) Option[T]) Option[T] {
    if o.isPresent {
        return mapper(o.value)
    }

    return None[T]()
}

Could this work?

func (o Option[T]) FlatMap[R](mapper func(value T) Option[R]) Option[R] {
    if o.isPresent {
        return mapper(o.value)
    }

    return None[R]()
}
sirfilip commented 2 years ago

Not without declaring the R type in the struct definition iirc

On Fri, Sep 16, 2022, 22:01 Elle Mundy @.***> wrote:

As it is now:

func (o Option[T]) FlatMap(mapper func(value T) Option[T]) Option[T] { if o.isPresent { return mapper(o.value) }

return None[T]() }

Could this work?

func (o Option[T]) FlatMap[R](mapper func(value T) Option[R]) Option[R] { if o.isPresent { return mapper(o.value) }

return None[R]() }

โ€” Reply to this email directly, view it on GitHub https://github.com/samber/mo/issues/11#issuecomment-1249754937, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAUTW3AFM6OHBPVOM73WUTV6TGZFANCNFSM6AAAAAAQDOKIBA . You are receiving this because you authored the thread.Message ID: @.***>

exchgr commented 2 years ago

Ah, right you are

joel-u410 commented 2 years ago

A while back, I had written a similar type (though I named my package rs because it was inspired by Rust!) and ended up just having a top-level Map function.

type Option[T any] struct {
    value *T
}
func Map[T any, U any](o Option[T], fn func(T) U) Option[U] {
    switch {
    case o.IsSome():
        return Some(fn(o.Unwrap()))
    default:
        return None[U]()
    }
}

Though, it might be tricky to make that work for both Option and Result -- which you'd probably want. Maybe you could have a Mappable interface, which provides the necessary support for implementing Map in a generic way, and then Map could be something like Map[M Mappable, T any, U any]. I haven't tried it though, it might still run up against the lack of generic methods. ๐Ÿ˜ž

exchgr commented 2 years ago

That signature is similar to loโ€™s Map, which is for iterating over slices. (I sometimes think of Options as slices with at most one element, so mapping them makes sense, and so does the similar function signature.) I think while go doesnโ€™t allow generic methods (functions tied to structs), it allows generic functions (top-level), so something like this would work.

tbflw commented 1 year ago

We're starting to look into using this lib in our Go stack. The lack of mapping/transform functions for the different algenraic data types is a bit f nuisance, so I really support the idea of doing something about that. IMO, the only way of doing that (due to the already mentioned (silly) constraints in the Go generics implementation for types), is to add top level functions for each type. Internally, we for example have defined a function something like this:

func TransformEither[L, R, T any](either mo.Either[L, R], tLeft func(L) T, tRight func(R) T) T {
    if either.IsLeft() {
        return tLeft(either.MustLeft())
    }
    return tRight(either.MustRight())
}

would it make sense to add a package either containing these top level functions? Actually, IMO, it would also make sense to have type constructors in that package, so you could write v := either.Right[L,R](someValue).

tbflw commented 1 year ago

@samber I'd be happy to contribute some PRs if we can agree on a direction for the structure.

GiGurra commented 1 year ago

I guess the limiting factor is that go does not allow type parameters on methods? If that was possible, transforming the value would be simple. There is an ongoing (been going on for years) discussion about adding support for this, but no conclusion :S

https://github.com/golang/go/issues/49085

Basically they haven't found any practical way of doing it because of how Golang implements generics (similar to how C++ templates). If they disallow type parameters for interface methods, it can be done though. I am hoping they will implement that in some version of the language :S

https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#No-parameterized-methods

Stack overflow with discussion about the same thing being impossible in C++ https://stackoverflow.com/questions/3251549/creating-an-interface-for-an-abstract-class-template-in-c.

Ofc, in other languages that are relying on erasure instead of code generation for generics (which is inferior in many other ways), the problem goes away entirely :S

tperdue321 commented 1 year ago

@tbflw @samber I've also been thinking the same thing and would be happy to contribute PRs that add functors for the various types. an adhoc example of functor

func Map[T any, U any](option mo.Option[T], mapper func(value T) (U, bool)) mo.Option[U] {
    val, present := option.Get()
    if present {
        return mo.TupleToOption(mapper(val))
    }
    return mo.None[U]()
}

inspiration coming from https://typelevel.org/cats/typeclasses/functor.html

(edit/late addition to my comment): While it would be ideal to use type parameters on methods, since Go doesn't support that, I think that creating top level functors (maybe scoped by packages that are named by types?) is an acceptable best-efforts alternative.

taman9333 commented 3 months ago

@sirfilip Does Fold implement what you need ๐Ÿ‘€