Specialized Proxies: Types#
We’ve seen specialized module-, symbol- and array-proxies. jluna has a fourth kind of proxy, jluna::Type
, which wraps all of Julia’s varied introspection functionalities in one class.
Hint: Introspection is the act of gaining information about the language itself, such as properties of types and functions. While possible, introspection in C++ can be quite cumbersome. Julia, on the other hand, was build from the ground up with it in mind.
While some overlap is present, jluna::Type
is not a direct equivalent of Base.Type{T}
, even though it is asserted to manage an object of type T
such that T isa Type
. It just provides more functions than are available using only Julia’s Base
.
Constructing a Type#
There are multiple ways to construct a type proxy:
// get type of proxy
auto proxy = Main.safe_eval("return " + /* ... */);
auto type_proy = general_proxy.get_type();
// implicit cast
Type type_proxy = Main.safe_eval("return Base.Vector");
// result of `Base.typeof`
Type type = Base["typeof"](/* ... */);
Most often, we will want to know what type a variable is an instance of. For this we can either use Base.typeof
, binding the result to an explicitly declared jluna::Type
, or we can usejluna::Proxy::get_type
.
Base Types#
For most types in Base
, jluna offers a pre-defined type proxy in jluna::
namespace, similar to the Main
and Base
module proxies.
The following types are available this way:
jluna Constant Name |
Julia-side Name |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Where T
, U
are arbitrary types, N
is an Integer
Type Order#
Julia types can be ordered. To conceptualize this, the relation of types is best thought of as a directed graph. Each node of the graph is a type, each edge is directed, where, if the edge goes from type A
to type B
, then B <: A
. That is, B
is a subtype of A
, or, equivalently, A >: B
: A
is a supertype of B
.
This relational nature is heavily used in Julia’s multiple dispatch and type inference, for now, it gives us a way to put types in relation to each other. jluna::Type
offers multiple functions for this:
// (*this) <: other
bool is_subtype_of(const Type& other) const;
// (*this) >: other
bool is_supertype_of(const Type& other) const;
// (*this) <: other && (*this) >: other
bool is_same_as(const Type&) const;
Because we can assign an order to types in this way, jluna::Type
also provides proper boolean comparison operators:
// (*this) <: other
bool operator<(const Type& other) const;
// other >: (*this)
bool operator>(const Type& other) const;
// (*this) === other
bool operator==(const Type& other) const;
// !((*this) === other)
bool operator!=(const Type& other) const;
This ordering becomes relevant when talking about TypeVar
s.
Julia Hint:
Base.TypeVar
is a class that represents a not-yet-defined type, such as a parameter for a struct. It has a lower boundlb
and upper boundub
, where, for all typest
represented by theTypeVar
, it holds thatlb <: t <: ub
.
TypeVar
s can be thought of as a sub-graph of the type-graph. An unrestricted types upper bound is Any
, while its lower bound is Union{}
. A declaration like T where {T <: Integer}
restricts the upper bound to Integer
. Any type that is “lower” along the sub-graph originating at Integer
, Int64
for example, can still bind to T
. This is useful to keep in mind.
Type Info#
When gathering information about types, it is important to understand the difference between a types fields, its parameters, its properties and its methods. Consider the following type declaration:
# in Julia
mutable struct MyType{T <: Integer, U}
_field1
_field2::T
_field3::U
function MyType(a::T, b::U) where {T, U}
return new{T, U}(undef, a, b)
end
end
This type is a parametric type, it++i has two parameters called T
and U
. T
s upper bound is Integer
while U
is unrestricted, its upper bound is Any
.
MyType
has 3 fields:
_field1
which is unrestricted_field2
which is declared as typeT
, thus it’s upper bound is alsoInteger
_field3
which is declared as typeU
, however becauseU
is unrestricted,_field3
is too
The type has 1 method, MyType(::T, ::U)
, which is its constructor.
This type furthermore has the following properties:
# in Julia
println(propertynames(MyType.body.body))
(:name, :super, :parameters, :types, :instance, :layout, :size, :hash, :flags)
Properties are often reserved for “hidden” members, such as those containing internal implementation details.
Parameters#
We can access the name and upper type bounds of the parameters of a type using jluna::Type::get_parameters
:
// declare type
State::safe_eval(R"(
mutable struct MyType{T <: Integer, U}
_field1
_field2::T
_field3::U
function MyType(a::T, b::U) where {T, U}
return new{T, U}(undef, a, b)
end
end
)");
// access type as Type proxy
Type my_type = Main["MyType"];
// get parameters
std::vector<std::pair<Symbol, Type>> parameters = my_type.get_parameters();
// print elements of vector with Julia-style pair syntax
for (auto& pair : parameters)
std::cout << pair.first.operator std::string() << " => " << pair.second.operator std::string() << std::endl;
T => Integer
U => Any
get_parameters
returns a vector of pairs where:
.first
is a symbol that is the name of the corresponding parameter.second
is the parameters upper type bound, as ajluna::Type
In case of T
, the upper bound is Base.Integer
, because we restricted it as such in the declaration. For U
, there is no restriction, which is why its upper bound is the default: Any
.
We can retrieve the number of parameters directly using get_n_parameters()
. This saves allocating the vector of pairs.
Fields#
We can access the fields of a type in a similar way, using jluna::Type::get_fields
:
// declare type
State::safe_eval(R"(
mutable struct MyType{T <: Integer, U}
_field1
_field2::T
_field3::U
function MyType(a::T, b::U) where {T, U}
return new{T, U}(undef, a, b)
end
end
)");
// access type as Type proxy
Type my_type = Main["MyType"];
// get fields as vector
std::vector<std::pair<Symbol, Type>> fields = my_type.get_fields();
// print
for (auto& pair : fields)
std::cout << pair.first.operator std::string() << " => " << pair.second.operator std::string() << std::endl;
_field1 => Any
_field2 => Integer
_field3 => Any
We, again, get a vector of pairs where .first
is the name, .second
is the upper type bound of the corresponding field.
If we actually want the value of a field, we need use operator[]
on a jluna::Proxy
that is an instance of that type, not the type itself.
While less useful, we can access a types methods using the Julia-side Base.methods
. Similarly, to access a types properties, Base.propertynames
and Base.getproperty
can be used.
Type Classification#
To classify a type means to evaluate a condition based on a types attributes, in order to get information about how similar or different clusters of types are. jluna offers a number of classifications, some of which are available as part of the Julia standard library, some of which are not. This section will list all offered by jluna, along with their meaning:
is_primitive
: was the type declared using the keywordprimitive
Bool_t.is_primitive() // true Module_t.is_primitive(); // false
is_struct_type
: was the type declared using the keywordstruct
Bool_t.is_struct_type() // false Module_t.is_struct_type(); // true
is_declared_mutable
: was the type declared usingmutable
Bool_t.is_declared_mutable() // false Module_t.is_declared_mutable(); // true
is_isbits
: is the type aisbits
type, meaning it is immutable and contains no references to other values that are not also isbits or primitivesBool_t.is_declared_mutable() // true Module_t.is_declared_mutable(); // false
is_singleton
: a typeT
is a singleton iff:T
is immutable and a structtypefor types
A
,B
ifA <: B
andB <: A
thenA === B
State.eval("struct Singleton end"); Type singleton_type = Main["Singleton"]; singleton_type.is_singleton(); // true
is_abstract_type
: was the type declared using theabstract
keywordFloat32_t.is_abstract_type() // false AbstractFloat_t.is_abstract_type(); // true
is_abstract_ref_type
: is the type a reference whose value type is an abstract typeType(jluna::safe_eval("return Ref{AbstractFloat}")).is_abstract_ref_type(); //true Type(jluna::safe_eval("return Ref{AbstractFloat}(Float32(1))")).is_abstract_ref_type(); // false
is_typename(T)
: is the.name
property of the type equal toBase.typename(T)
. Can be thought of as “is the type a T”// is the type an Array: Type(State::safe_eval("return Array")).is_typename("Array"); // true Type(State::safe_eval("return Array{Integer, 3}")).is_typename("Array"); // also true
“Unrolling” Types#
There is a subtle difference between how jluna evaluates properties of a type and how pure Julia does. Consider the following:
# in Julia
function is_array_type(type::Type)
return getproperty(type, :name) == Base.typename(Array)
end
println(is_array_type(Base.Array))
Some may expect this to print true
, however, this is not the case. It actually prints false
.
This is because Base.Array
is a parametric type. typeof(Array)
is actually UnionAll
, which does not have a property :name
:
# in Julia
println(propertynames(Array))
(:var, :body)
To access the property :name
, we need to first unroll the type, meaning we need to specialize all its parameters. Once we do so, it seizes to be a UnionAll
, gaining the properties expected of a Base.Type
:
# in Julia
# access type
type = Array
# specialize fully
while (hasproperty(type, :body))
type = type.body
end
# print type after unrolling
println(type)
# print propertynames of unrolled type
println(propertynames(type))
Array{T, N}
(:name, :super, :parameters, :types, :instance, :layout, :size, :hash, :flags)
Once fully unrolled, we have access to the properties necessary for introspection. jluna does this unrolling automatically for all types initialized by jluna::initialize
(see the previous sections list).
If desired, we can fully specialize a user-initialized type manually using the member function .unroll()
. Without this, many of the introspection features will be unavailable.