-
Notifications
You must be signed in to change notification settings - Fork 166
Specifying types
Clojure uses symbols to name types. In the JVM world, this is relatively straightforward.
- The symbols
int,double,float, etc. in type hints to refer to the corresponding primitive types. - a package-qualified symbol (one containing periods internally) is taken to name the Java class with the same character sequence
- a namespace may contain a mapping from a symbol to a Java class, via
import
ClojureCLR extends the family of special type names such as int to include the primitive types available on the CLR but on the JVM: uint, ushort, ulong. Also, byte is refers to the System.Byte type, which is unsigned. (The corresponding byte reference on the JVM is to the signed byte type.) ClojureCLR adds sbyte. The shorthand array references such as ints are expanded to provide uints, ushorts, ulongs, and sbytes.
For package-qualified symbols, one quickly runs into trouble on the CLR because typenames contain characters that are not compatible with symbols. We use the underlying CLR syntax for types, not the glosses provided by C#, F#, or Visual Basic. Fully-qualified type names can contain an assembly identifier, which involves spaces and commas. Generic type names include square brackets and backquotes. Array types also contain square brackets. In fact, CLR typenames can contain arbitrary characters.
To enable expressing symbols with non-standard characters, ClojureCLR extends the reader syntax for symbols by the mechanism of vertical bar quoting, an adaptation of the mechanism in Common Lisp. See Reader extension: |-quoting.
With this mechanism we can make a symbol referring to a type such as:
(|com.myco.mytype+nested, MyAssembly, Version=1.3.0.0, Culture=neutral, PublicKeyToken=b14a123334343434|/DoSomething x y)
or
(reify
|AnInterface`2[System.Int32,System.String]|
(m1 [x] ...)
I2
(m2 [x] ...))
Special note should be made of the proper way to refer to generic types (instantiated or not).
|System.Collections.Generic.IList`1[System.Int32]|
This is the official CLR way of referring to the type that would be referred to in C# (with using) by IList<int>.
Starting with version 1.12.6, ClojureCLR relaxes some of the syntactic requirements for typenames and provides a way to make aliases for types. This brings ClojureCLR much close to the ease of specifying types found in C#, etc.
- If type arguments are provided to a generic type, you no longer need to provide the backquote n generic argument count.
- You can use the special type names
int, etc., are generic type arguments.
Thus, we can replace
|System.Collections.Generic.IList`1[System.Int32]|
with
|System.Collections.Generic.IList[int]|
The standard import mechanism used in ns (or as a standalone call) does not work well with complicated type names. ClojureCLR introduces alias-type and add-type-alias to extend the basic import mechanism. The former is a macro that does not require quoting and is generally preferred. The second is a standard function, just in case. The following are equivalent:
(alias-type Dictionary |System.Collections.Generic.Dictionary`2|)
(add-type-alias 'Dictionary |System.Collections.Generic.Dictionary`2|)
Note that when referencing a generic type definition (not containing type parameters), as is done here, the backquote+arity must be supplied. If we define aliases:
(alias-type Dictionary |System.Collections.Generic.Dictionary`2|)
(alias-type List |System.Collections.Generic.List`1|)
(alias-type IntList |List[int]|)
we can refer to types such as
|Dictionary[String, List<long>]|
|Dictionary[String, IntList]|
and create expressions such as
(def my-list (IntList/new))
(IntList/.Add my-list 42)
(defn f [^IntList xs] ... )
The CLR supports nested classes. Referring to these is very straightforward – unless generics are involved. For a simple case such as
namespace MyNamespace;
public class Outer
{
public class Inner { }
}
you could refer to MyNamespace.Outer and MyNamespace.Outer+Inner. You could import MyNamespace+Outer and then refer just to Outer+Inner. All fine and dandy. (note that subclass names are conjoined with +; on the JVM, $ is used.)
Now consider:
namespace MyNamespace;
public class GenParent<T1, T2>
{
public class Child
{
public class GrandChild<T3>
{
public class GreatGrandChild<T4, T5>
{
}
}
}
}
If you are working in C#, you could refer to various types such as
GenParent<,> // Generic type definition
GenParent<int, string> // constructed generic type
GenParent<,>.Child // nested type -- this is also a generic type definition
GenParent<,>.Child.GrandChild<> // nested type -- constructed generic type
GenParent<int string,>.Child.GrandChild<double> // constructed generic type
// etc.
However, if you were to print the fully-qualified name of the C#-named type
GenParent<int, string>.Child.GrandChild<double>.GreatGrandChild<int, long>
you might be surprised (I was) to get (leaving out assembly information):
GenParent`2+Child+GrandChild`1+GreatGrandChild`2[System.Int32,System.String,System.Double,System.Int32,System.Int64]
The actual generic type definition name is
GenParent`2+Child+GrandChild`1+GreatGrandChild`2
and it takes five type parameters. C# hides this from you.
ClojureCLR mostly hides this. Matching the C# examples above, you can write:
|MyNamespace.GenParent`2| ;; Generic type definition, backquote-arity required
|MyNamespace.GenParent[int, String]| ;; No need for `2 here
|MyNamespace.GenParent`2+Child| ;; Nested generic type definition; must provide `2
|MyNamespace.GenParent`2+Child+GrandChild`1| ;; Nested generic type definition; must provide `2 and `1
|MyNamespace.GenParent[int,String]+Child+GrandChild[double]| ;; No need for `2 or `1 here
If you introduce type aliases, the same rules apply:
(alias-type GP |MyNamespace.GenParent`2|)
(alias-type GPC |GP+Child|) ;; we know the arity from the alias for GP
We can then refer to
|GP[int, string]| ;; constructed generic type;
|GPC[long, double]| ;; constructed generic type;
A confusing factor in CLR type naming is that square brackets are used in three different ways:
- To delimit the list of type arguments to a generic type definition. Example:
Dictionary2[System.String,System.Int32]`. - To indicate an array type. Example:
System.String[]is an array of strings. - To delimit assembly names in assembly-qualified type names.
For the last case, you do not need brackets around the string if the assembly is for the top level name, such as
System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
The comma after System.String indicates that an assembly name follows. However, if you are supplying an assembly name for a generic type parameters, you need the brackets. Example:
System.Collections.Generic.List`1[[System.Int64, System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Private.CoreLib, Version=9.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]
which is deconstructed as
typename[ ...type argument ... ], ...assembly-specifier
Where the type argument in this case does have brackets around it: [typename, assembly-specifier].
I hope you never have to deal with nested generic types and assembly-qualified names.