26
$\begingroup$

I'd like to implement several behaviors for a particular function using a Method option added to the function definition:

Options[saveData] = {Method -> "Addition"};
saveData[vars_, opts : OptionsPattern[{saveData}]] := 
  saveData[vars, OptionValue[Method]]
saveData[vars_, "Addition"] := Total @ vars
saveData[vars_, "Multiplication"] := Times @@ vars

saveData[{1, 2, 3}, Method->"Multiplication"]
(* 6 *)
saveData[{a, b, c}, Method->"Addition"]
(* a + b + c *)

This seems to work alright, but is probably clunky or fragile in ways that haven't occurred to me. I've seen a number of built-in commands that take this sort of Method option to run different approaches on the same problem. What are good (robust?, simple?, efficient?) patterns for doing this kind of code-switching?

See also: [How to extract a list of available Method-s ]

$\endgroup$
  • 1
    $\begingroup$ If each method has a sufficiently complex implementation, I would have done the same thing; no sense trying to stuff all the complexity into a single monolith. $\endgroup$ – J. M. will be back soon Apr 1 '16 at 18:26
  • $\begingroup$ @dionys Do you plan to pass sub-options to the method option? E.g. saveData[someVars, Method->{"Addition",Method->"CompensatedSummation"}]. $\endgroup$ – Anton Antonov Apr 3 '16 at 13:05
  • $\begingroup$ @AntonAntonov Maybe you can give a answer to demonstrate the strategy/solution that the built-in used with the developer's perspective.:) $\endgroup$ – xyz Apr 3 '16 at 13:31
  • 1
    $\begingroup$ @ShutaoTANG Thanks for the suggestion! I consider posting an answer to this question in the next few days. $\endgroup$ – Anton Antonov Apr 3 '16 at 13:34
  • $\begingroup$ @AntonAntonov Looking forward to your constructive answer. $\endgroup$ – xyz Apr 3 '16 at 13:41
17
$\begingroup$

Introduction

What are good (robust?, simple?, efficient?) patterns for doing this kind of code-switching?

This answer outlines a development strategy that can produce robust and extensible method option handling. Conceptually and development-wise, it is not that simple, but it has been successfully applied in large software projects with complicated dependencies between the algorithms and their corresponding specifications. The options of NIntegrate are a very good example.

(Note that as it was indicated in the comments of the question post, simpler option structures are of interest for OP. )

The main idea is to treat the option values given to Method as sentences from a Domain Specific Language (DSL). One good way to explain this approach is to use of the design pattern Interpreter as a reference, see [GoF94]. The approach is robust because it allows easy extension of the grammar, and easy addition and/or variation of the interpretations of its sentences (option specifications in this case).

For more a general discussion of DSL design and interpretation see this MSE answer. (Alternatively, see [AADSL16].)

Set-up

Given the example in the question formulation let us assume that we want both saveData methods "Addition" and "Multiplication" to take the option "Delimiter" and "Addition" to also take the method sub-option "CompensatedSummation" (which is an option of Total). "CompensatedSummation" can take options on its own, say, "WorkingPrecision".

Strategy steps

Grammar design

First, we come up with a grammar for the Method option. Here is an example using EBNF :

 <method-opt> = ( 'Method' , <rule-arrow> ) | ( <opt-spec> | <core-method> ) ;
 <opt-head> = 'Method' | 'WorkingPrecision' | 'Delimiter' ;
 <core-method> = 'Addition' | 'Multiplication' | 'CompensatedSummation' ;
 <opt-rule> = <opt-head> , <rule-arrow> | ( <core-method> | <opt-value> | <opt-spec> ) ;
 <opt-value> = '_String' ;
 <opt-spec> = '{' | <core-method> , [ ',' | <opt-list> ] | '}' ;
 <opt-list> = <opt-rule> , { ',' | <opt-rule> } ;
 <rule-arrow> = '->' | '\[Rule]' ; 

Parsing

