Skip to content

Advanced Techniques

Shared Members

Certain .NET classes provide methods, fields, and properties that can be called directly without the need to create an instance of the class first. These members are known as shared, because they have the same definition for the class and for any instance of the class.

The methods Now and IsLeapYear exported by System.DateTime fall into this category.

Example

     ⎕USING←,⊂'System'

     DateTime.Now
18/03/2020 11:14:05

     DateTime.IsLeapYear 2000
1

APL Language Extensions for .NET Projects

.NET provides a set of standard operators (methods) that are supported by certain classes, for example, methods to add and subtract .NET objects and methods to compare two .NET objects.

Example 1: DateTime – Adding and subtracting

The op_Addition and op_Subtraction operators add and subtract TimeSpan objects to DateTime objects:

      DT3←System.DateTime.Now
      DT3
15/02/2024 10:35:35
      TS←⎕NEW TimeSpan (1 1 1)
      TS
01:01:01
      DateTime.op_Addition DT3 TS
15/02/2024 11:36:36
      DateTime.op_Subtraction DT3 TS
15/02/2024 09:34:34

Example 2: DateTime – Comparing

The op_Equality and op_Inequality operators compare two DateTime objects:

      DT1←⎕NEW DateTime (2024 4 30)
      DT2←⎕NEW DateTime (2024 1 1)

      ⍝ Is DT1 equal to DT2?
      DateTime.op_Equality DT1 DT2
0

Some corresponding APL primitive functions have been extended to accept .NET objects as arguments and call these standard .NET methods internally. The methods and the corresponding APL primitives that are currently available are:

.NET Method APL Primitive Function
op_Equality = and
op_Inequality and

This means that Example 2 becomes:

      DT1←⎕NEW DateTime (2024 4 30)
      DT2←⎕NEW DateTime (2024 1 1)

      ⍝ Is DT1 equal to DT2?
      DT1 = DT2
0

Information

Calculations and comparisons performed by .NET methods are performed independently from the values of APL system variables (such as ⎕FR and ⎕CT).

Exceptions

When a .NET object generates an error, it does so by throwing an exception. An exception is a .NET class whose ultimate base class is System.Exception.

The system constant ⎕EXCEPTION returns a reference to the most recently generated exception object.

For example, if you attempt to create an instance of a DateTime object with a year that is outside its range, the constructor throws an exception. This causes APL to report a (trappable) EXCEPTION error (error number 90) and access to the exception object is provided by ⎕EXCEPTION.

      ⎕USING←'System'
      DT←⎕NEW DateTime (100000 0 0)
EXCEPTION: Year, Month, and Day parameters describe an un-representable DateTime.
      DT←⎕NEW DateTime (100000 0 0)
         ^       
      ⎕EN
90
      ⎕EXCEPTION.Message
Year, Month, and Day parameters describe an un-representable DateTime.

      ⎕EXCEPTION.Source
System.Private.CoreLib

      ⎕EXCEPTION.StackTrace
at System.DateTime.DateToTicks(Int32 year, Int32 month, Int32 day)
at System.DateTime..ctor(Int32 year, Int32 month, Int32 day)

Information

The result of ⎕EXCEPTION.StackTrace can depend on the exact version of .NET – your result might look different, but if it includes System.DateTime..ctor(Int32 year, Int32 month, Int32 day) then it is showing the correct exception for this example.

Specifying Overloads

If a .NET function is overloaded in terms of the types of arguments that it accepts, then Dyalog chooses which overload to call depending on the data types of the arguments passed to it. For example, if a .NET function foo() is declared to take a single argument either of type int or of type double, Dyalog would call the first version if you called it with an integer value and the second version if you called it with a floating-point value.

Occasionally it might be desirable to override this mechanism and explicitly specify which overload to use. This can be done by calling the function and specifying the variant operator () with the OverloadTypes option. This takes an array of references to .NET types, of the same length as the number of parameters to the function.

Example

To force APL to call the double version of function foo() irrespective of the type of the argument val, enter:

      (foo ⍠('OverloadTypes'Double))val

or (more simply):

      (foo ⍠Double)val

where Double is a reference to the .NET type System.Double.

      ⎕USING←'System'
      Double
(System.Double)

Taking this a stage further, suppose that foo() is defined with five overloads as follows:

foo()
foo(int i)
foo(double d)
foo(double d, int i)
foo(double[] d)

The following statements will call the niladic, double, (double, int) and double[] overloads respectively:

