Message Boards Message Boards

Wolfram in the Enterprise: Math functions that fail with bad input

Posted 3 years ago

After using the Wolfram Language and Mathematica for over a year now I have a love-hate relationship with how easy the Wolfram Language transitions between symbolic and numeric math.

When exploring data, the seamless transition between symbolic and numerical math is awesome. But, once I have figured out how to do what I want to do, I then need to create an application that automates the generation of many reports with numbers without passing through symbolic representations. Since the applications that I write often run at least once a day and can run for hours, I have the following requirements:

  • The program should abort immediately when it encounters data that does not make sense
  • No one should have to do a quality check of each report (there could be hundreds) before it is published. That would eliminate much of the efficiency of the solution.
  • I should not have to add extra code throughout the application to handle bad data. Instead, I want to call a set of consistently defined functions that abort instead of returning symbolic results.

Before I go on, I should explain the primary reasons my applications tend to fail:

  • The source system changed data formats and did not communicate the change
  • The source system does not have rigorous input checks and bad data slips through

So, I sometimes find myself troubleshooting a failure after the application having worked great for weeks, months or even years.

Since I love to write code that writes code, I developed the proof-of-concept below that I would love to get some constructive criticism from the community.

As my proof-of-concept, I'm going to create alternative functions for simple math functions that take a single argument.

Now for the code that writes code. Please note that this code is part of a package called wwMath`.

buildCode[code_String, args:___] := ToExpression[
    ToString[
       StringForm[
         code,
         args
       ]
    ]
];

createReplacementFunction[functionName_String] := Module[
    {
       replacementFunctionName = "wwMath`wwN" ~~ functionName
    },
    Unprotect["wwMath`*"];

    buildCode[
       "Attributes[`1`] = {Listable, NumericFunction}", 
       replacementFunctionName
    ];

    buildCode[
      "`1`[value_?NumericQ] := `2`[value] // N", 
      replacementFunctionName,
        functionName
    ];

    buildCode[
       "`1`[value_?MissingQ] := value", 
      replacementFunctionName,
        functionName
    ];

    buildCode[
       "`1`[args:___] := msgDefinitionNotFound[\"" ~~ functionName ~~ "\", args]", 
       replacementFunctionName
    ]

];

I can use createReplacementFunction[] to generate consistent code that is ready for unit testing as follows:

createReplacementFunction["Floor"];
createReplacementFunction["Ceiling"];
createReplacementFunction["Cos"];
createReplacementFunction["Sin"];
createReplacementFunction["Abs"];

To take a peak at the generated code, we can use Definition[wwNFloor] and we get:

{
 {Attributes[wwNFloor] = {Listable, NumericFunction, Protected}},
 { },
 {{
   {wwNFloor[wwMath`Private`value_?NumericQ] := N[Floor[wwMath`Private`value]]},
   { },
   {wwNFloor[wwMath`Private`value_?MissingQ] := wwMath`Private`value},
   { },
   {wwNFloor[wwMath`Private`args___] := msgDefinitionNotFound["Floor", wwMath`Private`args]}
  }}
}

To handle aborting with an explanation, I defined msgDefinitionNotFound[] as follows:

msgArg::DefinitionNotFound = "`1` does not have definition for arguments: `2`";

msgDefinitionNotFound[functionName_String, args_List] := Module[
    {},
    Message[msgArg::DefinitionNotFound, functionName, args];
    Abort[];
];

msgDefinitionNotFound[functionName_String, args_] := Module[
    {},
    Message[msgArg::DefinitionNotFound, functionName, args];
    Abort[];
];

msgDefinitionNotFound[functionName_String, args_] := Module[
    {},
    Message[msgArg::DefinitionNotFound, functionName, args];
    Abort[];
];

msgDefinitionNotFound[args:___] := msgDefinitionNotFound["msgDefinitionNotFound", args];