Next we make a parser for this EBNF. This can be done using ad-hoc programming or a parser creation/generation package like FunctionalParsers.m. The parser would produce a tree for each command. Below is a table with the parsing results of multiple commands made with the above grammar. Note that we have produced trees of the option specifications that are easier to traverse using as prescribed by the design pattern Interpreter.

enter image description here

The table above was made using the package FunctionalParsers.m but in the context of this answer its application is only for experimental purposes, to easily derive and try out different grammars. I think it is better the parser to be made in a more ad-hoc manner by applying the prescription of Interpreter to program a parser-interpreter function (or class) for each grammar rule. One benefit is that we can have better, more detailed handling of wrong options. (This is how it was done in NIntegrate.)

Interpretation

At this point we are ready to program the interpretation of the parser result trees. Those trees can be interpreted in different ways in different contexts of data and function signatures.

Example

A real life example of applying this approach -- NIntegrate's Method option -- is discussed in this video between 25:00 and 27:30. For the Method option parsing trees produced in NIntegrate see the section "UPDATE" of this answer of "Determining which rule NIntegrate selects automatically".

Extensions

We might want to have a more elaborated grammar that does not parse incompatible combinations of methods and sub-methods. For example, in the table above the command 3:

"Method -> {Multiplication, Method -> {CompensatedSummation, WorkingPrecision -> 40}}"

is successfully parsed but we probably consider "Multiplication" and "CompensatedSummation" to be incompatible.

Similarly, the command 6:

"Method -> {Addition, Method -> CompensatedSummation, WorkingPrecision -> 34}"

is successfully parsed, but we probably want "WorkingPrecision" to be a valid option only to some of the methods of "Addition".

These observations lead us to the conclusion that the grammar was too simple and general since it allows too many incorrect sentences. At this point we need to decide (1) do we handle the incorrect sentences at the interpretation phase, or (2) do we make a more complicated grammar that allows successful parsing of only meaningful combinations.

I would say for both development directions it is better to have a rigorous grammar that allows only meaningful commands. The EBNF of such a grammar can be also seen as a compact API specification and functionality design.

Here is a such more detailed grammar:

 <method-opt> = ( 'Method' , <rule-arrow> ) |> <opt-spec> ;
 <opt-spec> = <add-method> | <mult-method> ;
 <opt-head> = 'Method' | 'WorkingPrecision' | 'Delimiter' ;
 <add-method> = 'Addition' | '{' | 'Addition' , [ ',' | <add-opt-list> ] | '}' ;
 <mult-method> = 'Multiplication' | '{' | 'Multiplication' , [ ',' | <mult-opt-list> ] | '}' ;
 <compsum-method> = 'CompensatedSummation' | '{' | 'CompensatedSummation' , [ ',' | <compsum-opt-list> ] | '}' ;
 <add-opt-list> = <add-opt-rule> , { ',' | <add-opt-rule> } ;
 <mult-opt-list> = <mult-opt-rule> , { ',' | <mult-opt-rule> } ;
 <compsum-opt-list> = <compsum-opt-rule> , { ',' | <compsum-opt-rule> } ;
 <add-opt-rule> = ( 'Method' | 'Delimiter' ) , <rule-arrow> | ( <opt-value> |  <compsum-method> ) ;
 <mult-opt-rule> = 'Delimiter', <rule-arrow> |> <opt-value> ;
 <compsum-opt-rule> = 'WorkingPrecision', <rule-arrow> | <opt-value> ;
 <opt-value> = '_String' ;
 <rule-arrow> = '->' | '\[Rule]' ;

References

[GoF94] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, 1994, Addison-Wesley.

[AADSL16] Anton Antonov, "Creating and programming DSLs", (2016), MathematicaForPrediction blog at WordPress.

[AAFP] Anton Antonov, Functional Parsers topic at MathematicaForPrediction blog at WordPress.

