A little while ago, I suggested to add Ecto.Changeset.optimistic_lock/4 in addition to the already existing Ecto.Changeset.optimistic_lock/3. While José didn’t like the idea, he also pointed out that if he were to consider it, he’d prefer modifying the API so that it would accept an opts Keyword list instead of adding optimistic_lock/4.
Ever since that, I have been wondering: When is it preferable to use an opts Keyword list instead of additional function parameters? What criteria do you usually use to decide which one to pick?
In many cases so far, I have found Keyword lists to be less convenient than »regular« parameters since you can’t do pattern matching on them.
My impression is that they might be better for public-facing interfaces in order to have a cleaner API. I’d then extract the values in the public function and work with them in additional private functions where pattern matching can be used again. What are your thoughts on this? Any rules of thumb that you employ when making the decision?
A function has many parameters, and remembering the order of these is hard. Using a keyword list is nice in this case, since it shows at a function call location what information is used for which purpose inside the function.
A function has many optional parameters.
A function takes some arguments from the list of options, and passes the rest on. (Note that while this is nice in some cases, it is often a bad idea as it does not ‘fail fast’; you won’t notice right away if you made a typo in the option name, for instance)
An important drawback of keyword lists is that when you ‘just’ use them, your function might be passed without some argument that it needs, so depending on where your function is used, you might want to add some descriptive error messages if people forget to pass it.
As you indeed already mentioned, not being able to pattern-match on them, as the keywords might be passed in any order is another drawback. Therefore, while they are nice in an outward-facing API, internally you’ll probably end up calling a high-arity function anyway.
So, my rule of thumb: Use them when it makes the public-facing code more clear, and use them when there are many optional arguments.
In general I would say that, for public functions, whenever you get more than 3 or 4 arguments you should really look into making them all (or at least some of them) a keyword. As @Qqwy mentioned many positional arguments are error prone.
Another, not mentioned yet, but very important use case are boolean arguments. Positional boolean arguments are a horrible API (most of the time), using keywords makes it slightly better since you’re able to name the thing.
Pattern matching on keywords is indeed problematic, but there are ways they can be used cleanly. A solution that I really like and often see in Erlang code, but rarely in Elixir, is reducing over the keyword to build some sanitised state.
This sounds very interesting. How would this look, exactly? Would it implement reduction using primitive recursion (every time building up a one-higher arity function from the matched keyword), or in some way use e.g. Enum.reduce?
One other possibility would be to convert the keyword list to a map before pattern matching. Of course, one has to make a choice about how to handle duplicate arguments when doing that (first wins, last wins, accumulate in a list).