The problem
It is at times necessary to create functions that take a large number of arguments. This can make using and refactoring such functions awkward, as the Wolfram Language does not have built-in named parameters, instead relying on position. For example, one can define a function
pets[catName_String,dogName_String]:=(*body*)
but if your cat is named Felix, and your dog is Rover, then the function must always be called pets["Felix","Rover"]
and never pets["Rover","Felix"]
. Not much of a problem for two arguments, but if there are many, it can make for bug prone code.
Associations give us a neat way of implementing named arguments. We could redefine the above function as
pets[petNames_Association]:=(*body*)
and call it using an association e.g. pets[<|"dog" -> "Felix", "cat" -> "Rover"|>]
, and it will now not matter which order we pass the pets' names in, as petNames["cat"]
and petNames["dog"]
in the function body will return the correct pet name.
However in solving one problem we have introduced another: it is now no longer clear from the left hand side of the function what the arguments should be (beyond being supplied as an Association). This could be a real problem for anyone else using the code. Furthermore we can no longer use pattern matching to restrict the 'types' of arguments - e.g. we could call this function with pets[<|"dog" -> 1.234, "cat" -> 5.678|>]
and it would attempt to evaluate.
The solution
With a bit of experimenting, I've found what I think is a nice way of implementing named function parameters using associations, in such a way as to make the parameter names explicit, and also allow for pattern matching on the values. First define a function:
AssociationContaining[a_Association,
keyPatterns : {(_String -> (_Pattern | _PatternTest | _Blank)) \
..}] := MatchQ[a, KeyValuePattern[keyPatterns]]
... and then make a second definition that makes use of 11.3's new Curry function:
AssociationContaining[
keyPatterns : {(_String -> (_Pattern | _PatternTest | _Blank)) ..}] :=
Curry[AssociationContaining][keyPatterns]
AssociationContaining is a predicate function (i.e. returns True or False) that takes an association, and a list of keys and value-patterns, and uses the built-in MatchQ and KeyValuePattern to determine whether the given association contains all of the key/pattern pairs in the list.
And now we can use AssociationContaining in its curried form to implement pattern matching on named function arguments, for example:
pets[petNames_?(AssociationContaining[{"cat" -> _String, "dog" -> _String}])] := (*body*)
We can access the argument values in the function body as expected, i.e. petNames["cat"]
and petNames["dog"]
. But now the function will only attempt to evaluate if the supplied association contains "cat" and "dog" keys, and both are strings. So the function call
pets[<|"dog" -> "Rover", "cat" -> "Felix" |>]
will evaluate, as will
pets[<|"cat" -> "Felix", "dog" -> "Rover" |>]
However attempting to call
pets[<|"cat" -> 1.234, "dog" -> "Rover"|> (* OR *) pets[<|"cat" -> "Felix" |>]
will not evaluate, thanks to the pattern matching restrictions placed on the arguments.
I hope this proves useful to someone. I had great fun playing around with this, and for me it is another illustration of how powerful and flexible the Wolfram Language is.