Generics - dynamic types
Swift | Rust |
---|---|
pending | supported |
Generics can refer to dynamic types with constraints, with type safety guarantees provided by the runtime.
As described in “Generics - static types”, we can define generic algorithms that can operate on concrete types. This page explores the considerations of using generalized types without knowing the underlying concrete types.
The main concern of using generic types dynamically is to ensure the same degree of type safety at runtime as there is during compilation.
This is usually achieved by a pattern called “boxing/unboxing”. In this pattern, a type is “boxed” to allow referring to its generic type, and “unboxed” to safely refer to its concrete type.
Swift
In Swift, boxed types are also called “existential types” and are referred to by their protocol name.
For a long time, Swift had several limitations for existential uses. These limitations would frequently be surprising to developers and have limited the usefulness of the feature. Oftentimes, workarounds such as type erasing structs would be needed, such as AnyHashable
and AnyPublisher
. Other times, it would be impossible to write code that is truly generic at all.
Swift 5.7 has lifted most of the limitations, by implementing several big enhancements:
- SE-0309 - Unlock existentials for all protocols - allow using a protocol as a type without limitations.
- SE-0353 - Constrained Existential Types - allow constraining a dynamic type using its type parameters.
- SE-0352 - Implicitly Opened Existentials - allow “unboxing” a dynamic type to a conrete type.
- SE-0335 - Introduce existential
any
- a new keyword to explicitly refer to a protocol as a type. Introduced in Swift 5.6, will be mandatory for all protocols in Swift 6.
The enhancements to any
types (existential types) work seamlessly with the enhancements to some
types (opaque types).
To adapt the example from static types (Swift 5.7):
// define a generic type
protocol Costume {
associatedtype BellType
var bells: [BellType] { get }
}
// define a generic function that checks a concrete type
func hasBells(_ costume: some Costume) -> Bool {
!costume.bells.isEmpty
}
// define a generic function that works with dynamic types inside an array
func firstWithBells(costumes: some Sequence<any Costume>) -> (any Costume)? {
costumes.first(where: { hasBells($0) })
}
Note: constraining the associated type
Sequence.Element
with type parameter syntaxSequence<Element>
is made possible by SE-0346 (Lightweight same-type requirements for primary associated types, Swift 5.7) and SE-0358 (Primary Associated Types in the Standard Library, in review).
This generic function can accept any Sequence
that can contain different types of Costume
s:
// define two concrete types conforming to `Costume`
struct GymnastCostume: Costume {
let name: String
var bells: [Never] { [] }
}
struct ClownCostume: Costume {
enum ClownBell { case big, small }
let name: String
let bells: [ClownBell]
}
// a generic array of costumes
// the explicit type annotation is there so the array is not inferred to be `[Any]`
let costumesArray: [any Costume] = [
GymnastCostume(name: "A"),
ClownCostume(name: "B", bells: [.big, .big, .small])
]
// a concrete result `Optional<Costume>`
let firstCostume = firstWithBells(costumes: costumesArray)
print(firstCostume) // prints "B"
Rust
Rust also allows using generalized types by their interfaces. The term for this is “trait object” and it’s denoted using the dyn
keyword.
Trait objects must be references, an as such are usually declared as:
&dyn Trait
Box<dyn Trait>
To implement a generalized function that finds the first costume with bells:
// define a generic type
pub trait Costume {
fn description(&self) -> String;
fn has_bells(&self) -> bool;
}
// define a generic dynamic function
fn first_with_bells<'a, 'b, I>(iter: &'a mut I) -> Option<Box<dyn Costume + 'b>>
where I: Iterator<Item = Box<dyn Costume + 'b>> {
iter.find(|c| c.has_bells())
}
Use it with two distinct types:
// first concrete type
struct GymnastCostume {
name: &'static str,
has_bells: bool,
}
impl Costume for GymnastCostume {
fn description(&self) -> String {
let name = self.name;
format!("Gymnast {name}").to_string()
}
fn has_bells(&self) -> bool {
self.has_bells
}
}
// second concrete type
struct ClownCostume {
name: &'static str,
}
impl Costume for ClownCostume {
fn description(&self) -> String {
let name = self.name;
format!("Clown {name}").to_string()
}
fn has_bells(&self) -> bool {
true
}
}
// use it
fn main() {
// heterogenous vector
let costumes_vec: Vec<Box<dyn Costume>> = vec![
Box::new(GymnastCostume { name: "A", has_bells: false }),
Box::new(ClownCostume { name: "B" }),
];
let mut iter = costumes_vec.into_iter();
let first_costume = first_with_bells(&mut iter);
println!("{:?}", first_costume.unwrap().description()); // prints "Clown B"
}