$\endgroup$
  • $\begingroup$ Thanks for your detailed explanation. I would like to know that which file should I save for the EBNF defintions. $\endgroup$ – xyz Apr 5 '16 at 15:00
  • $\begingroup$ @ShutaoTANG (1) "Thanks for your detailed explanation." - Sure! (2) "[...] which file should I save for the EBNF" - I am not sure what do you mean. You can use text files for EBNFs. In Mathematica, I just assign EBNF grammars to strings. $\endgroup$ – Anton Antonov Apr 5 '16 at 15:06
  • $\begingroup$ If I write the EBNF defintion in the Mathematica notebook or *.m file, Mathematica will show syntax error $\endgroup$ – xyz Apr 5 '16 at 15:14
  • $\begingroup$ @ShutaoTANG As I mentioned, in Mathematica you can use strings for EBNF. $\endgroup$ – Anton Antonov Apr 5 '16 at 15:56
9
$\begingroup$

Normally this would be comment. but I post it here so there will be an answer.

There is nothing wrong with the way you implemented your function. Your code is robust, simple, and efficient and IMO follows good Mathematica functional programming style.

$\endgroup$
6
$\begingroup$

In general, I using the following strategy:

  • Using a varible $MethodValues to store all the possible method value.

  • Using If[] and Return[$Failed] to check the validness of method value that user given, which can make the code more robust.

  • Using Switch[]/Which[] to deal with each case.


Options[saveData] = {Method -> Automatic};
$MethodValues = {"Addition", "Multiplication", Automatic};

saveData::bdmtd = 
   "Value of Method -> `` is not one of " <> ToString@$MethodValues;

saveData[vars_, opts : OptionsPattern[]] :=
  Module[{mtd},
   mtd = OptionValue[Method];
   If[! MemberQ[$MethodValues, mtd],
    Message[saveData::bdmtd, mtd];
    Return[$Failed]
   ];
   Switch[mtd,
    Automatic | "Addition",
    Total@vars,
    "Multiplication",
    Times @@ vars
   ]
 ]

Let's test it

saveData[{1, 2, 3}, Method -> "1"]
saveData[{1, 2, 3}, Method -> "Multiplication"]
saveData[{a, b, c}, Method -> "Addition"]

enter image description here


When the function saveData[] only owns a option Method, the OP's implementation maybe reasonable. However, when function owns many options, the OP's solution will be complicated.

$\endgroup$
  • $\begingroup$ Conditionals like Which[] would be fine if the individual cases/methods are not too complicated; otherwise you end up with a monolith that is a nightmare to read and/or maintain. $\endgroup$ – J. M. will be back soon Apr 2 '16 at 2:20
  • $\begingroup$ @J.M. According to my practice, the values of Method option that I deal with are not too complicated. Namely, just a few of elements like {md1,md2...,mdn}., where $n<6$. So I would like to know how did you deal with many method value, such as NDSolve[]'s Method option? $\endgroup$ – xyz Apr 2 '16 at 2:27
  • 1
    $\begingroup$ @ShutaoTANG The implementation of the Method options of NIntegrate and NDSolve are discussed in this video between 25:00 and 27:30. The Method option of NIntegrate was designed by using the OOP design pattern Interpreter. $\endgroup$ – Anton Antonov Apr 3 '16 at 12:24
  • $\begingroup$ @AntonAntonov Thanks a bunch! Since you were a developer of WRI before, so I would like to know: (1) which language(Wolfram Language?) was used to implement NIntegrate[] or NSolve[]? (2) Recently, I have been developing a CAGD(Computer Aided Geometry Design) package, I used the Wolfram Language , could you give me some advice to improve the performance of my functions? $\endgroup$ – xyz Apr 3 '16 at 13:24
  • $\begingroup$ @ShutaoTANG (1) It was/is a mixture of C and Wolfram Language. (2) Performance for CAGD is outside of my expertise. I would say though, that using OOP Design Patterns has helped me a lot to make designs that provide good abstractions, ability for performance tuning, and transition to other developers. $\endgroup$ – Anton Antonov Apr 3 '16 at 13:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.