Creating tools for Developers

DATE 2025-07-25 -- TIME 00:44:41

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.

letall = 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?

letall = 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