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

  • By Samuel Berthe
  • Last update: Jan 1, 2023
  • Comments: 12

mo - Monads

tag GoDoc Build Status Go report codecov

🦄 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:

  • mo.Some() doc
  • mo.None() doc
  • mo.TupleToOption() doc

Methods:

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:

  • mo.Ok() doc
  • mo.Err() doc
  • mo.TupleToResult() doc

Methods:

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:

  • mo.Left() doc
  • mo.Right() doc

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:

  • mo.NewIO() doc
  • mo.NewIO1() doc
  • mo.NewIO2() doc
  • mo.NewIO3() doc
  • mo.NewIO4() doc
  • mo.NewIO5() doc

Methods:

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() doc
  • mo.NewIOEither1() doc
  • mo.NewIOEither2() doc
  • mo.NewIOEither3() doc
  • mo.NewIOEither4() doc
  • mo.NewIOEither5() doc

Methods:

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() doc
  • mo.NewTask1() doc
  • mo.NewTask2() doc
  • mo.NewTask3() doc
  • mo.NewTask4() doc
  • mo.NewTask5() doc
  • mo.NewTaskFromIO() doc
  • mo.NewTaskFromIO1() doc
  • mo.NewTaskFromIO2() doc
  • mo.NewTaskFromIO3() doc
  • mo.NewTaskFromIO4() doc
  • mo.NewTaskFromIO5() doc

Methods:

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:

  • mo.NewTaskEither() doc
  • mo.NewTaskEitherFromIOEither() doc

Methods:

State[S any, A any]

State represents a function (S) -> (A, S), where S is state, A is result.

Constructors:

  • mo.NewState() doc
  • mo.ReturnState() doc

Methods:

🛩 Benchmark

// @TODO

This library does not use reflect package. We don't expect overhead.

🤝 Contributing

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 ⭐️ if this project helped you!

support us

📝 License

Copyright © 2022 Samuel Berthe.

This project is MIT licensed.

Download

mo.zip

