NMath API updated with .NET Func<> delegates

At CenterSpace we are always working hard to keep the core NMath processing kernel start of the art, however changes to our libraries’ API usually lag behind any .NET framework developments in a process of deprecation and introduction. The .NET Func<> and Action<> delegates have been a part of the .NET framework now since .NET 3.5 and VS2008. Therefore, we are now deprecating all of our custom delegates in the NMath API and replacing them with .NET Func<> generic delegates. This improves interoperability between NMath and our customers’ code and between our C# API and other .NET languages. Customers will see this change in the NMath 5.2 and NMathStats 3.5.

For those not familiar with the usage and definition of Func<> types within .NET there is a short primer at the end of this post.

Compiler Warning for [Obsolete] NMath defined delegates

All of our NMath delegate types have been deprecated using the .NET [Obsolete()] attribute. The generated compiler warnings will provide the suggested Func type replacement.

Warning 52 'CenterSpace.NMath.Core.NMathFunctions.FloatUnaryFunction'
   is obsolete: 'Use Func< float, float >'   ...

To further clarify, the following table lists the deprecated NMath float delegate types side by side with their replacements. All NMath delegates should be redefined in your code using the generic Func types.

List of float-precision deprecated NMath delegate types
Old NMath delegates Replacement
float FloatFunction() Func< float >
float FloatUnaryFunction( float x ) Func< float, float >
float FloatBinaryFunction( float x, float y ) Func< float, float, float >
float FloatIntFunction( float x, int y ) Func< float, int, float >
float FloatVectorFloatFunction( FloatVector v ) Func< FloatVector, float >
FloatVector FloatVectorFloatVectorFunction( FloatVector x ) Func< FloatVector, FloatVector >

There are many other deprecated NMath delegates that should be similarly replaced in your code.

Code examples for updating your code

Where previously you might have created a delegate that takes a double argument and returns a double like this:

NMathFunctions.DoubleUnaryFunction soln =
   delegate(double x) {return (x*x*x) / 3.0 + y0; };

you should now do it like this:

Func soln =
   new Func< double, double >(  x => (x*x*x) / 3.0 + y0 );

All methods in NMath and NMath Stats now take Func<> type delegates. If you use the old NMath delegates types to call a NMath method, an obsolescence warning will be issued by the compiler.

NMath and NMath Stats contain many functors that have now been obsolesced and redefined using a generic Func<> type. The new functor naming convention simply drops the “tion” from “Function” to make the new name. For example, the delegate NMathFunctions.AtanFunction should now be replaced with NMathFunctions.AtanFunc. The old delegate has a type of NMathFunctions.DoubleUnaryFunction, and the new delegate has a type of Func< double, double >. This kind of replacement can be done safely with a global search and replace operation to rid yourself of compiler obsolescence warnings. The table below list a few functor redefinitions that demonstrate the renaming pattern that was applied across the API.

Sampling of deprecated NMath functors
Old NMath functor Replacement
DoubleUnaryFunction AbsFunction Func< double, double > AbsFunc
DoubleUnaryFunction AcosFunction Func< double, double > AcosFunc
DoubleUnaryFunction AsinFunction Func< double, double > AsinFunc
DoubleBinaryFunction PowFunction Func< double, double, double > PowFunc
DoubleIntFunction RoundFunction Func< double, int, double > RoundFunc
DoubleVectorDoubleFunction DoubleNaNMedianFunction Func< DoubleVector, double > DoubleNaNMedianFunc

Potential Issues

The deprecation process requires us to maintain the old custom delegate defintions in the API methods while introducing the new Func<> based API. As a result, every method call in our API which previously consumed a custom NMath delgate type, a functionally identical signature was added which consumes a Func<> based delegate. This duplication can cause an easy-to-fix compiler error in situations when an anonymous delegate was used instead of a NMath delegate type. Consider the follow line of code which inverts the singular values of a SVD.

      DoubleVector wInverse =
         svDecomp.SingularValues.Apply(
              delegate( double w )
                 { return w < eps ? 0.0 : 1.0 / w; }
         );

Written this way will now cause the following compiler error.

Error	43
The call is ambiguous between the following methods or properties:
'CenterSpace.NMath.Core.DoubleVector.Apply(
    CenterSpace.NMath.Core.NMathFunctions.DoubleUnaryFunction )'
