I wish I liked using lo. People at work swear by it but whenever I use it, I just get annoyed at how much better it could be.

For those unaware of what lo is, it’s a Go package inspired by lodash, the utility library for JavaScript that allows one to use higher-order functions for operating over collections. Think mapping, filtering, etc. Given that lodash is built for JavaScript, it’s allows for a lot of flexibility in the closures passed to the various functions. map, for instance, allows a closure that takes two arguments which, when used with an array, will be set to each item’s value and index. If you need both bits of information, define a closure with two arguments. If you just need the value, just use one.

Given Go’s strong type system, this flexibility is not granted to us Go developers. Whatever the closure signature is defined for Map or Filter is the one you’re stuck with (well, that’s not entirely true, but more on that in a bit). So it’s important for the library developer to come up with some good defaults.

I’m sure the developer of lo was trying to be helpful by defining Map and Filter to accept a closure that takes the item’s value and index. But by doing so, they totally screwed me over for the simple reason in that they’re not consistent. I can’t pass the same function into both Filter and EveryBy , as the latter does not accept a closure with an index.

This prevents me from predefining a set of predicates that I can reused. Being able to define a closure in Go is nice, but you know what’s better? Not having to define a closure. Riddle me this: which of these is nicer to read? This:

func isRect(shape Shape) bool {
    return shape.corners == 4
}

func doThingsWithShapes(shapes []Shape) {
    rects := lo.Filter(shapes, isRect)
    areAllRects := lo.EveryBy(shapes, isRect)
}

Or this?

func doThingsWithShapes(shapes []Shape) {
    rects := lo.Filter(shapes, func(shape Shape, _ int) bool {
        return shape.corners == 4
    })
    areAllRects := lo.EveryBy(shapes, func(shape Shape) bool {
        return shape.corners == 4
    })
}

Moot point, as the first one is impossible.

The mistake of Map providing two arguments is subtler. When I’m operating on a database, I’m usually find myself having to map from the types generated from sqlc into my own models. Moving this logic out into a separate function makes sense, one that I can invoke when I’m dealing with a result set — in which case I could pass this mapping function to Map — or a single row — in which case I just call function directly. But requiring the Map closure to take the index screws me here too. I could defining a mapping function that accepts an index that I won’t use, but that just makes the direct calls look rubbish. Am I just going to pass in 0 here? Urgh!

Now, I alluded to the function signatures potentially being a little more flexible. One could use the method of currying which will return a function that will take the first argument and ignore the second. This could allow the same predicate to be used in both Filter and EveryBy:

func ignoreSecondArg[T, U, V any](fn func(T) V) func(T, U) V {
  return func(t T, _ U) V {
    return fn(t)
  }
}

func isRect(shape Shape) bool {
    return shape.corners == 4
}

func doThingsWithShapes(shapes []Shape) {
    rects := lo.Filter(shapes, ignoreSecondArg(isRect))
    areAllRects := lo.EveryBy(shapes, isRect)
}

It would’ve been nice for lo to provided these functions. The closest I see are the Partial functions which almost do what I need. Yet they’re designed for pegging the first argument to a value and returning a function that takes the second one. I’ve have no need for these sorts of function, and having something which could help in adapting these predicates would seem much more useful to me.

(Deep breath)

Okay, I can see how this all looks a little unfair. And I’m not going to say the developer or users are making mistakes here. Well, I am, but I’m not say that they’re incompetent or have malicious intent. I’m sure there are good reasons behind these decisions.

It’s just… this could be so much better than it currently is. Consider this criticism from someone who’d like to see this package succeed, like a parent or a coach trying to help a struggling child. We’re not using JavaScript here; we’re bound to the type system we have. Embrace that. Recognise that the occasional closure is fine but can get unwieldily whenever we’re forced to make one with one of your functions. The flexibility you provide here is an example of the perfect being the enemy of the good.