Of course, we need unit tests for the generated functions. Since I'm using Wolfram Workbench, I get to use the Wolfram Unit Tester. While we might need some specialized unit testing on specific functions, we still need to test the standard test cases of all generated functions. So, let's encapsulate the standard test cases in a function that we can reuse:

testMathFunctionReplacement[
    replacementFunction_String -> systemFunction_String
] := Module[
    {
       randomReals = RandomReal[{-4 * Pi, 4 * Pi}, 10],
       randomIntegers = RandomInteger[{-100, 100}, 10]
    }
    ,
    Test[
       ToExpression["Attributes[" ~~ replacementFunction ~~ "]"]
       ,
       {Listable, NumericFunction, Protected}
       ,
       TestID -> replacementFunction ~~ ": Attributes"   
    ];
    Test[
       Map[ToExpression[replacementFunction], randomReals] // N
       ,
       Map[ToExpression[replacementFunction], randomReals] // N
       ,
       TestID -> replacementFunction ~~ ": Reals"
    ];
    Test[
       Map[ToExpression[replacementFunction], randomIntegers] // N
       ,
       Map[ToExpression[systemFunction], randomIntegers] // N
       ,
       TestID -> replacementFunction ~~ ": Integers"
    ]; 
    Test[
       Map[ToExpression[replacementFunction], {0}] // N
       ,
       Map[ToExpression[systemFunction], {0}] // N
       ,
       TestID -> replacementFunction ~~ ": Zero"
    ];

    Test[
       ToExpression[replacementFunction][randomIntegers] // N
       ,
       ToExpression[systemFunction][randomIntegers] // N
       ,
       TestID -> replacementFunction ~~ ": Listable Integers"
    ];
    Test[
       ToExpression[replacementFunction][randomReals] // N
       ,
       ToExpression[systemFunction][randomReals] // N
       ,
       TestID -> replacementFunction ~~ ": Listable Reals"
    ];
    Test[
       ToExpression[replacementFunction][Missing[]]
       ,
       Missing[]
       ,
       TestID -> replacementFunction ~~ ": Missing"
    ];
    Test[
       CheckAbort[ToExpression[replacementFunction][Infinity], $Failed]
       ,
       $Failed
       ,     
       {msgArg::DefinitionNotFound}
       ,
       TestID -> replacementFunction ~~ ": Infintiy"
    ];     
    Test[
       CheckAbort[ToExpression[replacementFunction][-Infinity], $Failed]
       ,
       $Failed
       ,     
       {msgArg::DefinitionNotFound}
       ,
       TestID -> replacementFunction ~~ ": Negative Infintiy"
    ]; 

    Test[
       CheckAbort[ToExpression[replacementFunction][], $Failed]
       ,
       $Failed
       ,     
       {msgArg::DefinitionNotFound}
       ,
       TestID -> replacementFunction ~~ ": No Parameters"
    ];     
    Test[
       CheckAbort[ToExpression[replacementFunction]["abc"], $Failed]
       ,
       $Failed
       ,     
       {msgArg::DefinitionNotFound}
       ,
       TestID -> replacementFunction ~~ ": Invalid Paramaters"
    ];     
];

testMathFunctionReplacement[systemFunction_String] := With[
    {
       replacementFunction = "wwN" ~~ systemFunction
    },
    testMathFunctionReplacement[replacementFunction -> systemFunction]
];

testMathFunctionReplacement["Floor"];
testMathFunctionReplacement["Ceiling"];
testMathFunctionReplacement["Sin"];
testMathFunctionReplacement["Cos"];
testMathFunctionReplacement["Abs"];

While my solution will allow me to write applications that are more likely to abort without producing bad reports, I am not yet convinced that this is the best solution, or even a good solution. But I do know that I have to solve this problem; otherwise, it will be difficult to convince my peers and bosses to make the switch to the Wolfram Language.

I would love to hear back from others in a similar situation.

Have a great weekend.

POSTED BY: Mike Besso
Reply to this discussion
Community posts can be styled and formatted using the Markdown syntax.
Reply Preview
Attachments
Remove
or Discard

Group Abstract Group Abstract