Creating tools for Developers

DATE 2025-04-15 -- TIME 07:29:06

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 for Set-s. Oh yeah right well this also makes sense, numbers and sets can be added transitively
  • .. is for concatenating two String-s.
  • ++ is for concatenating two Array-s. I could’ve made these one operator… but why? Having two provides more local context and makes the code more intentional.
  • ~~ is for merging Object-s and Dict-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 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.
  • 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.
  • 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 so 1..10 isn’t valid, that’ll help you remember)
  • 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.

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