But code written using this library is no longer Go: most Go programmers can't grok it, and it's awkward to call normal Go libraries because there's no way to know if that function you're calling is pure.
If your goal is to "make it easy and fun to write maintainable and testable code in golang" by making pure functions first-class, is there another way to do that without inventing a new language?
From experience and inspired by Carmack's classic essay on FP in C++[1], I tend toward a functional style: minimize state, treat locals as const, avoid non-const globals, enable parallelism by isolating state. Go makes it easy to write static analysis tools, so go vet could be augmented to, for example, keep track of which functions are pure, and show some yellow underlines at those places where input parameters are mutated.
I'd use something like that.
I cannot recommend this unless you really have to for whatever reason. Besides readability, another factor to consider is performance; Go is not optimized for functional programming structures. It doesn't have things like tail call optimization.
There's better languages than Go if you want to / have to do functional programming.
That is… literally just how libraries are. You need to understand the underlying language and the semantics and details of the library API.
This is exactly what I was afraid of when generics were introduced, and now I get to spend time arguing with people who read some blog post about how functional programming and type theory will save the world, instead of actually being productive. Ugh.
>... read some blog post about how functional programming and type theory will save the world, instead of actually being productive
I see the opposite side in Go a lot, where without testing or trying anything they dismiss everything they aren't already using right now as useless ivory tower academia, which is its own set of popular blog posts. Seems both sides have a lot of time to argue on the internet though, oddly the people who actually write code tend to be the productive ones regardless of philosophy.
Please dial back your casual critiques.
> This is exactly what I was afraid of when generics were introduced
Good god. IDK, perhaps hardware would better suit your skill set? It certainly scares me off, but you might have a good mind for it.
The creator made it because: "The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt. – Rob Pike 1"
Do we have to do the ad hominem thing? I studied FP and type theory in grad school. It's possible to know about FP and not want to use it in software engineering.
That said, I actually do remember my first exposure to Golang being a blog post about using monads to avoid incessantly typing `if err != nil`. Very much like that original author, my personal values in software engineering just don't align with Go at all, and that should be OK!
I'm biased because I've built a career on Go at this point but the pragmatism and ability to just get things done in Go without faffing about with unnecessary abstractions is I think one of the strongest practical demonstrations of how incredible an imperative language can be, and for me personally at least, no FP language will ever beat the productivity that I can achieve with Go, especially because at least in my problem domain the real world problems always have enough corner cases that FP wouldn't even be useful.
In Go I just systematically eliminate and handle each possible step and state, in a straightforward way, directly deal with the business logic, and then it's done and it works predictably and efficiently for years. Interfaces really are a sufficient form of polymorphism, too.
As it turned out, I accidentally started learning some functional-lite paradigms in Python. I learned that I actually do like some of these paradigms, and think through them already, I just couldn't connect my internal understanding with the language of FP.
I started learning Rust recently, as it's an exciting systems language with some hype. There, you see even more functional bits which is just a pleasure to use. I'm not in an area where purely functional would make sense but having the quality of life that certain paradigms brings is nice.
I'm still very much a novice in FP techniques, but the ability to try aspects as I go is helpful in learning.
At the time, I remember finding FP in go surprisingly ergonomic. Implementing the library to support it was a pain since the type system wasn't expressive enough to prevent everything from devolving into a pile of untyped reflection, but it was reasonably easy to keep that an implementation detail. On the whole, I felt like go would have lent itself well to the "dash of FP for flavor" style of programming that seems to be gaining popularity these days. Unfortunately, in 2017 at least, the Go community seemed to have very little interest in the idea.
I still have a fondness for Go. It always felt nice to use. If the language features have caught up to the point where a robust library like this is feasible, and the communities attitude has shifted, I might take another look at the language.
Are there any examples you'd be interested in in particular?
type IO[A any] func() A
If you consider this a valid approach, then the set of monadic helper functions make it easier to compose these effectful functions with pure functions.
This article https://betterprogramming.pub/investigating-the-i-o-monad-in... contains some more detailed reasoning.
data := F.Pipe3(
T.MakeTuple2("https://jsonplaceholder.typicode.com/posts/1", "https://catfact.ninja/fact"),
T.Map2(H.MakeGetRequest, H.MakeGetRequest),
R.TraverseTuple2(
readSinglePost,
readSingleCatFact,
),
R.ChainFirstIOK(IO.Logf[T.Tuple2[PostItem, CatFact]]("Log Result: %v")),
)
This looks like a pain to modify if you're not intimately familiar with the fp-go library and are just trying to insert a debug statement. Also, the passing two values in parallel via a chain of functions seems really brittle.There's some chat about adding some variant of (x, y => z) to Go, though even then you're adding some more symbols to an already symbol-heavy structure and it looks even worse when you're not using x y z but (username, accountId => a few lines of username and accountId being used).
People should stop pushing these things already, no one cares but them.
Go provides a set of nice features (fast startup, easy cross-platform building, great tooling, good package management) that can be hard to come by with other languages. It is not unreasonable to want to have your cake (all of the above features) and eat it too (occasionally use functional idioms in addition to the usual imperative ones).
For this reason I try to keep abreast of the various FP libraries in Go, though I have yet to use one in anger.
- https://github.com/samber/lo
- https://github.com/samber/mo
The split is also nice as you can choose to just use the generic convenience functions from lo without the more FP related things from mo.
Step 2: Add “Monoids for the Endomorphism where the `concat` operation is the usual function composition.”
Step 3: …
Step 4: Profit?
1) Go doesn't have a concise lambda expression. This makes the functional approach in Go will be more verbose and less readable than the traditional imperative approach.
2) Go's type inference is not sophisticated enough. Most of the time you will still need to explicitly annotate the types, which, again, makes it more verbose and less readable.
2) I absolutely agree, type inference could be better. However this has improved over time and go1.21 has also made good progress. I would expect that type inference will continue to improve in the future. This library tries to easy the pain of having to specify types redundantly by carefully choosing the order of type parameters. Those parameters that can be inferred (e.g. because they are part of the immediate function argument) come last, whereas the parameters that cannot be inferred come first, so you only have to specify those. This compromizes on a consistent type ordering, preferring useability over (internal) consistency. Examples are `Left` and `Right` of the `either` package. The order of type parameters is reversed between the two to avoid excessibe typing.
func TraverseParTuple10[F1 ~func(A1) ReaderIOEither[T1], F2 ~func(A2) ReaderIOEither[T2], F3 ~func(A3) ReaderIOEither[T3], F4 ~func(A4) ReaderIOEither[T4], F5 ~func(A5) ReaderIOEither[T5], F6 ~func(A6) ReaderIOEither[T6], F7 ~func(A7) ReaderIOEither[T7], F8 ~func(A8) ReaderIOEither[T8], F9 ~func(A9) ReaderIOEither[T9], F10 ~func(A10) ReaderIOEither[T10], A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6, A7, T7, A8, T8, A9, T9, A10, T10 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10) func(T.Tuple10[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10]) ReaderIOEither[T.Tuple10[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]]
(https://pkg.go.dev/github.com/IBM/fp-go/context/readerioeith...)
cracks knuckles over keyboard
But this complexity is an implementation detail of the library, you do not have to understand it as a user of these functions. From my perspective it is a valid approach to move complexity from use application layer into the library layer, so it can be hidden there and tested once.
Kill it with fire but make sure to pour some Holy water first.
client := H.MakeClient(HTTP.DefaultClient)
readSinglePost := H.ReadJson[PostItem](client)
readSingleCatFact := H.ReadJson[CatFact](client)
data := F.Pipe3(
T.MakeTuple2("https://jsonplaceholder.typicode.com/posts/1", "https://catfact.ninja/fact"),
T.Map2(H.MakeGetRequest, H.MakeGetRequest),
R.TraverseTuple2(
readSinglePost,
readSingleCatFact,
),
R.ChainFirstIOK(IO.Logf[T.Tuple2[PostItem, CatFact]]("Log Result: %v")),
)
result := data(context.Background())
fmt.Println(result())
https://github.com/IBM/fp-go/blob/main/samples/http/http_tes...Now, try and do the equivalent in "normal" Go code - it will be 3x-5x the lines of code. (Probably more)
https://godocs.io/github.com/IBM/fp-go/function
and check out the "constants":
https://godocs.io/github.com/IBM/fp-go/function#pkg-variable...
You are forced to handle errors. The result is almost certainly an Either[Error, Result].
fp-ts in TypeScript is not syntactically nice but it feels simple enough that after a week you can be pretty comfortable with it. Although, TypeScript compiler might be much better at inferring types than Go. My experience with fp-ts is that most of the time I do not have to write any type annotations except the top level ones.
The API looks fine, you will find similar type annotations in fp-ts and in Haskell, that's just how handling variadics works in most languages and for useful abstractions like traverse/map/chain you need to have the variadic ones available to avoid having to deal with arrays of anonymous functions (that might all need to have the same type).
When you spend a significant time writing fp-ts you barely look at the types. The experience of writing the code is smooth. Code written still has similar pitfalls as regular imperative programming, pyramids of doom, readability, most of the functions "annotated" with async but they are pure or can be pure if you order the data better etc.
I would say there is friction in the beginning, but as time passes, the brain learns how to parse the code. The effect felt very similar to me when I shifted from colored syntax to just plain black on white. After some point brain does its magic.
type ReaderIOEither[A any] RE.ReaderIOEither[context.Context, error, A]
In fact the test code you linked actually even does a check on the result: assert.Equal(t, E.Of[error](count), result())
In order to avoid all those excessive functions and 'silly' constants, Go needs to support const and variadic generics like C++ does. Then the API would become quite clean.I am not supporting the use of this library in prod code used in a large team - but its OK for small tools where one needs to iterate quickly. Folks familiar with FP constructs (esp users of fp-ts) would follow this code almost immediately. Basically the dirtiness and pain (most of it) has been encapsulated into the library.
This seems flawed. In idiomatic Go, T and error are always independently observable. The Either monad implies that they are dependent, which is not true.
There being a relationship between T and error is common, but observance of error is only significant when the error is relevant. Quite often it is, but not always, and in the latter case you can, assuming the code is idiomatic, safely use T and ignore error. T must be useful, after all.
It may be possible to create a scenario where T is not useful when error is not nil if you really want to screw with people, but that code would decidedly not be idiomatic. Indeed, there is always some way to screw with people if you try hard enough, but that's really beyond this discussion.
The use of the Either monad here is trying to cover a dependency which doesn't exist.
The idiomatic Go way to work around this is to write comments saying "sometimes T is non-nil even if err is non-nil, you need to handle this" and hoping your callers read your comments.
Funnily enough, your philosophy is far more true in a language with proper sum types. In Haskell/Ocaml/Rust, returning a tuple of (T, error) does mean that both T and error should both be "useful", because if they weren't the function would have chosen to return one or the other but not both. You're reading meaning into Go code where meaning can't be present, because there's no choice to be made, and ignoring languages where you actually can have the semantics you want Go to have.
By and large I think the stuff in this repo is too much and doesn't fit Go. I don't particularly want Go to pretend to be functional, but Either and Option at least would be nice to have in the stdlib and help prevent this exact issue where there are rare exceptions to normal practices. I don't see them getting widespread use without being part of the stdlib though. If Either/Option were common in Go but io.Reader was one of the few APIs returning (T, error), that would convey a lot more information.
https://github.com/koss-null/FuncFrog
still prefer non-FP part tho
Trying to merge this abstractions and patterns with existing Golang's philosophy and community libraries is simply a case of over-engineering.
Picture jumping into a codebase to quickly fix something, then stumble upon ChainFirstIOK or Eithersize5 because someone went overboard showing off that they remember FP from cs classes.
I’m quite sad to see this project as it demonstrates that Go is starting to lose many of the characteristics that attracted me to it in the first place.
(For some context, I know quite a bit about functional programming and formal type theory, having studied the latter in grad school. It is intrinsically very interesting but I believe it is a net negative in most software engineering contexts.)
I'd love to be able to survey the authors of the dozen-ish variations on this posted over the last couple of years (most of the much less elaborate than this) and see how many of them are still using it in their real code. Again I'm sure the answer isn't literally zero but I bet it's statistically-significantly fewer than all of them.
For example: this entire HN thread. And all the other libraries you mention that keep soliciting conversations, nerd sniping people who could be spending that time making better products instead of quibbling over FP code golf. But maybe those folks will always find things to quibble over...
Exactly, the place for FP was and always will be academia.
Real programs require real, readable logic.
Is there some other established Go library that contains these collections/containers?
1. To look clever.
2. To make junior programmers look stupid.
A pragmatic systems programming language with garbage collection and good support for functional programming already exits. Its called Ocaml and really deserves more love.
x := for y := range z { return y } // unclear return :-(
If you want Either, use Haskell.There seems also to be a performance problem with map(). It would work better if Go had Iteration instead of slices, otherwise map() creates a lot of slices. And if map does not return a slice you have an ugly
y := x.map(...).native
everywhere. public abstract class Option[T] implements Iterable[T] { }
// Some then is a one element Iterator/ None is an empty iterator
// With for you then can do something on Some
// orElse left as an exercise to the reader
for (String name: option) {
// do something with name
}
but today I think one should embrace the language and if it does not work for you, use something else.For Go I think something like Zigs !i32 would fit in perhaps, if one wants a higher level of error handling.
That being said though, it actually fit really well in golang. Allowed functions that used to return ‘null, err’ to return an Either, which improved on all the downsides of returning null (if you return null your callers have to check for it).
It actually improved the ergonomics quite a bit. ‘Either’ fits nicely into golang, but I doubt it will become mainstream anytime soon.
Not an expert in Go but I think you can do this:
func compose[A any, B any, C any](a func(A) (B, error), b func(B) (C, error)) func(A) (C, error) {
return func(aInp A) (C, error) {
res, err := a(aInp)
if err == nil {
return b(res)
} else {
return *new(C), err
}
}
}
The above is equivalent to haskells fish operator >=>The bind operator (>>=) can be implimented in terms of composition:
func bind[A any, B any, C any](a func(A) (B, error), b func(B) (C, error), aInput A) (C, error) {
return compose[A, B, C](a, b)(aInput)
} func TraverseTuple10[F1 ~func(A1) IOEither[E, T1], F2 ~func(A2) IOEither[E, T2], F3 ~func(A3) IOEither[E, T3], F4 ~func(A4) IOEither[E, T4], F5 ~func(A5) IOEither[E, T5], F6 ~func(A6) IOEither[E, T6], F7 ~func(A7) IOEither[E, T7], F8 ~func(A8) IOEither[E, T8], F9 ~func(A9) IOEither[E, T9], F10 ~func(A10) IOEither[E, T10], E, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10) func(T.Tuple10[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10]) IOEither[E, T.Tuple10[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]]1: https://github.com/scala/scala/blob/v2.13.11/src/library/sca...
Also yes it is awful.