and
'CenterSpace.NMath.Core.DoubleVector.Apply( System.Func )' ....

In this error message, the first Apply() is the old method signature and the second Apply() is the new Func<> based signature. The C# compiler can't choose between the two when provided an anonymous delegate. Exactly why this ambiguity error is generated by the compiler is carefully explained by Eric Lippert over on stackoverflow. Briefly, the ambiguity error arises because both methods equally satisfy the Apply() method signature and the C# standard does not provide a way to judge which would be 'better', and so reports an error. The fix is simple, we just specified explicitly that we want to use the method that takes a generic Func<>.

      DoubleVector wInverse =
         svDecomp.SingularValues.Apply(
              new Func< double, double >
                 ( delegate( double w )
                    { return w < eps ? 0.0 : 1.0 / w; }
                 )
         );

Or, somewhat more concisely using a lamda expression.

      DoubleVector wInverse =
         svDecomp.SingularValues.Apply(
              new Func< double, double >
                 ( w => w < eps ? 0.0 : 1.0 / w  )
         );

A brief Func<> & Action<> primer

The generic delegate types, Func<> and Action<>, were introduced with the .NET 3.5 framework to unify delegate types across various .NET libraries. This allows a .NET library developer like CenterSpace to create an API that accepts generic delegates that are defined elsewhere in client code. The Func and Action are differentiated by their return values: Actions have no return value, Funcs always have a single return value of type TResult. For each of them, the .NET framework defines a type signature that takes between 0 and 16 generically typed arguments.

.NET Func<> and Action<> definitions
Func Action
TResult Func() void Action<>()
TResult Func( T arg ) void Action( T arg )
... ...
TResult Func( T1 arg1, T2 arg2, ... T15 arg15, T16 arg16 ) void Action( T1 arg1, T2 arg2, ... T15 arg15, T16 arg16 )

This is still all abstract. Here's how we create a Func using a lambda expression.

Func mySquaringFunc =
   new Func< double, double> ( x =>  x*x ); 

double xSquared = mySquaringFunc( 45 );  // xSquared = 2025

This Func delegate takes single double value and returns a double value. Lamda expressions are commonly used today to generate delegates, however an anonymous delegate could also be used for the same purpose.

Func mySquaringFunc =
   new Func< double, double> ( delegate( double x ) { return x*x; } ); 

double xSquared = mySquaringFunc( 45 );  // xSquared = 2025

I find the lambda syntax to be more expressive and succinct. From the table above, T1 = double, and TResult = double.

Action types are not used within the NMath API because we are always dealing with functions and return values. However, Actions are similarly defined, and may be used for example in queuing code where a queue manager takes work in the form of Action delegates.

Action< DoubleVector > FFTWorker =
   new Action< DoubleVector >( v =>
      {
         DoubleComplexForward1DFFT fft = new DoubleComplexForward1DFFT( v.Length );
         fft.FFTInPlace( v );
       } );
...
QueueManager.AddWork( FFTWorker, bigDataVector );

The Action above computes the in-place FFT, and the QueueManager can now generically schedule this work on the queue.

Happy Computing,
Paul Shirkey

4 thoughts on “NMath API updated with .NET Func<> delegates

  1. Thanks, this makes sense.

    Another .net thing that would be good for you to support would be the conventional ToArrary() and ToList() functions on double vectors. You could achieve this by making double vector implement IEnumerable.

    Also maybe to add a ToVector() extension method to IEnumerable and IEnumerable, and a ToMatrix on double[,].

    We have done these internally using extension methods, and it means that we get very clear code when translating between nMath objects and native collections.

    We tend to expose native collections as the inputs and outputs at the outer layer of our libraries, so this translation tends to happen just a couple of times and not in the middle of performance critical code which just uses nMath objects or native objects and doesn’t switch between the two.

  2. By the way, your commenting system filters out angle brackets, which is a bit unfortunate when posting .net generics code. My examples in the previous comment were meant to be generic IEnumerables of double and float, but it just left them as IEnumerable

  3. We appreciate the input Mark. We have a ticket to implement IEnumerable for both the float and double vector types. I agree that will noticeably improve interop between NMath types and native .NET types, and enable users to use LINQ expressions across our types.

    I’ll check on the angle bracket filtering – that’s not good for a programming blog!

    Best, Paul

Leave a Reply

Your email address will not be published. Required fields are marked *

Top