You say:
One thing I haven't seen (…) is using a type as a value that can be passed around, allowing it to instantiate new objects, call static functions etc, while still providing all the benefits of strong type checking
Emphasis mine.
For example, I have 2 classes, J
and K
, both of which implement interface I
and in some circumstances may be used in the same place.
And:
if the type itself can be passed I could have a function that takes a type implementing I
and call whichever version of a static member function, based on the passed object
Emphasis mine.
Something that gets close is runtime interrogations. For example with simple C# pattern matching:
public static void Test<T>(T obj)
where T: I
{
if (obj is J objAsJ)
{
// use J, including static methods on J
}
if (obj is K objAsK)
{
// use K, including static methods on K
}
}
This provides "all the benefits of strong type checking". However, of course, has the drawback of forcing you to enumerate the possible types.
If the type is to be passed at runtime - for example, in a variable, as the question suggests - it means it is not known at compile time, so we lose that strong type checking. To pass a type at runtime but have some type information in compile time, we have generic type arguments and constraints, of course. Yet, that won't give you access to static members.
I only see two paths to keep such strong type checking: We interrogate the object, as shown above. Or stronger generic constraints…
Another thing that gets close is static interface methods. If we can make a generic constraint to such interface, we could be able to use those static members.
Java has interfaces with static methods, but you can't override those. So we need a language with has something like interfaces with static members that we can override. Rust is such language.
For example, I have 2 classes, J
and K
, both of which implement interface I
and in some circumstances may be used in the same place.
I'll have two types Dog
and Sheep
, and a trait Animal
implemented for both.
if the type itself can be passed I could have a function that takes a type implementing I
and call whichever version of a static member function, based on the passed object
Traits in Rust can have static functions, which we get to implement for each type.
I'll show how to call both static and instance functions defined in a trait, getting a different result depending on the actual type. So it calls "whichever version of a static member function, based on the passed object".
The reason I'm saying it gets very close is because I'll never have a variable storing the type, which is what the title of the question suggests ("Do any programming languages use types as values?").
To be fair, Rust has TypeID
, which is just a number. It can be used to identify and compare types, but that's about it.
Instead everything is type checked at compile time (which, according to comments, seems to be what you care about). Rust does not have runtime reflection.
Note: I'll be using String
(which is a heap allocated string), and i'll be cloning it. Not efficient, but I don't bother with lifetimes.
I'll have two types Dog
and Sheep
:
struct Dog { name: String }
struct Sheep { wool: bool, name: String }
An Animal
trait:
trait Animal {
fn new(name: String) -> Self; // Self is the type that implements the trait
fn name(&self) -> String;
fn noise(&self) -> String;
fn talk(&self) {
println!("{} says {}", self.name(), self.noise()); // This is default impl.
}
fn species() -> String;
}
And we implement the trait for both types. This is Animal
for Dog
:
impl Animal for Dog {
fn new(name: String) -> Dog {
Dog { name: name }
}
fn name(&self) -> String {
self.name.clone()
}
fn noise(&self) -> String {
"bark!".to_string()
}
fn species() -> String
{
"Canine".to_string()
}
}
This is Animal
for Sheep
.
impl Animal for Sheep {
fn new(name: String) -> Sheep {
Sheep { name: name, wool: true }
}
fn name(&self) -> String {
self.name.clone()
}
fn noise(&self) -> String {
if self.wool {
"baaaaah!".to_string()
} else {
"baaaaah?".to_string()
}
}
fn talk(&self) {
println!("{} pauses briefly... {}", self.name, self.noise());
}
fn species() -> String
{
"Ovine".to_string()
}
}
Let us use them:
fn test<T: Animal>(animal: &T) {
println!("{}", T::species());
animal.talk();
let clone = T::new("Clone of ".to_owned() + &animal.name());
clone.talk();
}
fn main() {
let my_dog: Dog = Animal::new("Snuppy".to_string());
let mut my_sheep: Sheep = Animal::new("Dolly".to_string());
test(&my_dog);
test(&my_sheep);
}
As you can see the test
function is generic. It has a type argument T
that must have an implementation of Animal
. And it borrows an argument of that type.
We are able to call static functions defined in the trait:
println!("{}", T::species());
Which outputs "Canine"
for Dog
and "Ovine"
for Sheep
.
We are able to call instance functions defined in the trait:
animal.talk();
We are able to create new instances of the same type we are given (this is just another static function):
let clone = T::new("Clone of ".to_owned() + &animal.name());
And use those those instances:
clone.talk();
Everything is type checked at compile time.
This is the output of the program:
Canine
Snuppy says bark!
Clone of Snuppy says bark!
Ovine
Dolly pauses briefly... baaaaah!
Clone of Dolly pauses briefly... baaaaah!
Type
type, for example, which will allow to do most if not all that is described here via reflection. We are not talking strings as types either, right? For example Visual BasicCreateObject
or PHPnew
andget_class
and similar which use strings.