Description
Extra's Extra Features // Extra
In no particular order, I’d like to share a bit about some of the fun Extra features of Extra.
Refined types
I already talked about these!, but I want to mention them again because I think Extra provides a really aesthetic solution to a very common data modeling problem: functions that require N items in order to do their work. My canonical example is a function that expects an IP address.
type IPv4 = Array(Int(0...0xff), length: =4)
type IPv6 = Array(Int(0...0xffff), length: =8)
type IP = IPv4 | IPv6
Tuples and algebraic data types will get you close, sure, but Int in the range 0 to 255
as a type is just wonderful.
Extra’s Operators are Friggin’ Sweet
Math is for math
+
is for numbers. Why anyone thought it was appropriate to “add” strings and
arrays is beyond me. Read a book! (about math)
+
is also for Set
-s. Oh yeah right well this also makes sense, numbers and
sets can be added transitively that’s what math is
Distinct concat/merge operators
<>
is for concatenating twoString
-s.++
is for concatenating twoArray
-s. I could’ve made these one operator… actually I did at one point! But having two provides more local context and makes the code more intentional.~~
is for mergingObject
-s andDict
-s, and here the order really matters because values on the RHS will overwrite values on the LHS.
Isn’t it nice that they’re all double characters? So consistent. It’s these little things that provide me with all my arrogance and charisma.
Matches
Regex matches are done w/ a keyword: string matches /regex/
. This was the ~=
operator at one point - or was it =~
? And since I couldn’t remember then
I can’t expect you to either so here we are you’re welcome.
Actually a lot of things can be done with the matches
keyword:
foo matches {a, b, null}
would be true (and assign a
and b
) if foo
is
a 3-tuple with null
as its third value.
Booleans
Boolean operators are also words (and
or
not
) however I wanted Extra
to be easy to write, so the C-style &&
||
also work and no they don’t have
different precedences (glares at Ruby.. then sees Matz and smiles and gives a
thumbs up, wow what a nice guy I hope he didn’t see me glare)
Null coalescing ??
and also ?|>
is a null-coalescing pipe operator! Like Maybe.map
with 2 less words and 2 more symbols.
Ranges
Range operators. We need to sit down and agree that I got it right, because
it’s absurd that there are languages that expect us to memorize the difference
between ..
and ...
(one of those is inclusive!).
1...10 1<..10 1..<10 1<.<10
- three characters each, inclusive and
exclusive options on both sides.
I won’t insult you by pointing out which are inclusive and which are exclusive
because it’s obvious. (and don’t worry ..
is not an operator and so 1..10
isn’t valid)
Type checks
There is also is
for type checking and has
for array/dict key checking.
Likewise !is
and !has
, by the way. Why not not is
and not has
, to match
the logical not
operator? I don’t remember, and I don’t think I’ll change it.
Conditional operators
There is, of course, an if()
expression, but there is also an inclusion operator for arrays/sets/dicts/objects. Another Extra Original™ (as far as I
know… and I asked Claude and he agreed so it must be true).
if(condition, then: expr1, else: expr2)
is the “if function”, and there’s an
even better syntax for it, using “block arguments” (naming is hard):
if (condition) {
then: expr1
else: expr2
}
The inclusion operator is something new (as far as I know). You can accomplish this using splat operators in JavaScript:
[1, 2, 3, ...(condition ? [x] : [])]
But can we do better?
[
1
2
3
x if x > 3
...items if includeItems
]
I went back and forth on whether to include a ternary operator… if I change my
mind, I know that I will implement it using then/else
and not ?/:
, but I
find that ternaries inevitably lead to unreadable code, so I am not inclined to
add them at the moment.
Default value ”fallback”
This has come up many times for me, and yet I’ve never seen another programming language offer up this feature (I’m probably wrong, please leave a comment below if you know of one (I’m kidding I don’t have comments, I’m the only troll allowed to comment on this site thank you very much)).
Let’s imagine we are consuming an API that has the function signature below. We know that it provides default values, and maybe we could go look those up, but this is an external library, and so we want to treat those default values as black-box implementation details.
fn addSomeNumbers(#a: Int = XXX, #b: Int = YYY) => a + b
We are, like very good 1st year CompSci students, going to wrap this in a local abstraction layer.
import API : {addSomeNumbers}
pub fn addEmUpPlusOne(#a: Int = XXX, #b: Int = YYY) =>
1 + addSomeNumbers(a, b)
What should we use for those values? Well, if we make them null
-able, we could
tediously check for that and call the function according to whether the value was
provided:
pub fn addEmUpPlusOne(#a?: Int, #b?: Int) =>
1 + (switch ({a, b}) {
case: {null, null} => addSomeNumbers()
case: {a, null} => addSomeNumbers(a)
case: {null, b} => addSomeNumbers() -- ah, hell
case: {a, b} => addSomeNumbers(a, b)
})
Not only is this garbage, we can’t even do it, due to the inability to call addSomeNumbers
and specify the second but not the first argument (arguably
this is a limitation of Extra, because maybe we should be able to call the
function this way).
Enter the “default value fallback”, which I named fallback
because I am
sometimes chagrined when default
is a reserved word - it’s a useful
variable name.
pub fn addEmUpPlusOne(#a?: Int, #b?: Int) =>
1 + addSomeNumbers(a ?? fallback, b ?? fallback)
-- P.S. above, we could've finished our garbage version with
-- `addSomeNumbers(fallback, b)`. I was just joking when I said extra
-- couldn't handle this 😏
fallback
means “use whatever the default value is, but I don’t want to specify
it because that would break Demeter’s law and while she seemed like one of the
nice Gods, I’m not going to roll those dice.”
Pipe Operator
I just love the pipe operator, and while I appreciate how Elixir and Elm implemented it, I think Hack actually nailed it (I know right? When’s the last time you heard someone mention Hack, and were they praising it? I doubt it).
The |>
pipe operator takes the left-hand-side and makes it available to the
rhs, usually in the context of calling a function. But what Hack does is it
assigns the lhs to a sigil that you can use in the rhs. I chose #
:
fullname
|> #.split(' ') -- String -> Array(String)
|> #.map(fn(name) => name.length) -- Array(String) -> Array(Int)
|> sum(...#) -- Array(Int) -> Int
|> `$fullname, you have $# letters in your name`
-- hack uses '$$' as the sigil for the pipe value, so I guess they didn't *quite* nail it...
Having the sigil gives tons of flexibility for how you use the value on the right… but sometimes the sigil isn’t enough, and so there’s also support for a match expression:
fullname
|> name => name.split(' ')
|> names => names.map(fn(name) => name.length)
|> lengths => sum(...lengths)
|> count => `$fullname, you have $count letters in your name`
At this point, though, you should consider a let
expression:
let
names = fullname.split(' ')
lengths = names.map(fn(name) => name.length)
count = sum(...lengths)
in
`$fullname, you have $count letters in your name`
Lastly, there is a null-coalescing pipe operator, because I like sounding smart:
fn(name: String?): String[] =>
name
?|> #.split(' ') -- returns String[] | null
|> # ?? []
Really Beautiful Comments
You’re going to roll your eyes but I don’t care. Extra tosses their hair back over their shoulder and says Express Yourself! Don’t write comments… DRAW comments!
-- this is a comment
--> so is this, obviously
<-- but so is THIS, because pointing at things is handy (and RUDE)
{- block comments {- and yes they can be nested -} -}
-- YAWN's-ville so far so what!?
╓ ╖
║ Now THIS is a comment! ║
╙ ╜
All Unicode box-drawing characters are ALSO comment sigils. I’m a monster, I know, but now you’re hopefully starting to get a taste of that “Extra” flair.
╱ Wait...
│ *this* is a comment?
╲ Sure!
← oh and also this
→ and this
↔︎ but not this because I had to draw the line somewhere.
Function arguments
I went all in on having expressive function arguments. Positional, named, variadic, and beyond.
Positional Arguments
You can’t be Extra if you’re not also Basic.
fn print(#text: String) => System.Console.Stdout.write(text <> "\n") -- Suck it Java
print("hello")
Positional arguments require the #
prefix. I wanted to encourage named
arguments, but still allow for positional arguments when the argument name isn’t
necessary.
Named Arguments
Now we’re getting somewhere:
fn surround(pre: String = '', post: String = '') =>
fn(#text: String) => pre <> text <> post
let
htmlify = surround(pre: '<', post: '>')
-- also surround(post: '>', pre: '<')
in
htmlify('a') --> '<a>'
Named arguments can be passed in any order, and can come before or after positional arguments. Don’t get too excited, though, the code formatter rearranges them to be in the order specified by the function.
Curry-able
Maybe it would be cooler if this was automatic… but I’ll take “possible and
explicit” over automatic most days. curry
is a function on functions that
accept some (or all) of the arguments of the original function, and returns a
function that expects the rest.
fn surround(#text: String, pre: String = '', post: String = '') =>
pre <> text <> post
let
htmlify = surround.curry(pre: '<', post: '>')
in
htmlify('🥹') --> '<🥹>'
Spread Positional Arguments
This is when you want “and all the rest are BLAH”. They have to be an Array of the same type:
fn sum(...#numbers: Array(Int)) => numbers.reduce(fn(memo, value) => memo + value, 0)
sum() --> 0
sum(10) --> 10
sum(10, 32) --> 42
Spread Named Arguments WHaaaaat!?
A combination of positional-named-arguments and named-arguments. The name is repeated:
fn math(#number, ...plus: Array(Int) = [], ...minus: Array(Int) = [])
=>
number
+ plus.reduce(fn(memo, value) => memo + value, 0)
- minus.reduce(fn(memo, value) => memo + value, 0)
math(8, plus: 2, minus: 5, plus: 10, plus: 2) --> umm, it's uh... 15... no... 17!
Fun fact: switch
is “just a function” (or it could be) where the case
argument is a repeated-named-argument:
fn switch<T, U>(...case: CaseStatement<T, U>): U
=> --- impossible to actually implement, but you get the idea
switch(
case: …case1
case: …case2
case: …case3
)
Keyword arguments
Lastly, any named arguments you didn’t specify in the args-list can be put into
a Dict
(kwargs in Python-speak)
fn greet(*names: Dict(String))
=> -- it's getting late, so please imagine a function implementation here
greet(colin: 'hello', alice: 'welcome', bob: 'sure come in I guess')
--> 'hello, colin. welcome, alice. sure come in I guess, bob'
String literals are 🔥🔥🔥🔥🔥
Let’s crack those knuckles and get the basics out of the way:
-- single quotes are simple, but do support escape sequences
'yeah wow\nthat IS $money' -- just how it looks (\n is a newline)
-- as of 2025-04-15 the '$' is being treated as a sigil by the code highlighter,
-- but I don't know why
-- double quotes support string interpolation
"You have $time remaining seconds"
-- as of 2025-04-15 the code colourization isn't detecting $time as an
-- interpolated string
-- backtick is an alternative to double-quoted, because I love all my
-- quote-looking characters
`I'm a backtick ${'string'}`
Sure, fine so far, but where’s the EXTRA!?
-- triple quoted variants
'''now that's a spicy meatball'''
-- if you put a newline after the triple quote, it will be *ignored*
'''
now that's a spicy meatball'''
-- indentation will also be ignored!
'''
now that's a
spicy meatball'''
==
'''
now that's a
spicy meatball'''
-- the final newline isn't ignored
'''
now that's a
spicy meatball
''' --> ends in a newline
-- unless you escape it with ''
'''
now that's a
spicy meatball''' --> ends in a meatball
Um what else, variable names I guess?
If it’s not a box-drawing character, and not an operator, I guess it’s a variable name? Also they can be hyphenated.
let
guess-my-age = 37 -- not true; copilot offered this up. what a flirt
🤓 = '😎'
in
…
string.length is pretty much what you expect - how many glyphs
"🤯👩🏽🏫".length --> 2
Commas are optional
Oh man I really buried the lede on this one. This is so cool!
In all cases where commas delineate items (arrays, imports, function arguments), they are optional if you use newlines instead.
The rule for “will it be considered a comma” is “is the next thing an operator?”
The one exception is “is the next thing a negative number”.
import Http : {
get
post
}
[
1
2
-3
4
+ 5
] --> [1, 2, -3, 9]
sum(
1
2
3
4
) --> 10
That’s a lot of Extra and I kid you not I got sleepy before I ran out of cool stuff to talk about.
See you next time!
Published: 2025-03-13