Alternate title: “For Go, it’s yes to lo, and no to mo.”

I am big, big fan of functional programming.

And I am big, big fan of Golang.

Consequently, one of my favorite open-source Go modules is samber/lo, which brings all the Map and Filter goodness all FP-enthusiasts know and love to Go:

package main

import (
  "fmt"

  "github.com/samber/lo"
)

func main() {
  someVals := []float64{1, 2, 3, 4}

  incVals := lo.Map(someVals, func(v float64, _ int) float64 {
    return v + 0.5
  })

  fmt.Printf("%+v\n", incVals)
}

[view/run this code in Go Playground]

The author of samber/lo has another library that has piqued my interest on more than one occasion: samber/mo, a library that brings Rust-style monads like Option and Result to Go.

I do the vast majority of my programming - both for work and for pleasure – in Go. But every time I veer into Rust for a short while, I miss its Option and Result monads when I come back to Go. (More recently, I have been playing around with Zig a little bit, and its “error union” construct is a lot like Rust’s Result monad.) And then, without fail, I find myself looking at samber/mo, and even listlessly fantasizing about writing my next greenfield Go project with monads… only to then not do that.

Today it finally clicked for me why that is - why samber/mo never “sticks” for me, the way samber/lo has.

A big part of the power of monads like Result in Rust is that the language has built-in syntactic sugar for manipulating them:

fn main() {
    let reader = input_util::input_reader_from_args(env!("CARGO_PKG_NAME"));

    match read_inputs(reader).map(|inputs| {
        ...

        output
    }) {
        Ok(value) => {
        println!("OUTPUT: {}", value);
        }
        Err(err) => {
            eprintln!("Error: {}", err);
            std::process::exit(1);
        }
    }
}

Now compare this with what happens when using samber/mo in Go, where no such syntactic sugar exists:

package main

import (
  "fmt"

  "github.com/samber/mo"
)

func main() {
  someVals := []int{1, 2, 3, 4}

  for _, value := range someVals {
    if result := doubleItUnlessEven(value); result.IsError() {
      fmt.Printf("%d yielded an error: %q\n", value, result.Error())
    } else {
      fmt.Printf("%d yielded: %d\n", value, result.MustGet())
    }
  }
}

func doubleItUnlessEven(value int) mo.Result[int] {
  if value%2 == 0 {
    return mo.Errf[int]("I am designed to return an error for %d", value)
  }

  return mo.Ok(value * 2)
}

[view/run this code in Go Playground]

Here’s what the same code would look like without the use of monads:

package main

import (
  "fmt"
)

func main() {
  someVals := []int{1, 2, 3, 4}

  for _, value := range someVals {
    if result, err := doubleItUnlessEven(value); err != nil {
      fmt.Printf("%d yielded an error: %q\n", value, err)
    } else {
      fmt.Printf("%d yielded: %d\n", value, result)
    }
  }
}

func doubleItUnlessEven(value int) (int, error) {
  if value%2 == 0 {
    return 0, fmt.Errorf("I am designed to return an error for %d", value)
  }

  return value * 2, nil
}

[view/run this code in Go Playground]

And that is the crux of the matter: absent the Rust-style syntactic sugar that exists for Result, using samber/mo in Go really doesn’t buy us very much.


An aside…

There is of course one big exception to this “doesn’t buy us much” conclusion - namely, when using monads in functional-programming constructs:

package main

import (
  "fmt"

  "github.com/samber/lo"
  "github.com/samber/mo"
)

func main() {
  someVals := []int{1, 2, 3, 4}

  results := lo.Map(someVals, func(value int, _ int) mo.Result[int] {
    return doubleItUnlessEven(value)
  })

  tenfoldResults := lo.Map(results, func(value mo.Result[int], _ int) mo.Result[int] {
    return value.Map(func(innerValue int) (int, error) {
      return innerValue * 10, nil
    })
  })

  for i, value := range tenfoldResults {
    if value.IsError() {
      fmt.Printf("%d yielded an error: %q\n", someVals[i], value.Error())
    } else {
      fmt.Printf("%d yielded: %d\n", someVals[i], value.MustGet())
    }
  }
}

func doubleItUnlessEven(value int) mo.Result[int] {
  if value%2 == 0 {
    return mo.Errf[int]("I am designed to return an error for %d", value)
  }

  return mo.Ok(value * 2)
}

[view/run this code in Go Playground]

The thing is, use-cases of this type are rare enough (…in my work, at least…) that the juice is just not worth the squeeze. In other words: introducing Go patterns that are not plain-vanilla, with all the attendant maintenance- and dependency-related costs that this brings in, is not worth the payoff in this case.

End of aside.


That’s basically it. My conclusion, for now, is that monads are awesome, but they provide an advantage over Go’s (SOMETYPE, error) convention only if there is syntactic sugar to support them (à la Rust). Which there isn’t in Go, and, if you know anything about the philosophy behind Go, there likely won’t ever be.

So for me, for now, it’ll keep being “yes” to samber/lo, and “no” to samber/mo.