diff --git a/dotnet-interface-guide/docs/dotnet-classes/advanced-techniques.md b/dotnet-interface-guide/docs/dotnet-classes/advanced-techniques.md index c84ab5e96c..56d16096c8 100644 --- a/dotnet-interface-guide/docs/dotnet-classes/advanced-techniques.md +++ b/dotnet-interface-guide/docs/dotnet-classes/advanced-techniques.md @@ -159,40 +159,117 @@ For example, if MyClass is a .NET class wit ## Generics -The .NET interface supports creating concrete versions of _generic classes_, instantiating them, and calling generic methods. This is done by calling `43⌶` with a right argument of `632` (for more information on `43⌶`, see the _Dyalog APL Language Reference Guide_). Note that this I-beam will be replaced with something that is better integrated into the language in a future Dyalog version. +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, and 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](#type-inference) to deduce the type arguments from the types of the method arguments. -A generic class is a class that has type parameters, which must be given values to create a concrete version of the class. Similarly, a generic method has type parameters that must be specified before the method can be called. +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. These types themselves might be the result of applying types to a generic .NET class. -The result of `43⌶632` is a monadic operator, which is used to apply the type arguments of generic classes or methods. +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: + +```C# +// Instantiate a concrete version of a generic class in C# +new System.Collections.Generic.List(); + +// Call a concrete version of a generic method in C# +System.Decimal.CreateChecked(5); +``` + +And the corresponding APL +```apl +⍝ 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 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: -A concrete version of the List class can be created using `43⌶632`. For example, a list class that contains integers can be created as follows: ```apl - IntList←System.Collections.Generic.List(43⌶632)System.Int32 - IntList -(System.Collections.Generic.List`1[System.Int32]) + ⎕USING←'' + System.Collections.Generic.List +(System.Collections.Generic.List[T]) ``` -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. +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: +```apl + ⎕USING←'' + IntList←System.Collections.Generic.List[System.Int32] + IntList +(System.Collections.Generic.List[System.Int32]) +``` -The operations can also be combined and multiple type argument specified. For example: +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: ```apl - types←System.Char System.Int32 - ⎕NEW System.Collections.Generic.Dictionary (43⌶632) types + ⎕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: +```apl + ⎕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 + ∧ ``` -Attempting to instantiate a generic class without the expected number of type arguments generates an exception. For example: +Similarly, applying too many type arguments also results in an error: + ```apl - ⎕USING←'' - ⎕NEW System.Collections.Generic.List -EXCEPTION: Invalid number of generic type arguments applied (expected 1, got 0) - ⎕NEW System.Collections.Generic.List - ∧ + ⎕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 looks just like with generic classes. The example below defines a function `IsBoolCollection`, which 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. + +```apl + ⎕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. + +```apl + ⎕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 8 generic overloads. + +```apl + ⎕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 @@ -206,34 +283,105 @@ 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 `43⌶632`. When the monadic operator `43⌶632` is run with a .NET method as its left argument, and a scalar type or a vector of .NET types as its right argument, the result is a derived function that calls the specified .NET method with both the generic type arguments and regular arguments applied. For example: +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. + ```apl - fn←System.Decimal.CreateChecked (43⌶632) System.Int32 - fn 123 -123 + ⎕USING←'System' + fn←Decimal.CreateChecked[Int32] + fn +System.Decimal CreateChecked[Int32](Int32) + fn 10 +10 + Decimal.CreateChecked[Int32] 50 +50 ``` -Attempting to call a method without specifying the type argument generates an exception. For example: +If a generic method has overloads with different numbers of type parameters, applying type arguments will narrow down the list of overloads which are applicable, as in the example below where only one overload expects a single type argument. + ```apl - System.Decimal.CreateChecked 123 -EXCEPTION: Invalid number of generic type arguments applied (expected 1, got 0) - System.Decimal.CreateChecked 123 - ∧ + ⎕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) ``` -### Calling a Niladic Generic Method +.NET methods with only a single overload which expects no arguments, are usually imported into APL as niladic functions, but when they are generic, they are imported as monadic functions so that the type arguments can be applied. -.NET methods that only export overloads with zero parameters are exported as niladic functions, which means they cannot be passed as an operand to `43⌶632` without being evaluated. To accomodate this, `43⌶632` also accepts a character vector as its left argument. In this case, it runs the .NET method with that name, with the generic type arguments applied. For example: ```apl - r←'System.Array.Empty'(43⌶632)System.Int32 - r≡⍬ + ⎕USING←'System' + Array.Empty +T[] Empty[T]() + + Array.Empty[Int32] +Int32[] Empty[Int32]() + + r←Array.Empty[Int32] ⍬ + r≡⍬ 1 ``` -Attempting to evaluate the niladic function without calling `43⌶632` generates an exception. For example: +Applying an incorrect number of type arguments to a method will generate an error: + ```apl - System.Array.Empty -EXCEPTION: Invalid number of generic type arguments applied (expected 1, got 0) - System.Array.Empty - ∧ + ⎕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] + ∧ ``` + +However when no type arguments are applied, in some cases [type inference](#type-inference) can take place, and no error will be generated. + +#### Type Inference + +If the arguments to a generic method have a concrete .NET type, then their type information might be enough for the .NET bridge to unambiguously select a method overload, and to automatically apply the needed type arguments. But if there is any doubt 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. + +``` + ⎕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 save the programmer from doing unnecessary typing, as shown in the last lines of the example above, but it is still allowed to manually apply type arguments. + +If the user has [specified an overload](#specifying-overloads), the type information is taken into account, which means another way of doing the above would be: + +```apl + ⎕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] +``` + +which 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, and therefore there is no need to explicitly apply the type arguments using square brackets. \ No newline at end of file