3 ms·
There was a recent discussion in /r/golang about chaining methods like this. The OP had a hard time writing tests for their implementation since each part of th
by nunez 5y ago
There was a recent discussion in /r/golang about chaining methods like this. The OP had a hard time writing tests for their implementation since each part of the chain returned interfaces that were relied on by following links within. The top answer in the thread was that Golang wasn't really meant to make "Promise-" type workflows possible due to it leaning heavily on code being intentional and explicit.
I agree with them.
One of the problems I have with things like LINQ or Promises is that they are immensely powerful but can be really difficult to debug when things go wrong because of implicit errors within the pipeline bubble up the stack without providing location context. While the standard boilerplate around errors in Go is annoying, I actually prefer it for two reasons:
1. It provides exactly where things went wrong while looking at an exception trace, and
2. From a readability perspective, it is much easier to understand what the author of the code meant to do and, more importantly, what the follow-on error s mean. Sometimes, errors aren't actually errors.
Either way, OCaml/Haskell/F#'s approach with match expressions and the `Result` type is the best way to deal with this, IMO. You get the best of both worlds: explicit declaration with a very expressive (triangular-like) "shape" to the code.
So something like this:
var data string = script.File("test.txt").Match("Error").CountLines()
Can be expressed like this:
var foundLines int32 = 0
var numLines int32 = match file.Open("test.txt") with
| Error e -> return e
| Ok f -> match f.Readlines() with
| Error e -> return e
| Ok lines -> match lines with
| /Error.*/ -> foundLines++
| _ -> // do nothign
return foundLines
instead of:
foundLines := 0
lines, err := ioutil.ReadFile("test.txt")
if err != nil {
return 0, err
}
for _, l := range lines {
re, err := regexp.MustCompile(`Error.*`)
if err != nil {
return 0, err
}
matches := re.FindStringSubmatch(`Error.*`)
if len(matches) > 0 {
foundLines++
}
}
return foundLines, nil
This way, you can see exactly where the error occurred but can still see that `numLines` is generated through a pipeline.
As far as the library itself, I personally wouldn't use something like this, though I see the appeal. I turn to statically-typed languages like Golang when a Bash script becomes too kludgey to stand on its own (usually when I need to begin relying on mild data structures) and when I care about type safety and portability more than what Ruby or Python can give me. When I'm writing a Go program, I want something that's testable that I know can work anywhere. With Ruby or Python, unless I'm distributing the script as a Docker image, I have to worry about versions, environments, etc., none of which are pleasant.
However, writing the error boilerplate is annoying, and I can see developers who want to write something quick but spend 99% of their time in Go using this to get something done with a tool they know well. I've seen similar things in the Java world; hell, that's the reason why Groovy and Kotlin exist!
TL;DR: The "wrong thing" in the Bitfield article isn't necessarily wrong; their library is useful, but niche; all hail pattern-matching and the Result type.
- kortex 5y agoPattern matching and Result type are truly among the most revolutionary computer science constructions and I am glad to see their adoption starting to catch on. So much nicer to work with than exception handling or endless propagating of error constants a la Go. Python just got pattern matching (although it lacks pattern-match assignment a la Rust), now it just needs an actual Result type in the stdlib instead of Optional[T] which is really just T|None rather than an actual container. Golang can start delving into Return-type-looking generics but I suspect pattern matching is a long ways out.
- zeugmasyllepsis 5y agoCurious, what is your argument in favor of a container over a type union to represent Result? As long as the type checking system forces users to refine the union to use the value and is aware of blocks of code within type guards (I've heard this referred to as "flow-aware typing" but I'm not sure if that's a standard term) then the advantage of an explicit Result type isn't very obvious to me. What am I missing?
- nunez 5y agoNone != Error would be my beef with it. Sometimes I'm expecting a NoneType in the happy path.
- kortex 5y agoIn terms of ergonomics, I'm more a fan of the fluent style [1] over flow-aware typing with guard blocks. I've never heard of till not but I presume you mean that's like where you have like foo: Optional[Foo] if foo is None: return some_error_handler() bar = foo.qux() # the above guard "upgrades" O[Foo] to Foo For me the real reason I love the ergonomics of the Rust-style "monads [2]" over Unions is that it lets you generically operate on each of 3 orthogonal parts: the value, the error, and the container. Optional or Union[T, E] don't quite hit the same way because you end up having to do more contortions to deal with managing the happy path vs sad path. What ends up often happening is without a container, your type "leaks" into other methods - you start ending up with functions that expect a Union[SomethingMoreSpecific, E], instead of just letting the Result.map(T->U) handle it. Also specifically in Python, the type system is kind of weak (especially those bundled with Pycharm), and a lot of functional operations which ought to be more strongly typed end up with their types erased for whatever reason. In particular functools.partial seems to discard type information more than I'd like. The Result python package [3] doesn't run into this problem near to the same degree. 1] https://martinfowler.com/bliki/FluentInterface.html https://martinfowler.com/bliki/FluentInterface.html 2] I find this term frustrating not only because it's infamously ineffable, but also because there appear to be disagreement whether Rust's containers are actual monads or not, leading me to dub what I call the "Engineer's Monad" which is a generic container you can map and flatmap over, even if isn't strictly an Applicative. 3] https://pypi.org/project/result/ https://pypi.org/project/result/