Comments(12)

  • 1

    Future[T] implementation discussion

    If we call (*Future[T]).Then after Future[T] has completed, the Than-callback will never been called. And if we try to Collect a completed Future[T], it could cause deadlock.

    for example:

    func Test_Future(t *testing.T) {
    	completed := make(chan struct{})
    	fut := mo.NewFuture(func(resolve func(int), reject func(error)) {
    		resolve(1)
    		close(completed)
    	})
    
    	<-completed
    	fut.Then(func(in int) (int, error) {
                fmt.Println(in) // will never been print
    	    return in, nil
    	}).Collect() // deadlock
    }
    

    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.

  • 2

    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 and either5_example_test.go as it is and just add either3.go, either4.go, either3_test.go and either4_test.go.

    I think that should be enough that way, WDYT?

  • 3

    Could we add EitherX ?

    I guess we could have:

    type Either3[T1 any, T2 any, T3 any] struct {
    	argId int
    
    	arg1 T1
    	arg2 T2
    	arg3 T3
    }
    

    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?

  • 4

    Auto-Boxing in method FlatMap

    package main
    
    import "github.com/samber/mo"
    
    func main() {
    	var x float64 = 2.0
    	op := mo.Some(x)
    	square := func(x float64) mo.Option[float64] { return mo.Some(x * x) }
    	cubic := func(x float64) mo.Option[float64] { return mo.Some(x * x * x) }
    	print(op.FlatMap(square).FlatMap(cubic).OrElse(-1))
    }
    

    This is an example of using this package, but I suppose function square should be

    	square := func(x float64) float64 { return x * x }
    

    This declaration of mapping functions is better, I think.

    Consider change the FlatMap into

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

    Some(aMap["nonExistingKey"]).OrElse("FallBackValue")?

    For aMap map[string]string, can I do Some(aMap["nonExistingKey"]).OrElse("FallBackValue")?

    It seems to return empty string all the time.

    Thanks.

  • 6

    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 the isRight boolean, enforcing only two possible states can be represented internally. This change has the benefit of simplifying some tests, albeit very minimally.

  • 7

    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

    package main
    
    import (
    	"fmt"
    	"unsafe"
    
    	"github.com/samber/mo"
    )
    
    func Ok[T any](value T) Result[T] {
    	return Result[T]{
    		value: value,
    		isErr: false,
    	}
    }
    
    type Result[T any] struct {
    	isErr bool
    	value T
    	err   error
    }
    
    func main() {
    	fmt.Printf("mo(bool): %v\n", unsafe.Sizeof(mo.Ok[bool](false)))
    	fmt.Printf("my(bool): %v\n", unsafe.Sizeof(Ok[bool](false)))
    	fmt.Printf("mo(int8): %v\n", unsafe.Sizeof(mo.Ok[int8](0)))
    	fmt.Printf("my(int8): %v\n", unsafe.Sizeof(Ok[int8](0)))
    	fmt.Printf("mo(int16): %v\n", unsafe.Sizeof(mo.Ok[int16](0)))
    	fmt.Printf("my(int16): %v\n", unsafe.Sizeof(Ok[int16](0)))
    	fmt.Printf("mo(int32): %v\n", unsafe.Sizeof(mo.Ok[int32](0)))
    	fmt.Printf("my(int32): %v\n", unsafe.Sizeof(Ok[int32](0)))
    	fmt.Printf("mo(int64): %v\n", unsafe.Sizeof(mo.Ok[int64](0)))
    	fmt.Printf("my(int64): %v\n", unsafe.Sizeof(Ok[int64](0)))
    	fmt.Printf("mo(float32): %v\n", unsafe.Sizeof(mo.Ok[float32](0)))
    	fmt.Printf("my(float32): %v\n", unsafe.Sizeof(Ok[float32](0)))
    	fmt.Printf("mo(float64): %v\n", unsafe.Sizeof(mo.Ok[float64](0)))
    	fmt.Printf("my(float64): %v\n", unsafe.Sizeof(Ok[float64](0)))
    	fmt.Printf("mo(complex64): %v\n", unsafe.Sizeof(mo.Ok[complex64](0)))
    	fmt.Printf("my(complex64): %v\n", unsafe.Sizeof(Ok[complex64](0)))
    	fmt.Printf("mo(complex128): %v\n", unsafe.Sizeof(mo.Ok[complex128](0)))
    	fmt.Printf("my(complex128): %v\n", unsafe.Sizeof(Ok[complex128](0)))
    	fmt.Printf("mo([12]byte): %v\n", unsafe.Sizeof(mo.Ok[[12]byte]([12]byte{})))
    	fmt.Printf("my([12]byte): %v\n", unsafe.Sizeof(Ok[[12]byte]([12]byte{})))
    }
    
    mo(bool): 32
    my(bool): 24
    mo(int8): 32
    my(int8): 24
    mo(int16): 32
    my(int16): 24
    mo(int32): 32
    my(int32): 24
    mo(int64): 32
    my(int64): 32
    mo(float32): 32
    my(float32): 24
    mo(float64): 32
    my(float64): 32
    mo(complex64): 32
    my(complex64): 32
    mo(complex128): 40
    my(complex128): 40
    mo([12]byte): 40
    my([12]byte): 32
    
  • 8

    Misuse of resolve/reject causes "close of closed channel" panic

    Hello,

    If I run:

    mo.NewFuture(func(resolve func(struct{}), reject func(error)) {
    	resolve(struct{}{})
    	reject(errors.New("oh no..."))
    })
    

    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.

  • 9

    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.

  • 10

    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?

    func Map[T any](e Either[L, R], onLeft func(L) T, onRight func(R) T) T 
    

    It would be useful to be able to Map an Either to another type.

  • 11

    Implement options package for cross-type transformations

    As stated on the package doc:

    The functions provided by this package are not methods of mo.Option due to the lack of method type parameters on methods. This is part of the design decision of the Go's generics as explained here: https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#No-parameterized-methods

    Providing these methods as a separate package also matches Go's primitives and standard library:

    • The string type don't have methods, but there we have the strings package.
    • The []byte type don't have methods, but there we have the bytes package.
    • The io.Reader defines a single method, and all manipulations of a reader is done on packages io and ioutil.

    This package includes:

    • options.Map
    • options.FlatMap
    • options.Match
    • options.FlatMatch

    The FlatMatch function is to replace this pattern:

    var thing ExpensiveThing
    if opt.IsPresent() {
      thing = NewExpensiveThingFrom(opt.MustGet())
    } else {
      thing = NewExpensiveThing()
    }
    
    // Can now be done like this:
    
    thing := options.FlatMatch(opt, NewExpensiveThingFrom, NewExpensiveThing)
    

    I would have this be the behavior of Match and return an Option 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 it FlatMatch since it allows the same use case as FlatMap, where the given functions return an Option and the result is that same Option.

    If you agree with this idea I can send a results package right away.

  • 12

    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?