Monads need syntax: a Go deep read

Monads need syntax: a Go deep read

A deep read of Omer Preminger's argument that monads only pay off in Go when the language gives them native-feeling syntax. The issue explains why `Result` feels natural in Rust but awkward as a plain Go library abstraction, and what that means for importing FP ideas into mainstream codebases.

Go already has a stubbornly effective error story: return a value plus an error, check it, keep moving. Omer Preminger's July 5, 2026 post argues that dropping Rust-style Result into that world does not buy much unless the language gives the abstraction syntax that makes it feel ordinary. 1

Recommended post

Original: Monads need syntactic sugar, by Omer Preminger, published July 5, 2026. Preminger frames the piece around a practical Go question: why does samber/lo feel useful in his Go work, while samber/mo keeps tempting him but never sticks? 1
That makes this a good first deep read for the channel. It sits exactly at the seam between FP theory and mainstream-language practice: not "what is a monad?" but "what has to be true before monads improve day-to-day code?"

The post in one sentence

Preminger's claim is that monads are not portable as library objects alone. They need host-language support at the call site. Without that support, Go's conventional (value, error) style can be just as clear, with fewer new dependencies and fewer unfamiliar idioms. 1

Full read-through

The post opens with a friendly contradiction. Preminger says he is a big fan of both functional programming and Go, then points to samber/lo as one of his favorite Go modules. The library describes itself as a Lodash-style Go package built on Go 1.18+ generics, with helpers such as map, filter, find, and related collection utilities. 2
That part feels natural in Go because lo.Map is still close to the grain of ordinary Go: pass a slice, pass a function, receive a slice. The syntax is slightly more functional, but the mental model is not alien.
The paired library samber/mo is a different proposition. Its README says it brings monads and FP abstractions to Go projects, including Option, Result, Either, Future, IO, Task, and State. 3 Preminger's attraction to it is easy to understand if you have moved between Rust and Go. Rust's Option and Result are useful partly because they make absence and failure explicit in the type system.
His objection is that Go does not give Result the same syntactic treatment Rust does. In Rust, error propagation and result handling are woven into the language. In Go, the mo.Result example still leaves the programmer checking whether the result is an error, then extracting either the error or the value. Preminger compares that with the ordinary Go version that returns (int, error) and finds the shapes nearly identical. 1
That comparison is the post's hinge. The monad has changed the type vocabulary, but not the local experience of writing or reading the code. If the call-site ceremony is the same, the abstraction has to justify its own weight.
Preminger does not say monads are useless in Go. He carves out a real exception: when monads appear inside FP-style collection transformations, Result can compose with lo.Map in a way that feels more meaningful. 1 The problem is frequency. In his own work, those cases are too rare to pay for a non-standard pattern across a Go codebase.
The post ends with a pragmatic verdict: lo stays, mo does not. It is not an anti-FP take. It is a host-language-design take. An FP abstraction has to cooperate with the syntax, conventions, and maintenance culture of the language it is entering.

The important technical detail

The strongest idea in the post is that syntax is not cosmetic. Syntax decides whether an abstraction stays visible.
Go gives us a useful contrast. In Go 1.23, the language added range-over-function support and standardized iterator forms such as iter.Seq and iter.Seq2, so user-defined iteration can plug into for/range. 4 That is a small but telling example: once the language gives an abstraction a native use site, the abstraction can recede into normal code.
Result in Go does not have that privilege. There is no Rust-like ? operator, no do-notation, and no standard pattern that makes the happy path read as the main path. The result is that the monad remains visible at every step. You see the wrapper. You see the extraction. You see the check.
That visibility is not automatically bad. It can be excellent when a team wants the failure model to be explicit in types. But it changes the trade-off. A monad library is no longer just a safer error type; it is a local dialect your team has to teach, review, and defend.

What the post gets right

Preminger's argument is useful because it avoids the usual religious split: "Go is too simple for FP" versus "Go should import more FP concepts." The better question is narrower: where does the host language already provide an ergonomic slot for the idea?
lo has a slot. Go developers already understand slices, functions, and generic helpers. A library call such as lo.Map is explicit, but it does not ask the rest of the program to reorganize itself.
mo.Result asks for more. It competes with one of Go's most entrenched idioms: return an error and check it immediately. To win that fight, it would need more than theoretical elegance. It would need to make the common path shorter, clearer, or safer in a way readers feel every day.
That is the portable lesson for FP in mainstream languages. A functional abstraction succeeds when it disappears into the host language's ordinary motion. It struggles when every use reminds the reader that a foreign abstraction has arrived.

How to apply it

Use Preminger's post as a quick test before importing an FP abstraction into a non-FP codebase:
  1. Does the host language have syntax for it? If yes, the abstraction may feel native. If no, expect visible ceremony.
  2. Does it improve the common path or only the rare path? A rare improvement may not justify a permanent dependency.
  3. Can ordinary maintainers debug it without a glossary? If the answer is no, keep it at module boundaries or in small, expert-owned areas.
  4. Does it compose with something the code already does often? Map and Filter compose with slices. Result only pays off when failures themselves are being transformed or combined.
None of this argues against FP. It argues for respecting the language you are actually using.

Lines worth keeping

Preminger's shortest summary of the comparison is also the clearest:
"That is... almost exactly the same."
The main claim follows immediately after:
"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."
His practical cost-benefit test is blunt:
"The juice is just not worth the squeeze."
And the final verdict keeps the piece from becoming an anti-monad rant:
"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)." 1

Bottom line

This is a small post with a large design lesson. FP ideas do not travel as pure concepts. They travel through syntax, error conventions, library norms, and team habits. If those layers cooperate, an abstraction can make code simpler. If they do not, even a good abstraction can become one more thing the reader has to mentally unwrap.

관련 콘텐츠

  • 로그인하면 댓글을 작성할 수 있습니다.