(foo ⍠ (⊂⍬)) ⍬                               ⍝ niladic
(foo ⍠ Double) 1                             ⍝ double
(foo ⍠(⊂Double Int32))1 1                    ⍝ double,int
(foo ⍠(Type.GetType ⊂'System.Double[]'))⊂1 1 ⍝ double[]

Overloaded Constructors

If a class provides constructor overloads, then a similar mechanism is used to specify which of the constructors is to be used when an instance of the class is created using ⎕NEW.

For example, if MyClass is a .NET class with an overloaded constructor, and one of its constructors is defined to take two parameters; a double and an int, then the following statement would create an instance of the class by calling that specific constructor overload:

      (⎕NEW ⍠ (⊂Double Int32)) MyClass (1 1)

Generics

In .NET, a method, interface, or class can be generic, which means that it is a template or recipe for a concrete method, interface, or class. What makes them generic is that they have a list of type parameters; the user must apply a matching number of type arguments to create a concrete version. In the case of methods, it is not always necessary to apply type arguments, as the .NET interface can sometimes perform type inference to deduce the type arguments from the types of the method arguments.

Syntax

The syntax used to apply type arguments to methods, classes, and interfaces, is square brackets, for example G[T] where G is the generic entity, and T is a .NET type or a vector of .NET types. The types could be the result of applying types to a generic .NET class.

The square bracket syntax means that working with generics in Dyalog APL and C# looks visually similar, except that C# uses angle brackets, as illustrated by the example below:

// Instantiate a concrete version of a generic class in C#
new System.Collections.Generic.List<System.Int32>();

// Call a concrete version of a generic method in C#
System.Decimal.CreateChecked<System.Int32>(5);

The corresponding APL is:

⍝ Instantiate a concrete version of a generic class in APL
⎕NEW System.Collections.Generic.List[System.Int32]

⍝ Call a concrete version of a generic method in APL
System.Decimal.CreateChecked[System.Int32] 5

Creating a Concrete Version of a Generic Class

The class System.Collections.Generic.List is a generic class with one type parameter, which is the type of the elements of the list. The display form of the type indicates that it is generic:

      ⎕USING←''
      System.Collections.Generic.List
(System.Collections.Generic.List[T])

A concrete version of the List class can be created using square brackets. For example, a list class that contains integers can be created as follows:

      ⎕USING←''
      IntList←System.Collections.Generic.List[System.Int32]
      IntList
(System.Collections.Generic.List[System.Int32])

The shared members of the IntList class can then be accessed, and the class instantiated using ⎕NEW.

It is not necessary to give the constructed class a name before creating instances of it. Multiple type arguments can also be specified. For example:

      ⎕USING←''
      types←System.Char System.Int32
      ⎕NEW System.Collections.Generic.Dictionary[types]
System.Collections.Generic.Dictionary`2[System.Char,System.Int32]

Attempting to instantiate a generic class without the expected number of type arguments generates an error. For example:

      ⎕USING←''
      ⎕NEW System.Collections.Generic.List
LENGTH ERROR: No overload of the type expects the given number (0) of generic type arguments
      ⎕NEW System.Collections.Generic.List
      ∧

Similarly, applying too many type arguments also results in an error:

      ⎕USING←''
      System.Collections.Generic.List[3⍴System.Int32]
LENGTH ERROR: No overload of the type expects the given number (3) of generic type arguments
      System.Collections.Generic.List[3⍴System.Int32]

Creating a Concrete Version of a Generic Interface

Applying type arguments to generic interfaces closely resembles applying type arguments to generic classes. The example below defines a function IsBoolCollection. This checks whether a given .NET type implements the concrete version ICollection[Boolean] of the generic ICollection interface, which is often implemented by data structures that act as collections of elements of a specific type.

      ⎕USING←'System' 'System.Collections.Generic'

      IsBoolCollection←{ICollection[Boolean]∊∊⎕CLASS ⍵}

      a←⎕NEW HashSet[Int32]
      b←⎕NEW List[Boolean]
      c←⎕NEW Dictionary[Int32 Boolean]

      IsBoolCollection¨a b c
0 1 0

Multiple Overloads of .NET Classes and Interfaces

Some .NET classes and interfaces have multiple overloads, varying in the number of generic type parameters. The display form of the type makes this clear, and the .NET interface will automatically use the appropriate overload based on context.

      ⎕USING←'System'
      ValueTuple
(System.ValueTuple)
(System.ValueTuple[T1])
(System.ValueTuple[T1,T2])
(System.ValueTuple[T1,T2,T3])
(System.ValueTuple[T1,T2,T3,T4])
(System.ValueTuple[T1,T2,T3,T4,T5])
(System.ValueTuple[T1,T2,T3,T4,T5,T6])
(System.ValueTuple[T1,T2,T3,T4,T5,T6,T7])
(System.ValueTuple[T1,T2,T3,T4,T5,T6,T7,TRest])

The ValueTuple class has one non-generic overload and eight generic overloads.

      ⎕USING←'System'
      ValueTuple[Int32]         ⍝ Create concrete version of overload with 1 generic parameter
(System.ValueTuple[System.Int32])

      ValueTuple[Int32 Boolean] ⍝ Create concrete version of overload with 2 generic parameters
(System.ValueTuple[System.Int32,System.Boolean])

Calling a Generic Method

Generic methods have a display form with a generic type parameter list shown in square brackets. For example:

    ⎕USING←''
    System.Decimal.CreateChecked
System.Decimal CreateChecked[TOther](TOther)

The CreateChecked function has one type parameter, shown in square brackets, and one regular parameter, shown in parentheses.

The generic type argument can be applied using square brackets, and the result is a concrete version of the generic method. The method can either be given a name or evaluated directly. The display form indicates that the type parameters have been replaced to form a concrete function.

      ⎕USING←'System'
      fn←Decimal.CreateChecked[Int32]
      fn
System.Decimal CreateChecked[Int32](Int32)
      fn 10
10
      Decimal.CreateChecked[Int32] 50
50

If a generic method has overloads with different numbers of type parameters, applying type arguments will narrow down the list of overloads that are applicable. For example, when having only one overload means that a single type argument is expected:

      ⎕USING←'System'
      ValueTuple.Create
System.ValueTuple Create()
System.ValueTuple`1[T1] Create[T1](T1)
System.ValueTuple`2[T1,T2] Create[T1,T2](T1, T2)
System.ValueTuple`3[T1,T2,T3] Create[T1,T2,T3](T1, T2, T3)
...

      ValueTuple.Create[Boolean]
