TL;DR
Let’s use (static) multiple dispatch for UnionAll type application!
This would introduce type constructors. They would take a UnionAll to construct a type similar to how inner (object/value) constructors take a type to construct an object/value.
Motivation
Backwards Compatibility
Suppose we wanted to generalize SubString from
struct SubString{T <: AbstractString} <: AbstractString
string::T
offset::Int
ncodeunits::Int
end
into:
struct SubString{T <: AbstractString, R <: AbstractUnitRange{<:Integer}} <: AbstractString
string::T
ind::R
end
This would have multiple advantages (allowing smaller index types, allow something like Base.OneTo, generally reusing code) which are not relevant for our discussion here.
Object constructors and properties could preserve much of the existing interface. However, SubString{String} would become a UnionAll, so existing fields declared as ::SubString{String} would no longer be concrete. For all I know, this incompatibility could not be avoided (withing the desired solution space, i.e without introducing additional types or something) with Julias features as of today.
Constraining Types
Currently, it’s often not possible to usefully constraint types so that invalid types cannot even be constructed (used). Instead, constructors need to make sure that they don’t construct an object of an (then already existing) invalid type.
So assume we wanted to define for our example above (to not introduce a more useful, but another example)
SubString{UnitRange{Int}}(bytes::Vector{UInt8}) =
SubString{String, UnitRange{Int}}(String(bytes), 1:length(bytes))
then this could not be constructed, as SubString is a UnionAll, which only accepts a T <: AbstractString as first TypeVar, not an AbstractUnitRange.
The only possibility to make this work is to remove the type constraint(s) from the type definition and let the constructor do this work. While that kind of works, it not only feels wrong, but it becomes obvious, that regular constructors are currently more powerful.
Asymmetry
There is also an inherent asymmetry involved in defining parametric types with more than one type parameter, closely related the the point mentioned before: SubString{T <: AbstractString} automatically is a UnionAll, while SubString{R <: AbstractUnitRange{<:Integer}} isn’t, although they are clearly distinguishable. Again, regular constructors, using multiple dispatch, are just more powerful due to their multiple dispatch.
Conclusion
Julia lets us customize object construction through constructor methods, but type application such as Foo{T} always follows positional parameter substitution.
If we only had multiple (static) dispatch for type construction! So could we support inner type constructors, analogous to inner object constructors?
Proposed Syntax
Inside the type definition, allow (type) constructors without the parenthesized object argument list:
struct SubString{T <: AbstractString, R <: AbstractUnitRange{<:Integer}} <: AbstractString
string::T
ind::R
SubString{T <: AbstractString} = new{T, UnitRange{Int}}
SubString{R <: AbstractUnitRange{<:Integer}} =
new{T,R} where T <: AbstractString
SubString{T,R} = new{T,R}
end
The distinction would be:
new{T,R}returns the canonical type through native parameter substitution.new{T,R}(string, ind)constructs an instance, as today.
From the user’s perspective:
SubString{String}
# Concrete type: String parent, UnitRange{Int} indices
SubString{String, UnitRange{Int32}}
# Concrete type: String parent, UnitRange{Int32} indices
SubString{UnitRange{Int}}
# UnionAll: any permitted string type, UnitRange{Int} indices
# Equivalent to:
SubString{T, UnitRange{Int}} where T <: AbstractString
SubString{String, R} where R <: AbstractUnitRange{<:Integer}
# UnionAll: String parent, any permitted range representation
SubString would still denote the generic UnionAll.
Semantics
Type constructors would dispatch on parameter combinations and bounds. Their bodies could use = begin … end with Julia code to validate parameters, supply defaults, or reorder them before returning a type.
Like inner object constructors, defining an inner type constructor would suppress automatic public type-application defaults. Explicit forwarding, as above, would restore the desired forms. Types without custom definitions would behave exactly as today.
So this should be fully backwards-compatible with the exception of local type aliases in the constructor which could have the same syntax. I have never seen them, but if they exist in larger frequency, we could either require, at least initially, a new (but this would disallow type constructor delegates) or change the syntax. Although I think that the proposed syntax is the logical choice, we could introduce a block-form with some keyword needing an end.
Arguments would be type objects, permitted value parameters, or TypeVars, so everything would need to be “static”. Ambiguous applications would be errors. At least initially constructors would be fixed with the type definition, so their would be no external type constructors. Type constructors must return specializations of that same nominal type (so a SubString type constructor must return a concrete or UnionAll SubString, not a Vector).
Summary
The goal is to make type application customizable in the same spirit as object construction, allowing a type’s representation to evolve while preserving useful existing spellings such as SubString{String}. This should solve all motivating problems and could make the language more consistent by allowing the same (and defining) language mechanism (multiple dispatch) to be applied not only on functions and on objects (=callables), but also on types.
What do you think? Obviously, a lot of details would need to be clarified before/while implementing it, but does the general idea sound interesting?