Let's assume you have a slice of strings you want to uppercase. Which of the following is more readable?
uppercased := slices.Map(inputSlice, strings.ToUpper)
// or
uppercased := make([]string, 0, len(inputSlice))
for _, s := range inputSlice {
uppercased = append(uppercased, strings.ToUpper(s))
}
Let's say you want to parse a bunch of user-given inputs into durations, surely the following is more readable? parsed, err := slices.MapErr(inputstrings, time.ParseDuration)
I think map functions lead to cleaner code when used like the above, and a lot of for loops end up falling into those patterns.In your second example, as a retrofit, I find myself asking "when does MapErr stop consuming the input list?", "which error gets returned if multiple errors could be returned?", "is it a errors.Joim situation", "if there are multiple errors how do I map them to the failed elements in parsed", "if I did want each parse to have either success or fail (think Result type) how would I represent this generically in a language that favours multiple return types"
There's a lot going on with that example that a for loop makes explicit and flexible for other choices.
What I’ve seen in the “for loop” approach that I can’t stand, are things like (pseudo code)
var a = []
for x in coll1 {
a.push(foo(x))
}
do_stuff_with(a)
// … further down the function
for y in coll2 {
a.push(bar(y))
}
do_other_stuff_with(a)
Reading code like that, is the first call to do_stuff_with(a) a bug, because it’s not fully built from both coll1 and coll2 yet? Or is the second call to do_other_stuff_with(a) a bug because a now contains more stuff than the developer probably thought? Can I safely move both loops next to each other, or does that break something subtle? If I need to pass a to a new function, where can I safely do this? Before or after I add from coll2? (In my actual times seeing this, a is really a map of cached key/values or something, and it’s kinda ok that the contents were different each time it was used, but subtle bugs emerged…)IMO the sane way to do it is to just not incrementally mutate things like that, and stick with giving things a single place where they’re defined and initialized. Go doesn’t really help you here because there’s no such thing as immutable data. So just adding Map/collect or whatever doesn’t really buy you much.
Of course if you're Rust the stdlib and compiler might conspire to optimise an operation you wrote which reads as non-mutating into an actual mutation which was faster.
This is another benefit of the "destructive move". If I consume X and spit out Y, the X is gone, so it's OK if secretly I just mutate X and tell you that's Y now.
> Function literals are verbose and inlining is far less agressive.
> Even python shuns map and filter in favor of comprehensions.
That's the problem of the language design. And Python isn't the best language to turn to for language design
> A for loop is more readable than the lambda soup.
A for loop shoving modified items into a temp variable with append() that is then returned is less readable than a map function transforming data. Too bad Go decided to turn lambdas into unreadable soup.
Comprehensions are not a well-designed feature but a consequence of poor design.
Which by the way has no issues being whitespace sensitive and having multiline lambdas.
The only reason Python doesn't support them is Guido not wanting to have them.