-
Notifications
You must be signed in to change notification settings - Fork 1
`extend` statements
extend
statements are a mechanism which allows for types/enums to be augmented outside of their original declaration block.
The syntax of a basic statement is as follows:
type Person {
firstName: String
lastName: String
}
extend Person {
func fullName(self): String = "${self.firstName} ${self.lastName}"
}
val me = Person(firstName: "Ken", lastName: "Gorab")
me.fullName() // "Ken Gorab"
If the subject of the extend
statement does not yet exist, that would fail to typecheck; also if a method being defined in the body of the extend
statement already exists on the datatype (or any other extend
statements in scope) that also would fail to typecheck.
This can have a few potential use cases.
- Adding conformances to traits:
import SomeType from "./someModule"
import SomeTrait from "./someOtherModule"
extend SomeType : SomeTrait {
// add method implementations here
}
This is especially helpful if SomeType
is defined in some other module that can't be modified (or maybe doesn't need to know about the particular functionality). One example of this would be for an imagined library that provides json serialization. Imagine there's a trait like:
trait JsonSerialize {
func toJson(self): String
}
It's pretty easy to add conformances to this trait for our user-defined types:
type Person : JsonSerialize {
...
trait func toJson(self): String = ...
}
But, what about a type that lives in a different module? Specifically, what about an Array
?
val people: Person[] = [...]
people.toJson() // how does this work?
To accomplish this, we can use an extend
statement to provide a conformance to the JsonSerialize
trait on the Array
type:
extend Array<T> : JsonSerialize {
trait func toJson(self): String {
val items = self.items
.map(item => item.toJson()) // <-- hmmm
.join(", ")
"[$items]"
}
}
However there's a problem, as pointed out in the hmmm
comment above - there's no guarantee that the items have a toJson
method on them! In order to ensure this, we can add a conformance requirement on T
for the JsonSerialize
trait:
extend Array<T: JsonSerialize> : JsonSerialize {
...
}
This means that, for the type Array<T>
where T
conforms to the JsonSerialize
trait, the extend
statement applies. So, given the example code above:
val people: Person[] = [...]
people.toJson() // this works now
// however, this fails to typecheck:
type Animal { name: String }
val animals: Animal[] = [...]
animals.toJson()
// ^^^^^^
// Error: no method `toJson` exists for type `Animal[]`
// The type `T[]` does have a method `toJson` when T conforms to the JsonSerialize trait, but here T is of type `Animal` which does not conform to that trait.
- Defining robust expressive APIs that are conditional on the type This usage is less critical I would say, since there's almost always a way around it. This might even end up being a bad idea, who knows? Imagine we wanted to express the following functionality:
[1, 2, 3, 4].sum() // 10
This could be written as
func sum(arr: Int[]): Int = ...
but what about if we had an array of Float values? Then it becomes necessary to define a sumInts
and sumFloats
function, which doesn't feel as expressive.
Using an extend
statement, we could write something like:
extend Array<T: Int> {
func sum(self): Int = ...
}
extend Array<T: Float> {
func sum(self): Int = ...
}
Then, we could be able to write something like
[1, 2, 3, 4].sum() // 10
[1.1, 2.2, 3.3, 4.4].sum() // 11
// however, this fails to typecheck:
["a", "b", "c"].sum()
// ^^^
// Error: no method `sum` exists for type `String[]`
// The type `T[]` does have a method `sum` when T is type `Int` or `Float`, but here T is of type `String`
Another problem that this could help solve is in the case of nested Option values:
val opts = [Some(123), Some(456), Some(789)]
opts[0] // type is Int??
I could potentially want to write a flatten
method which would convert this value from an Int??
to an Int?
by collapsing the None
states. However, this method can't live on the Option<T>
because it's only applicable when T
itself is an Option type. So one way to accomplish this would be as a type-specific extension:
extend Option<T: Option<U>> {
func flatten(self): U? = ...
}