Description
Extra is Amazing // Extra
Let’s start with some code. This is some testing code that I commented-up. You
can copy/paste it into the repl (bun run repl
after a fresh
clone).
let
-- 'let' assignments support explicit types, which really helps
-- during testing (and is useful in general, I think)
ints: Array(Int, length: 1...5) =
[1, 2, 3] -- the assignment is, of course, checked by the compiler;
-- is Array(1|2|3, length: =3) assignable to Array(Int, length: 1...5)?
-- (spoiler: yes!)
in
ints is
-- ^^ the match operator!
[
Int(>=1) as a -- type check the first arg
b -- and assign it to 'a' if it matches
d -- assign second and third, whatever they are, to 'b' and 'd'
...e -- assign remainder to 'e'
]
and {a:, b:, d:, e:}
-- if it matches, run the rhs, which is populated with assignments from
-- matches. Here we're using the object shorthand {key:} => {key: key}
--> what is the return type?
false -- if `ints` doesn't match, the 'is' operator returns 'false'
| { -- otherwise it returns an object populated by the match operations
a: Int(>=1) -- type match applied to 'a'
b: Int -- just the array type
d: Int -- " " " "
e: Array(Int, length: <=2) -- remaining items – look at the length!
}
This isn’t hypothetical, this code is working! Look how much type information is presented here, and how much is preserved and inferred… it’s immensely satisfying to me to see this working.
The is
operator accepts a match expression on the rhs - another way to do
matches is using case
statements in conjunction with switch
.
let
… -- same as above
in
switch (ints) {
case [Int(>=1) as a, b, d, ...e]:
{a:, b:, d:, e:}
else:
null
}
-->
null -- if ints doesn't match, we get 'null' as a default
| { -- (implement 'else' to provide a default)
…
}
The length
refinement on Array
(and String
, Set
, Dict
) is treated with
the upmost care (submit an issue if you find something wrong!), for instance
concatenation operations on types that have min/max lengths will return a new
type with the appropriate min/max.
let
ones: Array(1, length: 1...5) = [1, 1]
twos: Array(2, length: 2...6) = [2, 2, 2]
all = ones ++ twos
in
--> again, taken from the REPL
[1, 1, 2, 2, 2]: Array(1 | 2, length: 3...11)
Ok so we have length information, so what? Sooo!? Now the compiler knows a
little more about array access. Any int 0...3
is now guaranteed to be in the
Array.
let
…
all = ones ++ twos
in
{
all[0] --> 1 | 2
all[1] --> 1 | 2
all[2] --> 1 | 2
all[3] --> 1 | 2 | null
}
Extra doesn’t stop there. Extra encodes relationships into the type system, and uses those relationships to infer… well, as much as possible! Safe array-access, dict key presence, etc.
And remember I said “relationships” there, so not just types… you’ll see.
Take the naive case of accessing an array with an ol’ Int
. We expect the
compiler to emit T | null
, because who knows, maybe that index is bigger than
the array bounds. (Python would have you believe that raising an exception is the correct thing to do here!? Python got a lot of things right, but this
is not one of the things I find particularly useful).
let
…
someInts: Array(Int) = …
index: Int = …
in
someInts[index] --> 1 | 0 | null (could be null, because of unsafe array access)
Like we saw above, if we know something about the length of the array, like its
length is at least 3, then we expect an Int(0...3)
to also be safe, right?
let
…
all = ones ++ twos -- Array(1 | 2, length: 3...11)
index: Int(0...3) = … -- not just any int
in
all[index] --> 1 | 2 - success!
Now for the real show: what if we don’t know the length of the array, or the magnitude of the int… can we still claw back this “safe array access”?
Yes we can, and you’ll never believe it, we just use if
. The type system
can store the relationship index < letters.length
, and use that information to
determine that the array access is safe.
let
letters: Array(String) = ['a', 'b', 'c']
index: Int = 1
in
if (
index > 0
and index < letters.length
) {
then:
letters[index] -- safe! returns 'a' | 'b' | 'c'
else:
'z'
}
--> 'a' | 'b' | 'c' | 'z'
Spoiler you could do this specific example way easier
let
letters: Array(String) = …
index: Int = …
in
letters[index] ?? 'z'
Now I will leave you with the real brag. I’m copying this again from the REPL, you can try it out yourself if you don’t believe me:
let
letters: Array(String) = ['a', 'b', 'c']
numbers: Array(Int) = [-1, 0, 1]
index: Int = 1
in
if (
index >= 0
and index < letters.length
and letters.length <= numbers.length
) {
then: {
letters[index],
numbers[index],
}
}
--> return type is
{ String, Int } | null
Unsafe array access results in optionals, so you would be forgiven if you expect
this to return { String?, Int? } | null
, because how does the compiler know
that numbers[index]
is safe?
We learned that the relationship of index < letters.length
(and index >= 0
)
is enough to tell the compiler that letters[index]
is safe… and because we
also told the compiler that numbers.length <= letters.length
, it deduces that index < numbers.length
!
Specifally, it is capable of tracking comparison operations coupled with simple addition. This means you can also do things like:
let
index: Int = 1
prev = index - 1
next = index + 1
items: Array(String) = ['a', 'b', 'c']
in
if (index > 0 and index < items.length - 1) {
then:
{ items[prev] , items[index], items[next] }
}
-->
{String, String, String} | null
This is very cool, and I’m glad you agree.
Published: 2025-07-23