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.43k stars 79 forks source link

Type transformations in Map #37

Open gaurang-sawhney opened 7 months ago

gaurang-sawhney commented 7 months ago
Scala: def map[B](f: (A) ? B): Traversable[B]

Golang: func (r Result[T]) Map(mapper func(value T) (T, error)) Result[T] 

Generally the map function supports type transformations from T[A] => T[B] which is used the most in all practical use cases. While the library supports mapping, but its applicability is very limited due to this limitations.

I believe this is majorly because Golang does not support type arguments on methods.

Any means to bypass this and get a more practical mappers?

samber commented 7 months ago

I fully agree with this point.

It would be very nice to be able to transform any Traversable (not only Option). If you have an idea to do it in a generic way, please propose!

gaurang-sawhney commented 6 months ago

I tried to brainstorm a lot on this but most of the options (something that exists in other FP languages) are a bit constrained with Golang since it is not allowing type parameters on methods.

But I was able to come up with a workaround where we are able to exploit the functional aspects using the Functions in golang.

type Mappable[T any] interface {
    Get() T
    Error() error
}

type Future[T any] struct {
    value T
    err   error
}

func (f Future[T]) Get() T {
    if f.err != nil {
        panic(f.err)
    }
    return f.value
}

func (f Future[T]) Error() error {
    return f.err
}

func Map[A Mappable[T], T any, U any](obj A, mapper func(T) U) Mappable[U] {
    switch x := any(obj).(type) {
    case Future[T]:
        return futureMapper(x, mapper)
    default:
        panic("unknown type")
    }
}

func futureMapper[T any, U any](fut Future[T], mapper func(T) U) Future[U] {
    if fut.Error() != nil {
        return Future[U]{value: mapper(fut.Get()), err: nil}
    }

    return Future[U]{err: fut.Error()}
}

func Test() {
    future := Future[int]{10, nil}
    future1 := Map[Future[int], int, string](future, func(i int) string {
        return strconv.Itoa(i)
    })

    fmt.Println("future1", future1.Get(), reflect.TypeOf(future1.Get()))
}

There are two downsides which I see to this:

Please share your thoughts on this.

gaurang-sawhney commented 6 months ago

The mappable interface here can also be simplified, since we are using the pattern matching on the basis of types, and hence the class methods can be used directly here.

derelbenkoenig commented 2 months ago

Unfortunately this seems like a limitation of Go itself. If I wrote for example

type Action[T any] func() (T, error)

func [T, U any] (a Action[T]) FlatMap(f func(T) Action[U]) Action[U] {
  // ...
}

It doesn't compile because you can't declare generic type parameters on a method. I can write

func FlatMap[T,U any] (a Action[T], f func(T) Action[U]) Action[U] {
  // ...
}

and it compiles and works as expected, but what you lose is the syntactic convenience of being able to chain these like a method call or infix operation which avoids a bunch of deeply nested parentheses and makes the code nice and readable. One workaround would be to go full "Yolo" and make them all not generic, and instead have them all just take and return any. And the other workaround, which this library appears to have chosen, is to limit all of the chaining and stuff to keeping the same type. That keeps you the syntactic niceness of the method call syntax, and you lose the flexibility of being able to change types. It's a tradeoff and I wish Golang's type system were improved just enough that we wouldn't need the tradeoff.