Message Boards Message Boards

Use Associations as function arguments?

GROUPS:

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.

POSTED BY: Robert Ferguson
Answer
3 months ago

I may be missing a point, but can’t you do something similar using named optional arguments? This is described in a documentation tutorial titled “Setting up Functions with Optional Arguments”. Just a thought.

POSTED BY: Ian Williams
Answer
3 months ago

Group Abstract Group Abstract