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 justify my arrogance and enhance my charisma.
Matches
Regex matches are done w/ a keyword: string is /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, and again you’re welcome.
Actually a lot of things can be done with the is keyword:
foo is {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 safe
Null coalescing ?? is a null-coalescing pipe operator! Like Maybe.map with 2 less words and 2 more symbols. And also ?|>, which executes
the rhs only if the lhs is not null.
Monads and map family of functions
If I want to look cool in front of the Haskell nerds (I do) I have to mention monads. But to look cool in front of the TypeScript nerds, who couldn’t tell the difference between a monad and an algebraic data type to save their life, I will tell you something that is only kind of correct, but you’ll sound smart repeating it:
Monads are just boxes for other values. Arrays have lots of values,
Option/Maybe has 0 or 1, Result has a value that is one type or another.
These are all monads! Read the blog below if you want more accurate details, but
this is close enough for me to present another idea: values as their own monad.
Define a map function on all values, and you get interesting behavior when it
comes to the Option type.
(Read A Gentle Introduction to Monads for more details).
let
a: Int = 5
b: Int? = 5
c: Int? = null
d: Array(Int) = [5]
in
{
a.map(fn(x) => x + 1) --> 6 -- Int
b.map(fn(x) => x + 1) --> 6 -- Int?
c.map(fn(x) => x + 1) --> null -- Int?
d.map(fn(x) => x + 1) --> [6] -- Array(Int)
} map, flatMap, and compactMap functions are defined on all values. map is a straightforward 1:1 mapping function on all containers, and all
“atomic” values, like numbers and strings. Except null, where no matter what
function you throw at it, it returns null. This makes map on optional types
functionally equivalent to the ?|> null-coalescing pipe operator.
Other map functions are also defined, but have different behavior:
flatMapexpects the same Monad type as return value.Set(T).flatMapreturnSet(U),Array(T)returnArray(U), etc. It’s a way to increase or decrease the number of values in a container.filterMapexpects an optional return type.nullis filtered, all other values are kept. If you need to keepnullbut filter something else, you can either return.some(null)or useflatMap.- These methods behave differently on container types and optionals, but on
value types (Int, String, etc) these all behave identically as
map. - Worth noting that for the sake of these functions,
Objectis a value type, not a container type.
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 expression. else if to chain
multiple checks.
if condition -- newline is just as good as 'then'
expr1
else if condition2
expr2
else
expr3 The inclusion operator can be accomplished using splat operators in JavaScript:
[1, 2, 3, ...(condition ? [x] : [])] But can we do better?
[
1
2
3
x onlyif x > 3
...items onlyif 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 ”#default”
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 only { 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 #default”. Many reserved words are prefixed by #,
which namespaces these so that I can introduce future macros or special features
while preserving backwards compatibility.
pub fn addEmUpPlusOne(# a?: Int, # b?: Int) =>
1 + addSomeNumbers(a ?? #default, b ?? #default)
-- P.S. above, we could've finished our garbage version with
-- `addSomeNumbers(#default, b)`. I was just joking when I said extra
-- couldn't handle this 😏 #default 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 #pipe for
the name. Actually I chose # but later I changed it when I introduced more
macro-like features (#default, #line, etc).
fullname
|> #pipe.split(' ') -- String -> Array(String)
|> #pipe.map(fn(name) => name.length) -- Array(String) -> Array(Int)
|> sum(...#pipe) -- Array(Int) -> Int
|> `$fullname, you have $#pipe letters in your name`
-- hack uses '$$' as the sigil for the pipe value, so I guess they didn't *quite* nail it... 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
╓ ╖
║ 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.
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! 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' 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 member function that
accepts 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: '>')
-- htmlify: fn(# text: String): String
in
htmlify('🥹') --> '<🥹>' 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.
There are also ways to count code points or to request a specific encoding, but asking “how many letters is this” is the thing that is easiest.
"🤯👩🏽🏫".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 only {
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