Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type transformations in Map #37

Open
gaurang-sawhney opened this issue Dec 8, 2023 · 4 comments
Open

Type transformations in Map #37

gaurang-sawhney opened this issue Dec 8, 2023 · 4 comments

Comments

@gaurang-sawhney
Copy link

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
Copy link
Owner

samber commented Dec 8, 2023

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
Copy link
Author

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:

  • We are moving away from objects and keeping all these utilities as separate library support instead.
  • We will need to implement something like func Map2[A Mappable[T, U], T any, U any, V any](obj A, mapper func(T) V) Mappable[V, U] to support type where we are taking two type arguments or more.

Please share your thoughts on this.

@gaurang-sawhney
Copy link
Author

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
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants