mo - Monads
samber/mo
brings monads and popular FP abstractions to Go projects. samber/mo
uses the recent Go 1.18+ Generics.
Inspired by:
- Scala
- Rust
- FP-TS
See also:
- samber/lo: A Lodash-style Go library based on Go 1.18+ Generics
- samber/do: A dependency injection toolkit based on Go 1.18+ Generics
Why this name?
I love short name for such utility library. This name is similar to "Monad Go" and no Go package currently uses this name.
💡
Features
We currently support the following data types:
Option[T]
(Maybe)Result[T]
Either[A, B]
Future[T]
IO[T]
IOEither[T]
Task[T]
TaskEither[T]
State[S, A]
🚀
Install
go get github.com/samber/[email protected]
This library is v1 and follows SemVer strictly.
No breaking changes will be made to exported APIs before v2.0.0.
💡
Quick start
You can import mo
using:
import (
"github.com/samber/mo"
)
Then use one of the helpers below:
option1 := mo.Some(42)
// Some(42)
option1.
FlatMap(func (value int) Option[int] {
return Some(value*2)
}).
FlatMap(func (value int) Option[int] {
return Some(value%2)
}).
FlatMap(func (value int) Option[int] {
return Some(value+21)
}).
OrElse(1234)
// 21
option2 := mo.None[int]()
// None
option2.OrElse(1234)
// 1234
option3 := option1.Match(
func(i int) (int, bool) {
// when value is present
return i * 2, true
},
func() (int, bool) {
// when value is absent
return 0, false
},
)
// Some(42)
More examples in documentation.
🤠
Documentation and examples
GoDoc: https://godoc.org/github.com/samber/mo
Option[T any]
Option
is a container for an optional value of type T
. If value exists, Option
is of type Some
. If the value is absent, Option
is of type None
.
Constructors:
Methods:
.IsPresent()
doc.IsAbsent()
doc.Size()
doc.Get()
doc.MustGet()
doc.OrElse()
doc.OrEmpty()
doc.ForEach()
doc.Match()
doc.Map()
doc.MapNone()
doc.FlatMap()
doc
Result[T any]
Result
respresent a result of an action having one of the following output: success or failure. An instance of Result
is an instance of either Ok
or Err
. It could be compared to Either[error, T]
.
Constructors:
Methods:
.IsOk()
doc.IsError()
doc.Error()
doc.Get()
doc.MustGet()
doc.OrElse()
doc.OrEmpty()
doc.ToEither()
doc.ForEach()
doc.Match()
doc.Map()
doc.MapErr()
doc.FlatMap()
doc
Either[L any, R any]
Either
respresents a value of 2 possible types. An instance of Either
is an instance of either A
or B
.
Constructors:
Methods:
.IsLeft()
doc.IsRight()
doc.Left()
doc.Right()
doc.MustLeft()
doc.MustRight()
doc.LeftOrElse()
doc.RightOrElse()
doc.LeftOrEmpty()
doc.RightOrEmpty()
doc.Swap()
doc.ForEach()
doc.Match()
doc.MapLeft()
doc.MapRight()
doc
Future[T any]
Future
represents a value which may or may not currently be available, but will be available at some point, or an exception if that value could not be made available.
Constructors:
mo.NewFuture()
doc
Methods:
IO[T any]
IO
represents a non-deterministic synchronous computation that can cause side effects, yields a value of type R
and never fails.
Constructors:
Methods:
.Run()
doc
IOEither[T any]
IO
represents a non-deterministic synchronous computation that can cause side effects, yields a value of type R
and can fail.
Constructors:
mo.NewIOEither()
docmo.NewIOEither1()
docmo.NewIOEither2()
docmo.NewIOEither3()
docmo.NewIOEither4()
docmo.NewIOEither5()
doc
Methods:
.Run()
doc
Task[T any]
Task
represents a non-deterministic asynchronous computation that can cause side effects, yields a value of type R
and never fails.
Constructors:
mo.NewTask()
docmo.NewTask1()
docmo.NewTask2()
docmo.NewTask3()
docmo.NewTask4()
docmo.NewTask5()
docmo.NewTaskFromIO()
docmo.NewTaskFromIO1()
docmo.NewTaskFromIO2()
docmo.NewTaskFromIO3()
docmo.NewTaskFromIO4()
docmo.NewTaskFromIO5()
doc
Methods:
.Run()
doc
TaskEither[T any]
TaskEither
represents a non-deterministic asynchronous computation that can cause side effects, yields a value of type R
and can fail.
Constructors:
Methods:
State[S any, A any]
State
represents a function (S) -> (A, S)
, where S
is state, A
is result.
Constructors:
Methods:
🛩
Benchmark
// @TODO
This library does not use reflect
package. We don't expect overhead.
🤝
Contributing
- Ping me on twitter @samuelberthe (DMs, mentions, whatever :))
- Fork the project
- Fix open issues or request new features
Don't hesitate ;)
With Docker
docker-compose run --rm dev
Without Docker
# Install some dev dependencies
make tools
# Run tests
make test
# or
make watch-test
👤
Authors
- Samuel Berthe
💫
Show your support
Give a
📝
License
Copyright © 2022 Samuel Berthe.
This project is MIT licensed.
Future[T] implementation discussion
If we call
(*Future[T]).Then
afterFuture[T]
has completed, theThan
-callback will never been called. And if we try toCollect
a completedFuture[T]
, it could cause deadlock.for example:
Futures call next future once they have finished, so if we chain a callback on a finished future, the call chain would be broken.
And here is my commit to fix this commit.
Strat implementing EitherX for X between 3 and 5
I have only implemented Either5 so far.
@samber Could you do a quick PR before I duplicate the code for Either3 and Either4 ?
I was thinking of leaving the
README.md
andeither5_example_test.go
as it is and just addeither3.go
,either4.go
,either3_test.go
andeither4_test.go
.I think that should be enough that way, WDYT?
Could we add EitherX ?
I guess we could have:
Constructors:
mo.CreateEither3Arg1() mo.CreateEither3Arg2() mo.CreateEither3Arg3()
Methods:
.IsArg(i int) .Arg1() .Arg2() .Arg3() .MustArg1() .MustArg2() .MustArg3() .Arg1OrElse() .Arg2OrElse() .Arg3OrElse() .Arg1OrEmpty() .Arg2OrEmpty() .Arg3OrEmpty() .ForEach() .Match() .MapArg1() .MapArg2() .MapArg3()
@samber If you agree that it could be useful I could add Either3/4/5 If this is added I'm wondering if we should then have Either2 instead of the current Either type for consistency? And maybe mark the current Either as deprecated?
Auto-Boxing in method FlatMap
This is an example of using this package, but I suppose function square should be
This declaration of mapping functions is better, I think.
Consider change the FlatMap into
Some(aMap["nonExistingKey"]).OrElse("FallBackValue")?
For
aMap map[string]string
, can I doSome(aMap["nonExistingKey"]).OrElse("FallBackValue")
?It seems to return empty string all the time.
Thanks.
chore: clean Either internal state
Currently, the Either struct uses two booleans to represent what kind of value we have. Even though
mo
doesn't export these booleans, they allow for 4 different states instead of the desired 2. This PR removes theisRight
boolean, enforcing only two possible states can be represented internally. This change has the benefit of simplifying some tests, albeit very minimally.Reorder fields to save more memory
When size of T is not 8 bytes aligned, put isErr and value before err will save more memory
Misuse of resolve/reject causes "close of closed channel" panic
Hello,
If I run:
I get
panic: close of closed channel
.This makes me write defensive code (e.g. return after each call to reject / resolve). I would suggest to recover from panics and reflect it as an error as part of
Result[T]
's error.Translating types
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.
Add Helper function that would work like Either.Match but would return another type
Could we have a helper function like the following for
Either
?It would be useful to be able to Map an Either to another type.
Implement options package for cross-type transformations
As stated on the package doc:
This package includes:
options.Map
options.FlatMap
options.Match
options.FlatMatch
The
FlatMatch
function is to replace this pattern:I would have this be the behavior of
Match
and return anOption
when wanted, but since there is already a method that use the two return values I decided to have the same signature for the function with same name and create a new function for this pattern. I called itFlatMatch
since it allows the same use case asFlatMap
, where the given functions return anOption
and the result is that sameOption
.If you agree with this idea I can send a
results
package right away.Applicative
If m and n are two Option[int], I want to add them up. How can I finish this? I learnt haskell these days and it uses applicative (a special functor) to do this. Maybe an apply method is needed?