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
+
is for numbers. Why anyone thought it was appropriate to “add” strings and arrays is beyond me. Read a book! (about math)+
is also forSet
-s. Oh yeah right well this also makes sense, numbers and sets can be added transitively..
is for concatenating twoString
-s.++
is for concatenating twoArray
-s. I could’ve made these one operator… but why? 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.
- Regex matches are done w/ a keyword:
string matches /regex/
- Actually a lot of things can be done with the
matches
keywordfoo matches {a, b, null}
would be true (and assigna
andb
) iffoo
is a 3-tuple withnull
as its third value.
- 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! LikeMaybe.map
with 2 less words and 2 more symbols. - 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
I won’t insult you by pointing out which are inclusive and which are exclusive because it’s obvious. (and don’t worry..
is the string concatenate operator and so1..10
isn’t valid, that’ll help you remember) - There is also
is
for type checking andhas
for array/dict key checking. Likewise!is
and!has
, by the way. Why notnot is
andnot has
, to match the logicalnot
operator? I don’t remember, and I don’t think I’ll change it.
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
easily 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`
Having the sigil gives tons of flexibility for how you use the value on the right.
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.
Function arguments
I went all in on having expressive function arguments.
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")
Named Arguments
Now we’re getting somewhere:
fn surround(pre: String = '', post: String = '') =>
fn(#text: String) => pre + text + post
let
htmlify = surround(pre: '<', post: '>')
in
htmlify('a') --> '<a>'
Named arguments can be 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.
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, 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
-- oh and backtick strings can be _tagged_
html`<div class="fancy">hello</div>`
Uh I dunno, 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