## The Extra Programming Language
## TL;DR
`bun run repl`
Extra is a strongly-typed language and runtime that can be used to create client-side applications (and other things, I suppose but it's aimed at _frontend_). It's closest cousin is Elm, second cousin to React, long-time-listener-first-time-caller to Svelte, and uncanny valley similarity to TypeScript.
## OK, tell me moooore...
While Elm made good on the promise of being extremely well-reasoned, it was painful, to me, to compose components that needed to track their own internal state. Extra makes that really easy – but still explicit. While I was in there, I figured it wouldn't hurt to add TypeScript's branch-based type inference and derived types. Might as well add Swift's `guard` expression, too... and JSX is a great idea (but can we make it even more ergonomic?).
Extra will feel familiar to React developers, but without the cognitive dissonance of "let it render" or is it "prevent too many rerenders", and obviously not the "this was your best idea?" mess that is hooks. Whenever someone says "React is (declarative|functional|good|fine|not-a-mess)!" I die a little inside.
The big difference in Extra with other "render and diff" frameworks is how views are _updated_. Think spreadsheets instead of DOM diffing.
When you update a cell in a spreadsheet, the application is able to know exactly what cells were depending on that cell. It can create a dependency graph of all the downstream dependencies, including charts and pivot tables, triggers, etc, and only update _what is needed_. This is the strategy I took in Extra (also called "push based updates").
In Extra, your `` components create a runtime that is capable of tracking atomic changes. Think "assign new string value" and "push to an array". These atomic changes are handed to the components that were depending on that value, and the changes are propogated to the corresponding view object (dom or native view).
# I'm completely sold! But show me some more cool things nonetheless.
Before I jump into the application architecture, let's get to know Extra first. Because on top of being a really interesting runtime, it's also a pretty-darn-good™ programming language!
## Quick syntax primer
Just a little sample of code to get familiar with the syntax.
```extra
-- comments are hyphenated, like Ada
{- or nested like this -} -- like Haskell or Elm because they're cool I'm cool
<-- also this! Finally you can *point* to things using comments.
-- `let` assigns values to scope. Subsequent assigns can depend on previous
-- ones, and they all get passed to the `in` block
let
forty = 40, two = 2 -- comma separated
someNumber = 2 * 1 + 40 -- or newline separated
name = "Extra"
-- # name: -> positional argument
-- age: named argument
-- return type is inferred
fn format(# name: String, age: Int) =>
`Hello, $name! Are you $age years old?`
-- if you wanted to express the return type:
-- fn format(# name: String, age: Int): String
in
format(name, age: someNumber)
let
max = 10
-- hyphens are allowed in names
-- functions close-over local variables (`max`)
fn is-divisible-by-3(num: Int) =>
num % 3 == 0 and num < max
-- `if` syntax is "clean" - no parens. Expressions are terminated by a
-- newline, unless an operator indicates there is more. (the parser looks
-- ahead for these trailing operators, they can be on the current or next
-- line)
evens =
if max > 0
and max <= 10
[2, 4, 6, 8, 10]
else if
-- I'm just messing with whitespace to show what is possible. the
-- built-in code formatter (`extra normal`) can format this nicely
max > 10
and max <= 20
-- the ++ at the end indicates that the expression is incomplete
[2, 4, 6, 8, 10] ++
[12, 14, 16, 18, 20]
else
[2, 4, 6, 8, 10, 12, 14]
-- here the ++ comes *after* the entire if expression - that's ok, too
++ [-1]
odds = [
1 -- look ma, no commas!
3
5
7
-- '...' is the 'spread' operator; these items will be included in the array
...[9, 11]
-- 'then' can be used to form a ternary-expression-like-syntax
-- (the code formatter will attempt to keep these on one line)
...if max <= 20 then [13, 15, 17, 19] else []
-- even better, the `onlyif` operator – only allowed in arrays, objects,
-- dicts, and sets. `11` is only included in the array if `max > 10`.
11 onlyif max > 10
]
in
[...evens, ...odds]
.filter(is-divisible-by-3)
.sort(by: |a, b|
-- a <=> b will sort in ascending order, here we sort in descending order:
b <=> a
) --> [9, 6, 3]
-- the pipe operator assigns the left-hand-side to the `#pipe` symbol
|> inspect('filter', #pipe) --> prints "filter = [9, 6, 3]: [Int]" and returns that value
|> #pipe.map(|num| $num).join(',')
-- there's a JSX-like syntax built in.
Hello, Extra!
-- this is no longer a comment
{- this *is* a comment -}
-- arrays, dicts, sets, and objects support an inclusion operator `onlyif`
-- in this case, 'italic' is included in the array only if `@is-italic` is true
Hello, World!
```
Apps/Components are created using the `view` keyword, which is either a class or
pure function.
```extra
view Login {
@email: String = ''
@password: String = ''
fn handle-submit =>
-- if you've never seen the 'guard' expression, prepare to fall in love
guard
@email and @password
else
null
Request.post(API_URL, {email: @email, password: @password})
render =>
}
```
# Now in no particular order, some language features of Extra, that make it “Extra”
## Type refinements
You can provide much more type information to Arrays, Dicts, Sets, Strings, and Numbers. You can define types like "an Array of Ints, with at least one item, where each Int is greater than 0" (`[Int(>0), length: >=1]`).
In my mind, an "empty String/Array" is a different _type_ than "a String with 5 or more characters." And the reason they are different types is because there are often cases where I _know that I will need at least one of the thing_. For instance, a `name: String` variable. Would't it be nice if I could say `name: String(length: >0)`, indicating that it must have at least one letter? _Yes we can!_
```extra
String(length: =8) -- String of exactly length 8
String(length: 8) -- same, but =8 is canonical
String(matches: /^\d!$/) -- String matching a regex
String(matches: [/^.\d+!$/, /^a/]) -- String matching multiple regexes
Int(<8) -- any Int less than 8
Int(0...10) -- any Int 0 to 10, inclusive
Float(0..<10) -- any Float greater than or equal to 0, less than 10
Float(-10<.<10) -- any Float greater than -10, less than 10
Int(=8) -- this is just the literal number 8
Int(8...8) -- so is this!
Int(7<.<9) -- and this.
8 -- literals are also valid types
[Foo, length: >=3] -- Array of type 'Foo' with at least 3 items
[Foo, length: <=3] -- Array of Foo with 3 items or less
[Foo, length: =3] -- Array of Foo with exactly 3 items
-- < <= >= > comparisons also work
[Foo, length: <=3] -- array of Foo with no more than 3 items
[Foo, length: >3] -- array of Foo with more than 3 items
-- and ranges
[Foo, length: 2...4] -- array of Foo with 2, 3, or 4 items (inclusive range)
[Foo, length: 1<.<5] -- array of Foo with 2, 3, or 4 items (exclusive range)
[Foo, length: 2..<5] -- array of Foo with 2, 3, or 4 items (exclusive range)
[Foo, length: 2<..5] -- array of Foo with 3, or 5 items (exclusive range)
-- Dict / Maps
Dict(Foo, length: 3+) -- dict of Foo with 3 or more items
Dict(Foo, length: 3...10) -- dict of Foo with 3 to 10 items in it
Dict(Foo, keys: [key1:, key2:]) -- dict with specified keys - these keys must be present
Dict(Foo, keys: [key1:, key2:]) -- dict with specified keys - these keys must be present
Dict(Foo, keys: [key1:, key2:], length: 3+) -- specified keys and length >= 3
-- these types can be combined:
[String(length: =8), length: =10] -- array of strings
-- each string is 8 characters
-- and there are 10 of them in the array
[length: =10, String(=8)] -- if you prefer, these arguments can be rearranged
```
## Default value placeholder.
For situations where you are calling a function that offers a default value. Imagine a scenario where in _some_ cases you want to specify the argument, and in other cases you want to use the default.
I've chosen the name `#default` for this value. The `#` prefix is reserved for Extra and maybe also macros?
### Case 1
You only want to specify 1st and 3rd positional arguments.
```extra
foo(1, #default, 3)
```
This calls the function `foo` with the first and third arguments specified, but the second argument will _defer to the default value_. So simple, so handy. What _is_ the default value in this case? I dunno! Should I know? Do I look up the API for that? What if it changes?
### Case 2
If `b` is specified, use it, otherwise use the default.
```extra
let
fn bar(# a: Int, # b: Int = 10) =>
a + b
fn foo(# a: Int, # b: Int | null) =>
bar(a, b ?? #default)
in
[
foo(1), --> 11, default value of 10 is used
foo(1, 1), --> 2
]
```
In other languages, in order to avoid hard-coding b's default value 10 you would
have to provide two separate calls to bar:
```extra
fn foo(# a: Int, # b: Int | null) =>
if b == null
bar(a)
else
bar(a, b) -- 🤢
```
It really shakes my pepper that this doesn't exist in more languages! How is this not a thing!? I've often felt that I wanted this. Maybe it's just me. 🤷♂️
## Pipe operator 🤓
I'm a big fan of pipes from Elm and Elixir. In these languages, the value entering the pipe is automatically inserted into the receiving function. I think that having a sigil represent where you want the value to go gives them even more flexibility. Slow approving nod to Hack for this idea.
I picked `#pipe` for the name, the `#` sigil indicates "interal use", and is used for macros like `#default` and `#line`. JS's proposal currently favors `^^` I think? 🤢 Why can't JS do anything right... and why don't they just _ask me_, since I seem to know all the answers.
```extra
'abc' |> #pipe .. #pipe
--> 'abc' .. 'abc'
--> 'abcabc'
-- extract two elements from an object, place them in an array
{a: 'a', b: 'b', c: 'c'} |> [#pipe.a, #pipe.b]
```
Also available is the "null coalescing pipe". If the value is `null`, it skips the pipe and returns `null`. Otherwise, invokes the pipe with the non-null value. Elm would call this `Maybe.map`. Haskell would call this - ok I had to look this up and I got confused so I don't know what Haskell would call this. `>>=` or maybe `<*$>`.
```
let
fn example(# foo: String?) => foo ?|> #pipe .. "!"
in
[
example('bang') --> 'bang!'
example(null) --> null
]
```
I toyed with the idea of being able to name the pipe value... I decided against it. In most cases, I prefer having just one way to do things.
## Algebraic data types _of course_
In particular: **Sum Types**. Shoutout to [Justin Pombrio – but please get out of my head and stealing my rants](https://justinpombrio.net/2021/03/11/algebra-and-data-types.html#:~:text=The%20Baffling%20Lack%20of%20Sum%20Types) for a great writeup on Sum and Product types.
```extra
enum RemoteData {
.notAsked
.loading
.failure(error: Failure)
.success(value: Success)
static maybe(# value: S?): RemoteData(S, F) =>
if value
.success(value)
else
.notAsked
fn data(): Success? =>
switch this
case .success(value)
value
else
null
}
let
remoteData: RemoteData = .success('data loaded')
in
remoteData.data() --> 'data loaded': String?
```
There is also a shorthand syntax, only available when defining an enum as an
argument type:
```extra
fn print(
# text: String
color:
-- initial '|' is optional, but looks nice in multilines
| .rgb(r: Int(0..<256), g: Int(0..<256), b: Int(0..<256))
| .hex(String(length: =6))
| .name('red' | 'green' | 'blue')
| null
) =>
if color then …
```
**Product Types** in Extra are the good ol' `Object` type – `Record` or `struct` in other languages. Extra Objects are also Tuples, because the property name is optional - you can have positional and named properties (which aligns them with how function arguments support positional and named arguments - function arguments are just Tuples (or Objects)!)
## Functions with properties
TypeScript has a syntax for this - do you remember it? Well you won't remember this any better, even though it's much better and more memorable. Start by writing a function, then change direction partway through into an object, then go ahead and write that function, then return to writing object properties.
```extra
-- 'fn' starts the function, then '{' indicates something else is going on
adder = fn{
-- continue writing the function
(# lhs: Int, # rhs: Int) => lhs + rhs
-- then continue with the rest of the properties
inc: fn(# input: Int) => input + 1
dec: fn(# input: Int) => input - 1
}
adder(1, 2) => 3
adder.inc(3) => 4
adder.dec(4) => 3
```
## Type modifications
This is one of the killer features from TypeScript, and I am stealing it with no shame or embarrasment. "Stealing" is too strong a word, because I'm only supporting some parts.
### Omit/Pick
```extra
type User = {name: String, age: Int(>=0)}
type Ageless = Omit(User, 'age') -- User, but remove 'age'
type Named = Pick(User, 'name') -- User, but only 'name'
type StillUser = Pick(User, 'name', 'age')
```
### Include/Exclude
`Include(T, ...Types)` returns types of `T` that are assignable to `Types`. alias: `Extract`
`Exclude(T, ...Types)` removes `Types` from `T`
```extra
type A = 'a' | 'b' | Int | {name: String}
type OnlyBAndPositiveInts = Include(A, 'b', Int(>=0))
--> 'b' | Int(>=0)
type NoBOrNegativeInts = Exclude(A, 'b', Int(<0))
--> 'a' | {name: String} | Int(>=0)
enum Status {
.notAsked
.loading
.error
.done
}
type Pending = Include(Status, .notAsked, .loading)
--> Status.notAsked | Status.loading
type NotLoading = Exclude(Status, .loading)
--> Status.notAsked | Status.error | Status.done
```
### NonNull
`NonNull(T)` is a convenient shorthand for `Exclude(T, null)`. alias: `NonNullable`
### Partial/Required
`Partial(T)` makes all the properties of an object optional.
`Required(T)` removes `null` from the type of all the properties.
Note, `Required` doesn't remove `null` from a union type. If `Partial` doesn't _add_ `null` to a union, `Required` (the inverse) shouldn't remove it.
```extra
type Post = {title: String, created-at: Temporal.Instant, content?: String}
type DraftPost = Partial(Post) -- all fields are optional
type FinishedPost = Required(Post) -- 'content' becomes non-null
-- `Required` removes `null` from *properties* but not from a union type
type Info = {name: String?} | null
type RequiredInfo = Required(Info) -- {name: String} | null
```
### Return/Params
`Params(T)` extracts the arguments of a function as a tuple type. alias: `Parameters` or `Arguments`
`Return(T)` extracts the return type. alias: `ReturnType`
```extra
type CreateUser = fn(name: String, age: Int): {User, Boolean}
fn create-user(name: String, age: Int) =>
{User(name:, age:), age > 42}
type CreateUserArgs = Params(CreateUser) -- {name: String, age: Int}
-- also accepts constants that are in scope
-- type CreateUserArgs = Params(create-user) -- {name: String, age: Int}
type CreateUserReturn = Return(CreateUser) -- {User, Boolean}
```
### Element
`Element(T)` accepts an Array, Set, or Dict, and returns the type that it contains.
```extra
type User = {...}
type Users = [User]
type UserAgain = Element(Users)
```
## From Types to Views
Extra has built-in types, which you can build up into custom types. You can then `box` those types, which creates a simple wrapper around them. Then you can add functions to those via a `struct`. If you need mutability or inheritance, upgrade to `class`. If it's being rendered, it's a `view`.
`type → box → struct → class → view`
### Type
This is just an alias, it acts just like the type it aliases. You can also create type constructors.
```extra
type User = {name: String, age: Int(>=0)}
type Draftable(T extends {}) => { draft: Partial(T), complete: T? }
let
bob: User = {name: 'bob', age: 0}
bob-form: Draftable(User) = {
draft: {}
complete: bob
}
```
### Box
For cases where you want to make an opaque wrapper around a simple type. The canonical example is an `Id` type, like a `String` or `Int`, but you don't want to allow _all_ `Int` values, you want them to be explicitly marked as `Id`.
Boxed types have a `value` property to extract the wrapped value. They also implement functions `map` and `rewrap` to modify the value. `map` unboxes the value (you can return anything), `rewrap` requires the same type to be returned, and returns the same boxed type.
```extra
box UserId = Int(>0)
let
user-id = UserId(1)
-- calculating next-user-id three ways:
next-user-id = UserId(user-id.value + 1)
next-user-id = user-id.rewrap(|id| id + 1)
next-user-id = user-id.map(|id| UserId(id + 1))
-- functions that expect a
fn select(id: UserId) =>
-- only instances of UserId will be accepted,
-- not any ol' Int
...
in
❌ user-id + 1 -- won't work, because `user-id` is a boxed Int
❌ select(1) -- won't work, because `select` requires a boxed Int
```
### Struct
Think of `box` as a very simple `struct`. In a struct you can define all the properties and functions that you want on that type. Structs do not take part in Extra's state/mutation, do not support inheritance, and do not support custom constructors.
```extra
struct User {
name: String
age: Int(>=0)
next-age() =>
User(name: this.name, age: this.age + 1)
}
```
### Class
Lots to say about classes, because this is where we introduce Extra's flavour of "mutability" (spoiler: Extra is immutable - mutations are implemented via messages that describe changes, and the runtime takes care of implementing changes and updating views).
Classes have single inheritance, custom constructors, and like I said, they maintain mutable state.
```extra
class User(name: String, age: Int(>=0) = 0) {
@name = name
@age = age
-- this looks like a mutation, but it's actually a "Message", which has the
-- special symbol `&`. This has the message type `&Increment(@age, 1)`.
birthday() => @age += 1
}
class Student(name: String, age: Int(>=0)? = null, grade: Int(>0) = 1) extends User(name:, age: age ?? #default) {
@grade = grade
-- @{} is a message tuple.
-- This one is @{ @Increment(@age, 1), @Increment(@grade, 1) }
birthday() => @{
super()
@grade += 1
}
}
```
### View
And finally, the `view` type, which requires a `render` function. The constructor defines the props. Props are allowed (expected) to change, but state initialization only happens when the component is created. Stateless view functions are supported, as well as stateful components.
```extra
view Foo(message: String) => {message}
view Foo(message: String) {
@bangs = ''
tick() => @bangs ..= '!'
render =>
<>
{message}{@bangs}
>
}
```
There's a ton more to say about Views.
## Comments
I may have gone a bit overboard, just a heads up. 🤓
Haskell & Elm & Ada inspired:
```extra
-- line comment
{- block -}
{- block {- with nesting -} -}
```
The usual comment characters `#` and `//` both have special meaning in Extra, and so I looked elsewhere for inspiration, and looked no further than Ada (and yes, Ada, Elm, Lua _all_ use `--` for line comments... but Ada has a certain caché so I wanted to mention it first).
Let's get interesting:
```extra
--> arrow style line comment
"no longer a comment" <-- this is code (and this is a comment!)
<-- alternate arrow style line comment
← why stop there? arrow characters are also comments
→ pointing is rude, though
{- comment block, line 1
{- comment blocks _can_ be nested -}
comment, line 3 -}
-- Handy trick to comment/uncomment multiple lines easily:
{--} <-- removing the '}' here will turn all four lines into a comment
multiple |>
lines
--} <-- This brace is just part of a line comment until the '}' above is removed
```
## Extra Comments, or _Let's Get Weird_
This is maybe a little out of hand, but I like drawing boxes using old-school ASCII characters, so there's support for these as line-comment start characters.
All box-drawing characters _are also valid comments_ (U+2500 – U+257F).
```extra
╭────────╮
│ yup. │ ┌────────╖
╰────────╯ │go nuts!║
╘════════╝
```
Here's the complete set, so you can copy/paste your favourites:
```
0 1 2 3 4 5 6 7 8 9 A B C D E F
U+2500 ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏
U+2510 ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟
U+2520 ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯
U+2530 ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿
U+2540 ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏
U+2550 ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟
U+2560 ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯
U+2570 ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
┌─┬─┐ ╒═╤═╕ ╓─╥─╖ ╔═╦═╗
│ │ │ │ │ │ ║ ║ ║ ║ ║ ║
├─┼─┤ ╞═╪═╡ ╟─╫─╢ ╠═╬═╣
│ │ │ │ │ │ ║ ║ ║ ║ ║ ║
└─┴─┘ ╘═╧═╛ ╙─╨─╜ ╚═╩═╝
┍━┯━┑ ┎─┰─┒ ┏━┳━┓ ╭─┬─╮
│ │ │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
┝━┿━┥ ┠─╂─┨ ┣━╋━┫ ├─┼─┤
│ │ │ ┃ ┃ ┃ ┃ ┃ ┃ │ │ │
┕━┷━┙ ┖─┸─┚ ┗━┻━┛ ╰─┴─╯
```
### Blocks and Lazy types
Arguments can be marked `Lazy`, in which case they look like a value at the call-site, but are not evaluated until the parameter is invoked.
Here is a function definition using `Lazy` arguments:
```extra
fn doSomething(condition: 1 | 2 | 3, one: Lazy(T), two: Lazy(T), three: Lazy(T)) =>
switch condition
case 1
one()
case 2
two()
case 3
three()
-- usually you would call the function like this - "vanilla" extra code
doSomething(1, one: 1, two: 2, three: 3) --> 1
-- but the named arguments DSL allows this:
doSomething(1) {
one: 1
two: 2
three: 3
} --> 1
```
## Pattern Matching
_Obviously_ Extra supports pattern matching. `switch` is the most canonical way to group a bunch of matchers, but `is` is handy in a pinch. This was hard so you better like it!
```
-- Syntax:
-- [subject] is [matcher]
-- Or
-- switch [subject]
-- case [matcher]
-- expr
-- Ex:
subject is .some(value)
switch subject
case .some(value)
value
foo --> matches everything, assigns to 'foo'
_ --> same but ignore the value
1, 1...2.5 --> matches numbers and ranges
"foo" --> string literal
"<" .. tag .. ">" --> prefixed/suffixed string (assigns middle to 'tag')
/^<(?.*)>$/ --> matches a regex, assigns named capture group to variable 'tag'
[] --> matches an empty array
[a, _, b] --> matches an array with exactly least 3 items
[a, ..., b] --> matches an array with at least 2 items, assigns the first to 'a', and the last to 'b'
.blue --> matches an enum case
.rgb(r,g,b) --> matches and assigns values
{name:, address:} --> matches the object if it has properties name: and
--> address:, and assigns to 'name' and 'address' variables
```
###### Numbers
```extra
-- number matching works on literals and ranges
switch volume
case 0
'muted!?'
case 1..<2
'turn it up!'
case 2..<5
"that's enough"
else
`$volume is too loud`
```
###### Strings and Regex
Strings can be matched against regexes, and will assign matches to named capture
groups, or you can match against a prefix and assign the remainder.
```extra
switch name
case /(?\w+) (?\w+)/
`Hello, $first $last!`
case "Bob " .. last
`Did you say Bab? Bab $last!?`
case _ .. "!"
"Your name ends in an exclamation mark, wow, that's so cool 🙄"
else
`Hello, $name!`
```
###### Arrays
```extra
-- match specific lengths, or any length using the spread operator
switch friends
case []
"Aww, I'll be your friend"
case [one-friend]
`$one-friend sounds like a great friend!`
case [first, last]
`Wow you know $first and $last!?`
case [first, _, last]
`Wow you know $first and $last!? And someone else, but I forgot their name.`
case [...some, last]
`${some.join(", ")} and $last... that's too many friends.`
```
###### Enums
```extra
enum Permission {
.sudo
.sure-why-not
.readonly
.special(level: Int)
}
switch permission
case .sudo, .readonly
true
case .special(level:)
level > 10
else
false
```
###### Objects
```extra
-- match named or positional arguments, and you can *nest* matchers, which makes
-- this really useful
fn permission(user: User): Permission =>
switch user
case {role: .admin}
.sudo
case {name: "Colin"}
.sure-why-not
case {name: name, role: .staff}
.readonly(name)
else
.none
```
###### Putting it all together
```extra
-- input: String | [String]
switch input
case 'foo' .. bar
bar
-- bar: String, input: String
-- (TODO: add 'prefix' info to String type)
case [onlyOne]
onlyOne -- onlyOne: String, input: [String, length: =1]
case [...many, last]
many.join(',') .. ` and $last` -- many: [String], last: String, input: [String, length: >=1]
else
'not "foo…" or [a, …]'
```
## Match operator
You would be forgiven for thinking `is` is the _instanceof_ operator... and you'd be right, even though you're wrong: it's the "match" operator. ie `if x is .some(val)` will attempt to match the two sides. In this case, if the match succeeds, `val` will have the unwrapped value of `x`.
```extra
let
x = .some(42)
in
if x is .some(val)
-- val == 42
val
else
0
```
## String coercion and interpolation
Extra's "coerce to String" function is a unary operator `$`, and it's also the string interpolation delimiter.
```extra
-- look at the beautiful similarity between String templates
-- and String coercion:
`How many: $n`
"How many: " .. $n
-- because it's an _operator_, you can do things like
let
n = 1
in
$(n + 1) --> "2"
```
## Unambiguous operators
`+` is a mathematical operator that adds two numbers. Did you know that `a + b == b + a`? Except in Java and Javascript and Swift and many other languages. 🙄
`++` is a computer science-y looking operator that concatenates two arrays. `..` does the same for strings. Having distinct concatenation operators is either really nice for indicating intentionality, or an unnecessary distinction. I hate to side w/ PHP on this one, but I treat 'em differently. Or hey maybe I'm hitching my ride to PHP's weird and shocking resurgence!? Who knows!?
Words (`and` `or` `not` `is` `has` `else`) are used for logical operators, but not bitwise operators (`&` `|` `^` `~`). But I like to think I'm a reasonable person, so I also treat the traditional operators as aliases (`&& → and`, `|| → or`, `! → not`, etc).
## Commas are optional
I've tried hard to make sure the language grammar can unambiguously determine whether you are still writing an expression, or starting a new one. This allows for arrays, function-arguments, and imports to have commas as optional.
```extra
[
1
2
3
-4
-- if you want to continue the line, you need to end in an operator
8 -
5 -- equivalent to `8 - 5`
] --> [1, 2, 3, -4, 3]
--> [1, 2, 3, -4, 8 - 5]
{
name: 'Extra'
is-awesome: true
awesome-level: 11
}
-- > {name: 'Extra', is-awesome: true, awesome-level: 11}
add-two-numbers(
1
2
) --> 3
-- > add-two-numbers(1, 2)
-- import `sqrt` and `pow` functions from the Math package
import {
sqrt
pow
} from Math
-- import { sqrt, pow } from Math
```
### Classes
Even functional programming languages deserve classes! And in Extra, classes serve a very special purpose. While all types are immutable, _classes_ can at least _appear_ to be mutable...
```extra
class User {
-- class properties are prefixed with '@'
@name = ''
@count = 0
fn change-name(# new-name: String) =>
@name = new-name
fn increase() =>
@count += 1
}
```
This turns out to be magic sauce, and it is _the mechanism_ that powers UI updates. More in a bit.
Classes have single-inheritance, support generics, and support static methods.
# More formal language Design
Lots of repetition here. The above is a whirlwind tour, now I'll try to be more precise.
## Basic Types
### Null
`null`
**Don't Panic!** Null safety is built-in, and "calling method on `null`" is prevented by the compiler (if it's not, [open an issue!](https://github.com/colinta/extra-lang/issues))
### Booleans
`true` and `false`
### Truthiness and the Conditional type
I went back and forth on having "truthy" types. Most functional languages are strict about what goes in an `if ()` expression - only Boolean is allowed.
But this makes the `and` and `or` operators much less useful as short-circuiting operations. For instance, imagine you want to provide a default error message:
```extra
let
message = error.message or "Try that again please"
in
…
```
I think the intention above is clear - and the below is no less clear, but at the expense of a ton of boilerplate.
```extra
let
message = if error.message != ""
error.message
else
"Try that again please"
```
And so, Extra has "Truthiness", and we take a page from Python: anything "empty" is considered false.
```extra
null -- the null value
false -- the false value
0 -- the number 0
"" -- empty String
[], Dict(), Set() -- empty array, dict, set
{} -- empty object, tuple
1/0 -- NaN --> false… I guess? I dunno! What would **you** do with this dumb value!?
```
That leaves everything else as "truthy":
```extra
true -- the true value
1 -- any number != 0
"any" -- any String that isn't ''
[0], [a: ""], {""}, {foo: ""} Set(0) -- any non-empty array/dict/object/tuple/et
```
Exception: Views and Class instances (including Regex) are always truthy, and so it is considered a compile-time error to use them as a truthy value.
### Numbers
`1, 2, 0x10, -0b1001, 4e2, 1__000_000` --> Int
`1.0, 2., -0.000_001, 4e-2` --> Float
#### Supported number prefixes for other bases
- `0x` --> hexadecimal (not 0X)
- `0o` --> octal (not 0O)
- `0b` --> binary (not 0B)
TODO: Dozenal.
#### Supported formats
- any number of `_` are ignored
`1_000` --> 1000
`1___000` --> 1000
`0b_1111_0000` --> 240
- Scientific notation "m e ** p" is supported:
`42e4` --> 42 \* 10 ** 4 = 420,000
`6.022e23` --> 6.022 \* 10 \*\* 23
If you're thinking "wow these are all supported by JavaScript's `Number()` constructor" then you've figured out what language this is all built in, without noticing the two dozen JS config files in project root.
### Strings
Strings come in a few variants: single-quoted, double-quoted, backticks, and atomic. The quoted variants all support triple-quotes (`'''test'''`). Backticks support string interpolation and "tagged" strings. Single-quoted and double-quoted strings do not support String interpolation (`${}`).
Strings can be spread across multiple lines, though I _recommend_ triple-quotes for that. Triple quotes have the added feature of removing preceding indentation, up to the closing quotes (more below).
```extra
'testing' --> testing
'$money' --> $money
'test1\ntest2' --> test1
test2
'test1
test2' --> test1
test2
```
An even simpler string literal is the "atomic" string, so called because in Ruby and Elixir they are a different 'atom' primitive. They can only have letters, numbers, hyphens, underscores, and emojis.
```extra
:testing --> "testing"
:real-money --> "real-money"
:$wat --> ❌ syntax error
:🤯 --> "🤯"
```
Double-quoted strings: Same as single-quoted, just an alternative quote symbol.
Backticks: Support _string interpolation_.
```extra
"testing" --> testing
`$money` --> replaces $money with the stringified contents of `money`
`${money.currency}` --> replaces ${…} with the contents of `money.currency` reference
`$money.currency` --> replaces $money with stringified contents of `money`, but leaves ".currency"
`\$` --> If you need a dollar sign
`$123` --> If '$' isn't followed by a reference, there's no need to escape it.
```
String tags work similar to how they do in Javascript - the parts of the string are passed to the 'tag', which better be a function capable of handling all the parts.
Unlike in JS, though, each "part" is passed as its own arg (the string literals are not gathered into one array). Anywhere you use interpolation, the type of the argument is preserved - for example, `Int`s below.
```extra
let
one = 1
two = 2
calculator = fn(# a: Int, # op: String, # b: Int, # out: String) =>
let
result =
if op is /^\s*\+\s*$/
a + b
else
a - b
out = out.replaceAll('?', with: $result)
in
`$a$op$b$out`
in
calculator`$one + $two = ?`
--- ^^^^ a
--- ^^^ op
--- ^^^^ b
--- ^^^^ out
```
Triple quotes can be used to write multiline strings, which are, of course, very extra:
- If the first character is a newline, it is ignored.
- The indentation of the _closing_ quotes determines the indentation of every line
- The indentation is _required_ on every line (except blank lines)
- Line continuation is supported with a `\` at the end of the line (meaning the newline is ignored)
- The trailing newline is _preserved_. Remove it with `\`
````extra
let
something-cool: '''
this is a String,
right?
''' --> "this is a String,\nright?\n"
--^^^^^^^^^^ this indicates the indentation, because of the closing quotes
in …
let
something-cool: '''
remove-trailing-newline-\
from-all-lines\
''' --> "remove-trailing-newline-from-all-lines"
in …
str = '''
test1
test2
'''
-- this can also be written:
'''test1
test2
''' --> test1\n
test2\n
-- And because of the indent rule, this is also the same String:
'''test1
test2
''' --> test1\n
test2
-- continuation in the middle joins lines (newline is removed)
'''
hello \
world\
''' --> "hello world"
"""
multiline
strings
are
neat.\
""" -- no trailing newline
-- and of course backticks, with interpolation
```
use
${backticks}
if
you
prefer
```
````
All strings use backslash to escape special characters:
```extra
\n --> newline (\x0A)
\t --> tab (\x09)
\0 --> NUL/␀ (\x00)
\e --> ESC/␛ (\x1b)
\xNN --> 2 digit hex char
\uNNNN --> 4 digit hex char
-- are these characters really relevant? who uses _vertical tab_!?
\r --> silly char (\x0D)
\v --> vertical tab (\x0B)
\f --> form feed (\x0C)
\b --> backspace (\x08)
-- All other backslash+char combinations return the char, even if the character
-- doesn't have any special signifigance.
-- eg
\\ --> \
\' --> '
\` --> `
\) --> )
\$ --> $
```
### Regular Expressions / Regex
```extra
/\b(regular expressions)\b/g <-- classic perl style regex
/\b(\$\)\b/g
/[abc]/g --> global flag
/[abc]/i --> case-insensitive
/[abc]/m --> multiline match
/[abc]/s --> dot-all match
/\b\d+\D\s/ --> the usual regex features.
```
Extra runs within the JS runtime, and the regular expressions are passed directly to the `RegExp` constructor. The [Mozilla Regex cheat sheet](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet) has lots of good information about what's supported. Say what you will about JS's terrible API (thank you, I will!), the Regular Expressions support is very good.
### Container Types: Array, Dict, Set, and Object
Arrays and Objects are created using the common `[]` and `{}` symbols. Dicts (aka Map in JavaScript) are created using `dict(key: value)` and Sets are created using `set(value)` (`type` is optional in both cases, it is usually inferred). I am seriously considering adding a sigil for dict and set, if I do it will be `#{}` for dict and `#[]` for set. These are already supported as "optional" syntaxes, but the code formatter will rewrite them to `dict/set`.
Keys in Objects and Dicts can be strings, numbers, `null`, `true`, or `false` (i.e. any primitive value).
Objects play double duty as the Tuple type, because they can have positional properties as well as named. Up to you what you do with this ability to mix and match.
```extra
type User = {
String -- positional
age: Int(>=0) -- named
}
type Point = {Float, Float}
-- positional arguments work well when the order is obvious. there is nothing here
-- to indicate that user.0 refers to a 'name'.
user: User = {'Chuck', age: 50}
-- Point, on the other hand, is pretty obvious that this is {x,y}
pt: Point = {0, 0}
```
The container types can be split into two families:
- Homogenous
- All items must have the same type (array, dict, set)
- Heterogenous
- All items may have different types (object)
#### Homogenous types: Array, Dict, Set
Homogenous types have only one type, even if that type is an `optional` or `oneOf` type.
Array: a list of homogenous items, indexed by number.
Dict: a lookup/map/hashmap of homogenous items.
Set: an unordered collection of homogenous items. Only one of each item will be included in the set (according to deep equality checks).
Syntax:
- Array: `[] [] [1] [1,] [1, 2, 3]`
(alternatively you can use the "long form" `Array(1,2,3)`)
- Dict: `#{} #{key: 1} #{key: 1,} #{1: 1, 'key2': 2, "key$three": 3}`
(alternative longhand: `Dict(key: value)`)
- Set: `#[] #[1] #[1,] #[1, 2, 3]`
(alternative longhand: `Set(1, 2, 3)`)
#### Heterogenous types: Tuple, Object
Object: a lookup/map/hashmap of different properties. Each key can have a different type.
Tuple: same as an object, but indexed by number instead of string. Tuples and Objects are just one type that supports _both_ string and numeric keys.
Syntax:
- By (string) key: `{} {one: 1} {one: 1,} {1: 1, 'two': "two", "$three": [3]}`
- By index: `{} {1} {1,} {1,"two",[3]}`
- Mix: `{one: 1, 0, 1, zero: 0}`
#### More examples
```extra
-- Arrays
[1, 2, 3] --> [Int] (aka Array(Int)) with three entries
[] --> empty array ([Always])
["one", "two", ] --> [String] with two entries (trailing comma is ok)
-- Dicts
Dict(one: 1, two: 2, three: 3) --> Dict(Int) with three entries
Dict() --> empty dict (Dict(Always))
Dict(number1: "one", number-two: "two", ) --> Dict(String) with two entries
❌ {} --> empty object, not a dict!
-- Sets
Set(1, 2) --> Set(Int) (Set of Ints)
Set() --> empty set
-- Objects
{ age: 1, name: 'foo' } --> {age: Int, name: String}
{} --> empty object or tuple
-- Tuples are just objects with numeric keys
{1, "two", [3,4,5]} --> {Int, String, [Int]} (3-tuple of Int, String, Int array)
{} --> empty object or tuple
-- There is no actual Tuple type, objects support number keys, and they can be
-- mixed and matched.
{0, 1, last: 10} -- {Int, Int, last: Int}
```
Objects and Tuples can contain values with different types (this is called a **Product Type**). What happens if you put different types into an array or dict?
```extra
[1, 2, "3"] -- Invalid!? Nope! This has the type `[Int | String]`
-- (**ahem**, actually it has the type `[1 | 2 | "3"]`, because Extra assigns
-- the literal type to the corresponding literal value)
```
Enter the **OneOf** type.
### OneOf
OneOf types represent a value that could be one type or another (or three or however-many types).
The most common is called the _optional_ type, which is any type `T` or the `null` value. But you may also need to store a value that is _either_ of type `Int` _or_ a `String`.
OneOf types can be expressed in general as `type1 | type2 | ...`, e.g. `Int | String` or `[String] | null`. The optional type has a shorthand `Int? --> Int | null`.
```extra
-- literal values narrow down to the corresponding literal type
[ 1, 2, null] --> [1 | 2 | null] aka [(1 | 2)?]
let
a: Int = 1
b: Int? = ...
c: String = ...
in
[ a, b, c] --> [Int | String | null]
```
The only problem with _oneOf_ types is that you cannot call methods or properties on them, unless the method is shared between both types. You can get around this limitation using _type guards_ (or other type assertions).
### Literal types
So far we've been expressing numbers and strings using their types, but _literal_ types are also supported. For instance, the expression:
```extra
1 + 2
```
Is parsed as `literal(1) + literal(2)`, and resolved to the type `literal(3)`. You can express enumerations this way, too:
```extra
size: 'small' | 'medium' | 'large' --> size must be one of these strings, no others.
```
### Type definitions
We've seen many definitions already.
- `null` `true` `false` some literal value types
- `1` `1_000` `'text'` also literal types
- `Boolean` `Int` `Float` `String` the basic types
- `Boolean | Int` one of types
- `[Int]` `[Int | String]` `[Float?]` arrays
- `Dict(Int)` `Dict(Int | String)` `Dict(Float?)` dicts
- `{Int, String}` `{Int?, String?}` tuples
- `{foo: Int, bar: String}` `{foo: Int?, bar: String?}` objects
- `[Boolean] | [Int | String]` one of types mixed with container types
## Let
`let` is how you can assign values to local ~variables~ scope.
```extra
let
x = 1
y = 2
in
x + y
let
the-answer = 42
fn propose-answer(answer: Int): String => `The answer is $answer`
in
propose-answer(answer: the-answer)
```
Why not be a little _Extra_!? Useful when the return value is more interesting than the values.
```extra
in
{
x:
y:
sum: x + y
}
let
x = 1
y = 2
```
## Variable names
References can have hyphens like in Lisp (`valid-variable-name`), and emojis (`😎-languages = Set("extra")`).
## Functions
Extra's functions are bonkers. They support _positional_ and _named_ arguments, along with all sorts of variadic arguments, and
Positional arguments have a `#` prefix, `# like: This`. Named arguments `do: Not`. Named arguments can be aliased `like so: GotIt?`. Variadic arguments `...# are: LikeThis` or `...like: This`. Keyword args are `**like: This`.
Examples:
```extra
fn doEeet(# count: Int, # name: String = '', age: Int = 0, reason why: String) => …fn body…
-- '#count' (first positional argument) is required
-- '#name' (second positional argument) is optional (default value provided)
-- 'age' is optional, and is a named argument
-- 'reason' is required,
-- but the fn body uses the name "why"
doEeet(1, reason: '') -- name = '', age = 0
doEeet(1, 'foo', reason: '') -- name = 'foo', age = 0
doEeet(1, 'foo', reason: '', age: 42) -- name = 'foo', age = 42
❌ doEeet(reason: '') -- count is required
❌ doEeet(1) -- reason is required
```
If the argument type is null-able, you can make the argument optional `like?: This` (`like: This | null`). If the argument is _generic_, it will be made optional only if the type is null-able. In other words:
```extra
fn first-or(# array: [T], else fallback?: T) =>
if array
array[0]
else
fallback
let
a: [Int] = […]
b: [Int?] = […]
in
first-or(a, else: 1) --> else is required because type `Int` is not nullable
first-or(b, else: 1) --> still fine here, but...
first-or(b) --> else is optional (defaults to `null`) because `Int?` aka `Int | null` is nullable
```
Confusing! Sorry, it is, but I also think it is useful.
### Inferred types
The return type of a function can always be inferred (even recursive functions). Argument types are required when you are defining a function, but if you are calling a function that expects a function, like `map`, `reduce`, `sort`, you can use formula shorthand. The receiving function already defined the callback type, so the shorthand can infer its argument and return types.
```extra
-- map already defined its callback, so the argument types can be inferred (even
-- if 'map' is generic)
[1, 2, 3].map(|num| num + 1) --> [2, 3, 4]
```
In the example above, `num` is a named argument, but `map` expects a function that accepts two positional arguments `# value: T, index: Int`. Since the first named argument is compatible with `# value: Int`, and the second argument is ignored, the compiler figures out what to do.
### Variadic Arguments
There are _three_ brands of variadic arguments. I was fed up with all the Python coders boasting endlessly about args and kwargs, so I invented 'rargs'.
- variadic positional arguments - must be an `Array` type, and there can only be one.
- keyword argument list - must be a `Dict` type, and there can only be one
- repeated named arguments - must be an `Array` type, and there can be multiple
#### Variadic Positional Arguments
You can accept any number of positional arguments using an argument defined as `...# name: Type`
```extra
fn add(...# numbers: [Int]) =>
numbers.reduce(0, |memo, num| memo + num)
add() --> 0
add(1) --> 1
add(1, 10) --> 11
add(1, 10, 31) --> 42
let
numbers = [1, 10, 31]
in
add(...numbers)
```
#### Keyword Argument List
Any named arguments that are not otherwise declared can be put into a "keyword arguments list", `**remaining: Dict(String, T)`.
```extra
fn list-people(greeting: String = 'Hi,', **people: Dict(String)) =>
people.map(|name, honorific|
`$greeting $honorific: $name`).join(' - ')
list-people(greeting: 'Hello,', Jane: 'Doctor', Emily: 'Miss')
--> "Hello, Doctor Jane - Hello, Miss Emily"
```
If you have a `Dict` of values that you want to use as the keyword arguments, you can assign it via `**name`. This will always assign to the `kwargs` argument, even if the `Dict` contains keys that are also argument names. You can assign multiple `Dict`-s in this way, they will all go into the same `kwargs`.
```extra
let
people = Dict(Jane: 'Doctor', Emily: 'Miss', greeting: 'example')
in
list-people(**people)
--> "Hi, Doctor Jane - Hi, Miss Emily - example greeting"
```
#### Repeated Named Arguments
You can specify the same argument by name, multiple times. `...name: Type`
```extra
fn returnIf(# condition: Boolean, ...and: [Boolean], then: T): T?
returnIf(a == 1, and: b == 1, and: c == 2, then: 'yay!') --> 'yay!' | null
```
#### Proposal: Function overrides
_Warning_: I haven't implemented this - I'm, just considering this.
You can define separate function implementations if you want to have lots of different signatures all wrapped up in one function name. The Extra compiler will verify that the implementations are unambiguous. The functions have to be distinguishable by their argument names and arity (number of required positional arguments).
```extra
fn add {
fn(# a: Int) => a
fn(# a: Int, # b: Int) => a + b
❌ -- same number of arguments, can't be distinguished
❌ fn(# a: String, # b: String) => a .. b
fn (str: String, # b: String) => str .. b
-- still distinguishable, because it only accepts one argument, and the name
-- is different from `fn(# a: Int)`
fn (str: String) => str
}
add(1, 2) --> 3
add(str: 'a', 'b') --> ab
add(str: 'a') --> aa
add([1]) --> ❌
add(...[1]) --> 1
```
## `if`
As in all functional programming languages, `if` is an expression that returns the value of the branch that was executed. The `else` branch is optional. If the `else` value is not provided, `null` is returned.
```extra
if test1 or test2
result_1
--> result_1 | null
if test1 or test2
result_1
else
result_2
--> result_1 | result_2
```
Multiple `else if` branches can be provided:
```extra
if test1 or test2
result_1
else if test2
result_2
else
result_3
--> result_1 | result_2 | result_3
```
There is also an "inline" version using the `then` keyword. This is an indication to the code formatter that you would prefer to keep this condition on one line; however, if the line is too long, the formatter will automatically break it into multiple lines anyway because who do you think is boss here?
```extra
-- nice and tidy
if test1 then result_1 else result_2
-- no way, this will be reformatted
if test1 or test2 then result-1 else if test2 then result_2 else result-4
-->
if test1 or test2
result-1
else if test2
result_2
else
result-4
```
## `guard`
Guard expressions are useful in any language, but the `guard` syntax in Swift was one of my favourite language features, and so I'm unapologetically stealing it.
```extra
fn(# name: String?, hobbies: [String]): String =>
guard
name != null
else
''
guard
hobbies.length > 0
else
name .. ' is not very interesting'
name .. ': ' .. hobbies.join(', ')
```
Guard also has an inline version. As much as I love the `guard` expression in general, the inline version is a bit of a mouthful, and I don't recommend using it.
```extra
guard name != null else '' then name
```
## Operators
### Basic Math
```extra
1 + 2 --> 3 Addition
15 - 2 --> 13 Subtraction
8 * 2 --> 16 Multiplication
10 / 5 --> 2 Division
10 / 6 --> 1.6… Division returns a Float *even if* you provide two Ints, see // below
2 ** 8 --> 256 Power/exponent
```
### CompSci Math
```extra
-- Integer/floor division removes the floating point "remainder" by flooring the
-- result. When dividing negative numbers, it always rounds down (not towards
-- zero).
15 // 2 --> 7
-10 // 3 --> -4
10 % 3 --> 1 Modulo / Remainder, also works with floats
-- Binary Operators
0b100 | 0b001 --> 0b101 (5)
0b110 & 0b010 --> 0b010 (2)
0b110 ^ 0b010 --> 0b100 (4)
~0b11010110 --> -215
-- negate with a bitmask:
~0b11010111 & 0b11111111 --> 0b00101000 (40)
```
### Comparison
```extra
a > b
a >= b
a < b
a <= b
a == b --> does a deep comparison of objects/arrays/dicts/etc
a != b
a <=> b --> the sort operator compares two strings or two numbers, and returns -1, 0, or 1
```
### Logical Operators
Logical operators "short circuit", e.g. they return values without converting them to a Boolean.
```extra
a or b --> Logical Or, returns `a` if a is true, otherwise returns b
a and b --> Logical And, returns `b` if a is true, otherwise returns a
-- Examples
a = 5
b = 0
c = 1
a and c --> 1 (returns c, because a was true)
a or c --> 5 (returns a, because a was true)
b and a --> 0 (returns b, because b was false)
b or a --> 5 (returns a, because b was false)
```
Btw, if you think of `and` as "multiplication" (if either is 0/false, result is
0/false) and `or` as "addition" (if either is 1/true, result is 1/true) you'll
have an easier time remembering the order of operations (`and` first, then `or`)
### Regex Match Operator
Funny story... when I implemented the `matches` operator, I realized that `x matches Foo` (where `Foo` is a class or other type) could only reasonably mean that `x is Foo`. Well wait a second, if `is` can be used there, could I also use it in other match contexts? Yes! I had already moved regex matches into the same bucket as generic
```extra
"test String" is /[test]/ --> Boolean
```
### Null Coalescing Operator
Included only because of its cool name. 😎
```extra
a ?? b --> returns `b` if a is null, otherwise returns `a`
```
### Other Null Safe Operators
```extra
user.address?.street -- null-safe property access
items?.[0] -- null safe array access
user.format?.(address) -- null safe function invocation
```
### String Concatenation
I've never liked `+` as String/Array concatenation. `+` should be communative, because maths.
```extra
"aaa" .. "BBB" --> "aaaBBB"
$12345 .. 'dollars' --> "12345 dollars"
`${12345} dollars` --> "12345 dollars"
```
### Array Concatenation
Sure I could've implemented the `..` operator in a way that supported Strings
_and Arrays_, why not have two operators so that the _intention_ was that much
clearer? So that's what I did. `++` for Arrays.
```extra
[1,2,3] ++ [4,5,6]
```
### Object and Dict Merging
Last but not least, you can merge two objects or dicts with `~~`, and in this
case the values on the left-hand-side will be replaced with the values on the
right-hand-side if they have the same keys.
**Dict Example**
```extra
let
old_users = Dict(a: …, b: …)
new_users = Dict(b: …, c: …)
in
old_users ~~ new_users
-- returns Dict(a: …, b: …, c: …), with 'b' coming from new_users
```
**Object Example**
```extra
let
user = {name: 'Alice', age: 50}
updates = {age: 51}
in
user ~~ updates
```
Since Objects are _also Tuples_ I had to make a decision on how to merge
positional arguments. Should they override in numeric order (spoiler: yes they
do) or should they _concatenate_ (they don't)...
```extra
let
weather = {50, unit: 'celsius'}
new_temp = 60
in
weather ~~ {new_temp}
-- option A: {60, unit: 'celsius'}
-- option B: {50, unit: 'celsius', 60}
```
I went with option A. I'm relieved to hear that you agree with this decision.
### Splat operator `...`
All of the container types (Array, Tuple, Object, Dict, and Set) support the `...` unary operator to merge multiple arrays/tuples/dicts/sets into one. Some containers can be mixed and matched, others can't. Try 'em and find out!
```extra
-- Arrays
let
a = [1, 2, 3]
b = [4, 5, 6]
in
[...a, ...b] --> [1, 2, 3, 4, 5, 6]
-- a ++ b --> same
-- Sets
let
a = Set(1, 2, 3)
b = Set(3, 4, 5)
in
Set(...a, ...b) --> Set(1, 2, 3, 4, 5)
-- a ++ b --> same
-- Dicts
let
a = Dict(a: 1, b: 2, c: 3)
b = Dict(d: 4, e: 5, f: 6)
in
Dict(...a, ...b) --> Dict(a: 1, b: "2", c: 3, d: 4, e: "5", f: 6)
-- a ~~ b --> same
-- Dict + Object
let
a = Dict(a: 1, b: 2, c: 3)
b = {d: 4, e: 5, f: 6, 'foo'}
in
Dict(...a, ...b) --> Dict(a: 1, b: "2", c: 3, d: 4, e: "5", f: 6, 0: 'foo')
-- a ~~ b --> same
```
In an object type, the `...` operator works much like the `~~` operator, but consider merging multiple tuple objects (objects using positional values):
```extra
let
a = {1, "2"}
b = {3, "4"}
in
{
">"
...a
"|"
...b
"<"
} --> ?
```
I hope it's clear from this example that the positional values all need to concat. This is very _unlike_ merging two `Dict`s together, where the key semantic is very explicit.
```extra
...
} --> {">", 1, "2", "|", 3, "4", "<"}
```
### Putting it all together
I want to take a moment to point something out - there are always two ways to
merge/join/concat. You can start with the "container" and put in the parts you
want, or you can start with one container and join others onto it. I'll show you
what I mean:
#### String
1. String interpolation: `"${name} is $age years old"`
2. String concatenation: `name .. ' is ' .. $age .. ' years old'`
#### Array
1. Splat: `[...list1, ...list2]`
2. Concatenation: `list1 ++ list2`
#### Dict
1. Splat: `Dict(...dict1, ...dict2)`
2. Merge: `dict1 ~~ dict2`
(`dict2` overrides keys in `dict1` in both cases)
#### Set
1. Splat: `Set(...set1, ...set2)`
2. Union: `set1 + set2`
#### Tuple/Object
1. Splat: `{...obj1, ...obj2}`
2. Merge: `obj1 ~~ obj2`
I think this is a nice symmetry, and also the operators indicate (somewhat) the type that is being operated on.
### Array/Dict/Tuple/Object Access / Property Access
Property access looks like you'd expect `object.property`, and works on objects and dicts. `[]` works on all container types (object, dict, tuple, array), and accepts expressions (e.g. `object["foo"] --> object.foo` or `array[1 + 1] --> array[2]`). Tuples should use property access `tuple.0` but you _can_ use an array index if you're careful.
```extra
-- tuple: {Int, String, name: String}
tuple[0] == tuple.0 -- indexing with an int literal is fine
--> Int
-- x: Int
tuple[x] --> ❌
-- x: 0 | 1
tuple[x] --> oneOf(tuple.0, tuple.1) --> Int | String
```
An important difference with property access and array access is that property access will prefer built-in properties, whereas array access will always search for the value in the table. For example, Dict defines `map` and `mapEntries` methods, and so `dict.map` will call that function. But `dict["map"]` will ignore the built-in function and instead search for an entry named `map` and return that. It will never return the built-in 'map' function.
Yes I tried to make Extra familiar to JavaScript devs, but no I'm not going to copy parts that would clutter the language with ambiguities.
If the property access isn't a build-in, it will search for that property in the Dict/Object. So `things.foo == things['foo']`. These will return `T | null` unless the key is known to be in the dict/object:
```extra
let
ages: Dict(Int) = #{alice: 50, bob: 46}
in
ages['alice'] --> returns 50
ages.bob --> returns 46
ages.map --> `map` function, which iterates over the values
ages['map'] --> returns null
-- there is also a "null safe" property access operator
-- ie if `person` could be null:
person?.address.street then 'default address'
--> returns person.address.street if person is defined, otherwise returns 'default address' due to null coalescing operator
```
### Pipe Operator
Everyone's favourite! Well it's _my_ favourite, and if you haven't used it today's your day. It's more likely that you've used chained methods – the pipe operator is a natural companion, but in cases where a chained method isn't an option. Here's an example that surrounds a stringified array with `"[]"` characters, _and_ adds a trailing comma if the array wasn't empty.
```extra
[1,2,3].filter(|i| i < 3).join(',')
|>
if #pipe.length
-- recall that $ is the 'to string' operator
$#pipe .. ','
else
''
|>
`[$#pipe]` --> `"[1,2,3,]"`
```
There's also a null-safe version:
```extra
-- name is String | null
name ?|> #pipe .. ':' --> the pipe `#pipe` is guaranteed to be a `String`, otherwise the expression is skipped and `null` is returned.
```
[^1]: JSX
What!? Should I call it something else just because it _is_ something else? Bah. It walks like a duck and quacks like a duck, so I'm calling it JSX.
Similarities:
- Within a text node, `{…}` encloses an expression that is inserted as a child.
```extra
Name: {@user.name}
Item 1: {if (foo) { then: , else: }}
```
The differences from React JSX:
- attributes can receive extra values, so `` assigns the variable `bar` to `prop`
There are limitations to this, though: you cannot use most binary operators, only 'access' operators like `.` and `[]`. You can always enclose operations in `()`.
`` is invalid.
`` is correct.
`{}` is, like everywhere else in Extra, for creating objects.
```extra
```
- shorthand for boolean `isSomething` has corresponding `!isSomething` shorthand.
```extra
-- In React-JSX, boolean properties are either "bare" (`isNifty` in this example), or given the values `true|false`.
-- In Extra-JSX you can use `isNifty` like in JSX, or negate a property using `!isTerrible`
-- and, since expressions are supported, you don't enclose `true|false` in curly braces.
```