samber / mo

🦄 Monads and popular FP abstractions, powered by Go 1.18+ Generics (Option, Result, Either...)

Home Page:https://pkg.go.dev/github.com/samber/mo

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Translating types

sirfilip opened this issue · comments

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.

I 100% agree.

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

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]()
}

Ah, right you are

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. 😞

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.

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).

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

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

golang/go#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

@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.