4 ms·
map and filter are often just syntactic sugar for a loop, so I just write a loop. I work on a ~1M LOC codebase in Go and it's really not a problem. map and fi
by NateDad 11y ago
map and filter are often just syntactic sugar for a loop, so I just write a loop.
I work on a ~1M LOC codebase in Go and it's really not a problem. map and filter would not make my life significantly easier. They're solving easy problems.
Sure, I have some of this style code:
var names []string
for _, m := range machines {
names = append(names, m.Name)
}
But, really, is that so much worse than this?
names = [m.Name for m in machines]
Sure, it's spread across a few more lines, but line returns don't cost extra money... and if you decide you want to later do something more inside the loop for each machine (default the name, cache the ID, etc), you can do that trivially by adding lines inside the loop.
This code is not hard code to write. If you're used to just being able to slap out map/filter etc in a single line, I could see how it could be annoying... but it's easy, just write it. There are far more difficult things to figure out in our jobs, why worry about the easy stuff?
- dominotw 11y ago>> But, really, is that so much worse than this? names = [m.Name for m in machines] You can take that code and interpret that as database query, like C# LINQ. you frist example is 'how' vs 'what' of second example. Once you stop telling the computer how to do things and just tell it what you want all kinds of things become possible.
- NateDad 11y agoBut then you have no idea what the computer is actually doing. There's a big performance difference between an in-memory loop and querying a database. This is one of the things I like about Go... what the computer actually does in response to any random line of code is pretty obvious (except for function calls, which of course can do anything). When you hide away the loop inside a map statement, you get people doing dumb things like this: names = [m.Name for m in machines] ids = [m.ID for m in machines] addrs = [m.Address for m in machines] So now we're iterating of over the list of machines 3 times... or making 3 database queries or whatever.
- dominotw 11y ago>But then you have no idea what the computer is actually doing. You trust it the same way you trust go compiler to do the right thing when you give it code to compile.
- NateDad 11y agoThat's the nice thing about Go code... I almost always know exactly what it'll make the computer do. I know how much memory will get allocated, what the likely CPU usage is going to be, etc. The abstraction between the code and the computer is low, which helps lets a lot in understanding why your code is slow, or why it's producing a lot of garbage. "oh hey, here's a loop in a loop... oops, N^2 time".
- prodigal_erik 11y agoWhat the computer actually does is dramatically different between reading a register and reading a block of a mapped file, but I value concisely expressing the intent rather than elaborately repeating mechanics that might change (e.g., I could optimize machines.__iter__ without rewriting all the statements).
- sythe2o0 11y agoIt's worth noting you'd probably want to write this out in Go as follows: names := make([]string, len(machines)) for i, m := range machines { names[i] = m.Name } append() is pretty costly, and should be avoided when possible for performance. This is actually relevant to your how vs what concern-- on the one hand, we could trust the compiler and the builtins to always take the most efficient approach, and only program 'what' we want. That could avoid something like using append in a loop where you know the length of your intended output slice. On the other hand, if there are edge cases which the built-ins take into account that we don't care about and want to ignore, being able to and having the intention of programming 'how' can have performance benefits. I like writing 'how' because I get to benchmark and test different approaches to solve the same problem, whereas 'what' might take a usually-fast approach that isn't a good solution in our specific case.
- NateDad 11y agoYeah, true. And in production, that's what I do. Append is for when you don't know how long a slice will end up being.
- codemac 11y agoIt's not about how hard or easy the code is to write, at all. Any line of code you don't write, is a bug you didn't write either. I worked on a significantly size go codebase and the number of stupid bugs that popped up, like bad loops instead of a simple `filter` function, were just silly. Having all this memory safety and other stupid bugs ruled out but not map, filter, or reduce just felt lazy on the Go authors part. Of course, now that a member of the go team has said it, we'll see a huge shift in what the go team thinks about generics. I'm glad I don't deal with the go community anymore.
- akavi 11y agoYes, that's so much worse. At least to my eyes. It's a death by a thousand cuts. That's 3 dense lines that you need every time you map across a collection. Which obscures whatever else is happening in the rest of the method/function, so now you've got that much more cognitive load to figure out what it's actual core purpose is. Which may be subtly modified by the writer purposefully, but you miss it because you assume it's the same "it's just map idiom". Which may be modified by the writer accidentally, but you miss it because you've trained yourself to parse it as a lump and take its correctness for granted.
- NateDad 11y agoActually readability at the point of use is very reader-friendly: machines := db.GetAllMachines() sort.Sort(byName(machines)) For those not familiar with Go, the sort.Sort line is converting the list of machines into a type that matches the interface that sort.Sort expects. The list of machines is sorted in-place The only part that can be subtly changed is the Less function, which determines the sort order. And as I've said elsewhere... that definition of sort order is something you have to define in any language (for non-trivial types).
- akavi 11y agoWhat does the implementation of `byName` look like? I'm not sure I understand what its return type would be.
- howeman 11y agoEither sort.Interface or a type that implements sort.Interface
- NateDad 11y agobyName is a type. It's a named type based (likely) on a slice of machines. The byName type implements the functions necessary to support the interface that the sort.Sort function requires: type byName []Machine func (b byName) Len() int { return len(b) } func (b byName) Swap(i, j int) { b[j], b[i] = b[i], [b[j] } func (b byName) Less(i, j int) { return b[i].Name < b[j].Name } sort.Sort takes an interface type that has the methods Len, Swap, and Less, as defined in the signatures above, and uses them in a sorting algorithm which sorts the values in-place.
- jackielii 11y agoAnd also you don't have to remember all the different functions, only a for loop to learn.