Does verse have these features, or are there any potential discussions in providing these in the future?
- Meta types: A meta type is basically “the type of a type”
- Dynamic typing: Dynamic typing allows one to obtain an erased meta type for a given value.
Here are a few example from Swift:
struct Foo {}
struct Bar<T> {}
print(Foo.self) // prints `Foo` from the metatype value `Foo.Type`
print(Bar<Int>.self) // prints `Bar<Int>` from the metatype value `Bar<Int>.Type`
let any: Any = Foo() // `Foo` erased and stored in the `Any` existential
let foo = Foo()
type(of: any) // returns `Any.Type` which is an existential box containing `Foo.Type`
type(of: foo) // returns `Foo.Type`
if let fooType = type(of: any) as? Foo.Type {} // succeeds
In that spirit it would be highly beneficial if we could print type names from the meta type values (this should include parametric types if the type is generic).
Translating this to pseudo verse:
foo := struct {}
bar(t: type) := struct {}
Print("{foo}") # prints `foo` from the metatype value of `foo`
Print("{bar(int)}") # prints `bar(int)` from the constrained metatype value of `bar(t:)`
Any: any = foo {} # `foo` erased and stored in the `any` existential
Foo := foo {}
TypeOf(Any) # returns existential `any` metatype value containing the metatype from `foo`
TypeOf(Foo) # returns metatype value for `foo`
if (FooType := foo_metatype[TypeOf(Any)]) {} # should succeed
# ^~~~~~~~~~~~~ pseudosyntax for the metatype of `foo`
The meta types should have no need for custom ToString()
overloads and should gain that capability as a built-in compiler feature.
Meta types, yes. In Verse, type
is the type of meta types (subtype
can be used to put an upper bound on such a value). You have already declared a function taking a meta type as argument.
bar(t:type) := struct:
Property:t
is a function taking a value of type type
as argument and producing a struct
definition. As with any other function, you can use it as a value:
X := bar(int)
Y := X{Property := 0}
To put an upper bound on the allowed t
:
foo := class {}
bar(t:subtype(foo)) := struct
Property:t
However, there is no way to inspect information about a type that can be acted upon at runtime - including producing a string
for a type
. Additionally, parametric classes and structs are currently type-erased in Verse, and there is no way currently to recover the erased information at runtime.
2 Likes
There’s a lot of work going towards future type system features, such as improved parametric types and generic functions.
Some experiments early in the evolution of Verse followed the Smalltalk path into metaclasses and similar ideas, but we moved away from that in favor of types-as-sets-of-potential-values (also known as Curry typing) because it leads to simpler reasoning about programs in terms of math and logic (see articles on Proofs as Programs) and type system limitations as security properties (see Theorems for Free).
Verse goes to great effort to ensure that types just describe sets of potential values and nothing more. In C++, 0 has type int. In Verse, 0 doesn’t have a unique type but rather 0 belongs to every current and future type that happens to contain 0: the integers, the natural numbers, the rational numbers, and the type containing just 0.
So there isn’t a ‘TypeOf(MyValue:any):type’ function, but in the future you’ll be able to write syntax meaning ‘let t be any type that contains MyValue’.
Without metaclasses, how will we do the kind of type-dependent computations you’re referring to here? We plan to follow the general approach of Haskell typeclasses and Rust traits, in which programs can define generic bundles of operations and the types on which they operate, in a scalable and modular way that’s all checkable at compile time.
2 Likes
Just to clarify - does this mean there is no way to implement generic programming? Or is it OK for Verse types?
For example:
# Archetype parametric contract/wrapper
payloadbase(t:type) := class<abstract>():
Get() : t
Set(ValueIn : t) : void
# Example payload for an int value
payload_int := class(payloadbase(int)):
var Value : int = -1
Get<override>() : int = { return Value }
Set<override>(ValueIn : int) : void = { set Value = ValueIn}
# Example payload for a struct
payload_struct := class(payloadbase(payload_class_data)):
var Value : payload_class_data = payload_class_data{}
Get<override>() : payload_class_data = { return Value }
Set<override>(ValueIn : payload_class_data) : void = { set Value = ValueIn}
# Example struct
payload_class_data := struct:
Val1 : int = 0
Val2 : float = 0.0
…if one were to call Set
on payload_int
and/or payload_struct
; then later on payloadbase
, Get
will just return the default struct values since it is erased during runtime? Also How about the int
payload - not the case?
Although doesn’t the Event system rely on generics like this? Or is that different because they’re (a) “callables” (I.e. they simple follow a “Tell, Don’t Ask” principle?) or (b) Never cast to the base type (thus no erasure occurs)? Actually I can probably figure this out myself now, this might be a rubber duck situation…
Either way, good to see that deeper polymorphism is WIP though! Thanks to Tim for that background too (also in the “polymorphic quicksort” thread).
That should be fine - though the generic type is erased at runtime, that just means that a use of payloadbase(int)
's Get
and Set
will box and unbox the value (effectively coerce between int
and any
) - but you shouldn’t be able to do anything bad as far as types go. If you were to use payloadbase(payload_class_data)
's Get
, it would return a valid playload_class_data
, which if the value in question happens to be a payload_struct
, that may be a payload_class_data
if there was otherwise no call to Set
. In the int
case, if the payloadbase(int)
in question is a payload_int
and there had been no call Set
previously, Get
should produce -1
. Invoking either Get
or Set
on a value with declared type payloadbase(int)
vs. payload_int
does not matter - the dispatch will use the type of the underlying value (dynamically dispatch). In other words, X:payload_int = payload_int{}; X.Set(1); X.Get()
and X:payloadbase(int) = payload_int{}; X.Set(1); X.Get()
should produce the same result.
1 Like