System.ValueTuple`1[System.Boolean] Create[Boolean](Boolean)

.NET methods with only a single overload that expects no arguments are usually imported into APL as niladic functions. However, when they are generic, they are imported as monadic functions so that the type arguments can be applied. For example:

      ⎕USING←'System'
      Array.Empty
T[] Empty[T]()

      Array.Empty[Int32]
Int32[] Empty[Int32]()

      r←Array.Empty[Int32] ⍬
      r≡⍬
1

Applying an incorrect number of type arguments to a method will generate an error:

      ⎕USING←'System'
      Decimal.CreateChecked 50
LENGTH ERROR: No overload of the method expects the given number (0) of generic type arguments
      Decimal.CreateChecked 50
      ∧

      ValueTuple.Create[10⍴Int32]
LENGTH ERROR: No overload of the method expects the given number (10) of generic type arguments
      ValueTuple.Create[10⍴Int32]
                                ∧

Type Inference

If the arguments to a generic method have a concrete .NET type, then their type information might be sufficient for the .NET bridge to unambiguously select a method overload and to automatically apply the needed type arguments. If there is any ambiguity about the type, such as when the arguments are regular APL arrays (for example the scalar 0, which can be converted into a number of different .NET types), type inference will not take place. For example:

      ⎕USING←'System' 'System.Threading.Tasks'
      Task.FromResult
System.Threading.Tasks.Task`1[TResult] FromResult[TResult](TResult)

      Task.FromResult 123
LENGTH ERROR: No overload of the method expects the given number (0) of generic type arguments
      Task.FromResult 123
      ∧

      ⍝ Explicitly apply type arguments
      Task.FromResult[Int128] 123
System.Threading.Tasks.Task`1[System.Int128]

      ⍝ Explicitly apply type arguments, and pass in a .NET object of that type
      i128←Int128.Parse ⊂'123'
      Task.FromResult[Int128] i128
System.Threading.Tasks.Task`1[System.Int128]

      ⍝ Let the bridge infer the type argument from argument's .NET type
      Task.FromResult i128
System.Threading.Tasks.Task`1[System.Int128]

Type inference can remove the need for additional code (as shown in the last lines of the example above), but manually applying type arguments is also permitted.

If the user has specified an overload, then the type information is taken into account. This means that an alternative way of coding the above would be:

      ⎕USING←'System' 'System.Threading.Tasks'
      Task.FromResult
System.Threading.Tasks.Task`1[TResult] FromResult[TResult](TResult)

      Task.FromResult⍠Int128⊢123
System.Threading.Tasks.Task`1[System.Int128]

This works because we tell the .NET bridge that we want the overload that takes an Int128 as its argument, which means the type parameter TResult must be Int128; it is, therefore, not necessary to explicitly apply the type arguments using square brackets.