A language that makes building webapps as easy as finite-state machines
MIT License
A language that makes building webapps (and more!) as easy as building finite-state machines.
(Comparisons with: Elm, Flux, Redux, or Functional Core, Imperative Shell)
Status: First compile and run of Hello World works—and absolutely nothing else. Help wanted!
Okay.
State counter = 0
// Declarative JSX-like view syntax
%View '#app':
<div>
<p>Counter: {counter}</p>
<button @increment>+1</button>
</div>
// Event handlers imperatively update state
When increment.Click:
Change counter to (Current counter + 1)
That compiles to this fast, mutative (yet clean & readable) JavaScript:
var counter = 0
const p = <p>Counter: {counter}</p>
const increment_button = <button>+1</button>
render_view('#app',
<div>
{p}
{increment_button}
</div>
)
const increment = {
Click: event_stream(increment_button, 'click'),
}
increment.Click.subscribe(() => {
counter += 1
p.textContent = 'Counter: ' + counter
// ^ direct DOM manipulation, faster than
// virtual DOM diffing like React
})
(okay, that's theoretical, that doesn't actually work yet)
Install:
npm install mechanical
Run the compiler:
npx mechc some_file.mech
Which will output: some_file.js
Mechanical-specific extensions to JS expression syntax (which only make sense with Mechanical semantics, and wouldn't make sense for JS:
(TODO) cmd:
blocks
(TODO) Hashtagged values #tag value
and pattern-matching match tagged {...}
Let x = Current switch_state ? #on 72 : #off "sleeping for the night"
// expression-form pattern-matching:
Let y = match x { #on temp -> temp; #off message -> 68 }
// statement-form pattern-matching:
Match x:
#on temp ->
Change color to #green
Change temperature_dial to temp
#off message ->
Change display_message to message.slice(from: 0, to: 100)
(TODO) is
and isnt
pattern-matching operators
matches
makes more sense than is
, but what's the inverse?doesnt_match
?(TODO) Macros: the %
prefix, like %View
and %map{}
, is eventually
intended to connote macros aka userland syntax extensions, although for now
they'll be hardcoded into the compiler
Extensions to JavaScript features:
Multiline strings
Both string literals (""
and ''
) can have newlines in them, but
subsequent lines must be indented to at least the same level as the
open-quote; that indentation is then omitted from the result:
When button.Click:
Let valid_string = "first line
second line
third line" // == "first line\nsecondline\n third line"
When button2.Click:
Let invalid_string = "first line
second line" // error!
(TODO) Alternate string interpolation syntax $`text {expression} text`
In addition to JS template literal syntax (e.g.
`text ${expression} text`
), Mechanical supports an alternative
string interpolation syntax where you prefix the string with $
:
$`text {expression} text`
$'text {expression} text'
$"text {expression} text"
// are all equivalent to:
`text ${expression} text`
This is similar to C# but without support for format specifiers.
postfix/infix function calls, named parameters
// there are no 0-argument functions (functions can't have side-effects,
// so what would be the point?)
// 1-argument functions can be called prefix or postfix.
// These are exactly equivalent:
foo(x)
x.foo()
// 2-argument functions can be called prefix or infix.
// These are exactly equivalent:
bar(x, y)
x.bar(y)
// for 3-or-more-argument functions, labels are required for parameters
// after the first parameter.
// They can also be called prefix or infix, so these are equivalent:
qux(x, param: y, another: z)
x.qux(param: y, another: z)
// e.g.
list.reduce(from: initial_value, update: (so_far, next) => ...)
(TODO) Tilde functions like ~ + 1
, even lighter-weight than arrow functions:
// I think of the ~ as like a scribble and read it as "thing"
// so I read (~ + 1) as "thing plus one"
[1, 2, 3].map(~ + 1) == [2, 3, 4]
[1, 2, 3].map(2*~) == [2, 4, 6]
// basically, the tilde function expands "outward" to encompass built-in
// ops (arithmetic, string templating, boolean), and expands "rightward"
// to encompass .prop property accesses and .foo() postfix and infix
// function calls
// basic expressions:
~ + 1 // equivalent to x => x + 1
-1/(2*~) // equivalent to x => -1/(2*x)
`Hello, ${~}!` // equivalent to x => `Hello, ${x}!`
~ < 0 || ~ > 100 // equivalent to x => x < 0 || x > 100
~.prop // equivalent to x => x.prop
~.x**2 + ~.y**2 // equivalent to p => p.x**2 + p.y**2
#tag ~ // equivalent to x => #tag x
~.#tag() // equivalent to x => #tag x
[~] // equivalent to x => [x]
{ prop: ~ } // equivalent to prop => ({ prop })
// function calls:
foo(~, 1) // equivalent to x => foo(x, 1)
~.foo(1) // equivalent to x => foo(x, 1)
foo(1, a: ~, b: 2) // equivalent to x => foo(1, a: x, b: 2)
1/sqrt(~) // equivalent to x => 1/sqrt(x)
Do foo(~) // equivalent to x => Do foo(x)
(2*~).foo() // equivalent to x => foo(2*x)
~.foo().prop // equivalent to x => foo(x).prop
~.foo().Do! // equivalent to x => Do foo(x)
~.foo().bar().qux() // equivalent to x => qux(bar(foo(x)))
// 2-argument tilde function:
~ + ~~ // equivalent to (x, y) => x + y
~~**~ // equivalent to (e, b) => b**e
foo(~~, ~) // equivalent to (x, y) => foo(y, x)
// !!! Special Exception: rearranging arguments !!!
x.foo(1, a: ~, b: 2) // equivalent to foo(1, a: x, b: 2)
// [Note] ^ not an anonymous function
~.foo(1, a: ~, b: 2) // equivalent to x => foo(1, a: x, b: 2)
// invalid:
foo(~) // redundant, just use foo
~.foo() // redundant, just use foo
foo(~, ~~) // redundant, just use foo
~.foo(~~) // redundant, just use foo
x.foo(~) // ambiguous: foo(x) or y => foo(x, y) ?
// Just use foo(x, ~)
x.foo(a: 1, b: ~) // instead use foo(x, a: 1, b: ~)
#tag foo(1, ~) // ambiguous: #tag (x => foo(1, x))
// or x => #tag foo(1, x) ?
// instead use ~.foo(1, ~).#tag()
[ foo(1, ~) ] // ambiguous: [ (x => foo(1, x) ]
// or x => [ foo(1, x) ] ?
{ prop: foo(1, ~) } // ambiguous: { prop: (x => foo(1, x)) }
// or x => ({ prop: foo(1, x) }) ?
// potential pitfalls:
sqrt(~.x**2 + ~.y**2) // equivalent to: sqrt(p => p.x**2 + p.y**2),
// instead use: (~.x**2 + ~.y**2).sqrt()
foo(bar(1, ~)) // equivalent to: foo(x => bar(1, x)),
// instead use: ~.bar(1, ~).foo()
foo(bar(~)) // invalid
// instead use: ~.bar().foo()
foo(~).prop // invalid
// instead use: ~.foo().prop
#tag foo(~) // invalid
// instead use: ~.foo().#tag()
This is similar to the JS Partial Application proposal, the magic Scala _
underscore, or the Tulip autovar. I originally wanted to use _
just like
Scala (_ + 1
reads so nicely as "blank plus one"), but then I realized that
_ is #tag _
(equivalent to x => x is #tag _
) looks weird, since the _
means two different things in two different places. _
has to be the
pattern-matching wildcard because it should match destructuring, and _
is
universally the destructuring wildcard.
TODO: open a ticket to bikeshed syntax
I briefly considered whether this could be type-dependent, like:
// Consider:
Let foo = (x, y) => x + y
Let bar = x => 2*x
Let qux = f => cmd: Do console_log(f(100))
// That means:
// foo: (number, number) => number
// bar: (number) => number
// qux: ((number) => number) => VoidCmd
// But when parsing:
qux(bar(foo(1, ~)))
// We get the equivalent of:
qux(bar(x => foo(1, x)))
// which is a type error (bar() takes in a number).
// Theoretically we could notice that qux() takes in a unary function,
// and instead parse it as:
qux(x => bar(foo(1, x)))
// But...that's crazy. Like, for starters, it's crazy to have parsing
// dependent on whole-program type inference.
// More concretely, if the type of `bar()` were to change drastically
// to take in a function instead of a number (such as if `bar` were
// shadowed by the name for some totally different function), that could
// change the parsing, without this file having changed at all.
// The correct error message that should result is that hey, you call
// `bar()` and give it a number and that used to be correct, but now it
// takes a function which is totally different.
// But instead, because the parsing changed, the resulting error message
// would potentially be that the return type of `bar()` doesn't match
// the parameter type of `qux()`, even if neither of those changed at all.
The expression syntax is based on JavaScript's, with a few deliberate incompatibilities in edge cases that I think JavaScript syntax is confusing:
Identifiers may only have single, interstitial underscores
this_is_totally_valid
_foo
, foo__bar
, foo_
, $foo
foo__bar
?)Exponentiation isn't chainable
2**3**2
be (2**3)**2 = 64
, or 2**(3**2) = 512
? JS and Python512
, but I think that's confusing because it's the other way2/3/4 = (2/3)/4
.2**3**2
is a syntax error-2**2
is also a syntax error. This actually differs from-2**2 = -(2**2) = -4
, but that's confusing because it(-2)**2 = 4
)Comparisons are chainable
3 > 2 > 1
is false, which is confusing. In Mechanical, not onlya > b > c
behave as expected, but so does a > b >= c == d
, ora == b < c == d < e == f
. This feature is inspired by Python, howevera < b > c
are prohibited (they have!=
can't be chained at all.!=
can't be chained because should 1 != 2 != 1
be true or false?)No holes in arrays, but trailing commas are allowed, and you can use the
null value #none
// valid:
[ 1, 2 ]
[ 1, 2, ]
[
1,
2,
]
[ 1, #none, 2 ]
[ 1, 2, #none, ]
// invalid:
[ 1, , 2 ]
[ 1, 2,, ]
Mechanical records are much more restricted than JS objects. Field names must be valid identifiers, not arbitrary strings or numerals, and cannot be quoted. They also cannot be computed, cannot be [method definitions], and there are no getters or setters. Trailing commas are allowed though!
// valid
{ valid_identifier: 1 }
// invalid
{
invalid__ident: 1,
_invalid: 2,
$invalid: 3,
"not an identifier at all": 4,
5: 6,
[compute(7, 8)]: 9,
method() {
Return 10
},
}
Only arrow function expressions (e.g. x => 2*x
) are supported (there's
no function
keyword), functions must take at least one argument (all
functions are pure, so what would a no-argument function do?), and trailing
commas aren't allowed
(TODO: method-calling syntax/UFCS, named args when >2 params)
// valid
x => 2*x
(x, y) => sqrt(x**2 + y**2)
// invalid
() => 1
(x, y,) => sqrt(x**2 + y**2)
function (x) { return 2*x }
No bitwise operators
~
, &
, |
, <<
, >>
, >>>
built-in, but I hope toJS-specific things that don't make sense with Mechanical semantics:
===
/!==
. Regular in/equality ==
/!=
is++
/--
in
or instanceof
relations=
, +=
, -=
, *=
, /=
, etc(TODO: the previous section and this section are a bit too much of a brain-dump, and need to be entirely reorganized for readability)
[1, 2, 3]
or['one', 'two', 'three']
, aka monomorphic arrays) are exactly like in JS[1, 'two', 3]
)[#year 1, #name 'two', #year 3]
.mixed
in the array declaration.#none
(more on hashtags below).#none
and #error _
types (#error
T | #none
is the equivalent of other language's Option or Maybe type,T | #error E
is the equivalent of other language's Result or EitherT | #none | #error E
and T | #error E | #none
are the sameResult<Option<T>, E>
or Option<Result<T, E>>
, and there are evenfunctions' and commands' return values have to be a single type (monomorphized), like arrays. Like arrays, if a function does sometimes return a string and other times a number, just use hashtags:
// valid:
(x) => (x > 100 ? #msg "a string" : #count x/100)
// invalid:
(x) => (x > 100 ? "a string": x/100)
other than that, pure functions are pure functions, shouldn't have
surprises coming from JS
commands are values that can be thought of two ways:
in Mechanical, all functions are pure functions, and cannot have
side-effects. But they can return a command value, and you can Do
a
command to perform side-effects:
When button.Click:
Do foo(123)
Let c = cmd:
Do bar('thing')
Do baz('another thing')
Do c
foo = (x) => cmd:
Do qux(x + 1)
As you can see, When
-handlers can Do
commands, and a cmd:
block can
define a new command in terms of Do
-ing commands. That's it, there are
no other ways to Do
a command, but functions like foo()
can return a
cmd:
block, allowing composition of side-effectful commands. Note that
foo()
is still a pure function, one that takes in a number and returns
a command value.
Algebraic effects introduce elements of imperative programming that I greatly dislike: control flow and pervasive implicit ordering.
foo(bar(), baz())
, the order of evaluation is implicitlybar()
, then baz()
, then foo()
. In purely functionalIO
monad; by contrast, in Mechanical allAlgebraic effects are also more powerful than necessary, letting you do parlor tricks like evaluating one computation along multiple branches by feeding it multiple possible return values for an effect. I can't think of much practical use for this power, but I can see the practical downsides it has for performance (the continuation must be allocated or copied to the heap, rather than just temporarily on the stack).
[(Heads, Heads), (Heads, Tails), (Tails, Heads), (Tails, Tails)]
IO
actions in a do
-block, the resultingDo
statementsAfter Got
statementsthere are 4 types of command values: pure, sync, async, and void
void commands are commands with no Return
statement and therefore
don't return anything. They can Do
any other type of command.
asynchronous commands either have their Return
statement after an
After Got
statement, or return the result of Do
-ing an async
command. They can Do
any other type of command.
// examples:
cmd:
Future x = Do async_thing
~ After Got x ~
Return 2*x
cmd:
Do something
Return Do async_thing
cmd: Do async_thing
synchronous commands cannot have After Got
statements, and can
only Do
an async command if they ignore its return value.
// examples:
cmd:
Get x = Do sync_thing
Change state.x to x
Return x + 1
cmd:
Do async_thing
Do sync_thing
Return Do another_sync_thing
cmd: Do sync_thing
pure commands have no side-effects, can only Do
other pure commands,
and exist primarily for testing. The built-in function eval()
can
evaluate them and return the resulting value
examples: cmd: 0
, cmd: 1+1
, cmd: Do some_pure_cmd
,
cmd:
Let x = 1+1
Return x + 3
(no relation JavaScript's eval()
. Coincidentally, eval()
is
also special, but in a very different way: it's entirely erased
by the compiler, because a zero-argument pure function serves
no purpose at runtime)
eval()
,cmd: 0
and 0
are exactly equivalent?non-void commands (pure, sync, async) also have a return type and are
covariant in their return type, so a pure command returning #cat
is a subtype of a pure command returning #cat | #dog
. They also form
a hierarchy: PureCmd<#cat>
is assignable to SyncCmd<#cat>
which is
assignable to AsyncCmd<#cat>
which is assignable to VoidCmd
TODO: in Elm and Haskell, functions cannot be compared for equality. In Mechanical, we think it's nice to provide function equality so they can be used in Maps and such. Currently, we guarantee three useful invariants about function equality:
Firstly, we guarantee that it's an equivalence relation:
No matter what, x == x (reflexivity)
If x == y, then y == x (symmetry)
If x == y and y == z, then x == z (transitivity)
Secondly, we guarantee functional referential transparency:
If x == y, then f(x) == f(y)
Note that this isn't true in JavaScript:
let f = c => (x => x + c);
f(1) === f(1) // false
// whereas in Mechanical:
Let f = c => (x => x + c)
f(1) == f(1) // true
Importantly, this applies only to function calls, not other expressions:
Let g = x => x + 1
Let h = x => x + 1
Let k = ~ + 1
Does g == h or g == k? No guarantees either way
Does g == f(1)? Currently, no guarantees either way. In the
future, this may be guaranteed to be false.
However, for some literal functions it's useful to guarantee equality (the third guarantee):
~.prop == ~.prop
#tag ~ == #tag ~
#tag ~ == ~.#tag()
// file1.mech
Export a = ~.prop
Export b = #tag ~
// file2.mech
From 'file1.mech' Import a, b
a == ~.prop // true
b == #tag ~ // true
b == ~.#tag() // true
But for anything else, even just arrow functions, all bets are off:
Does ~.prop.another == ~.prop.another ? No guarantees
Does ~.prop == (x => x.prop) ? No guarantees
Does #tag ~ == (x => #tag x) ? No guarantees
TODO: this is currently one of the few instances of undefined behavior in the language, should we specify it? I don't see any way to specify it that will be forward-compatible with the compiler unifying functions with fancier AST-equivalence heuristics.
Do <expr>
, Current <state>
, Initial <state>
Mechanical has no conventional control flow
the mental model for a Mechanical program is a state machine. State machines don't have control flow, they atomically transition from one state to another in response to an event. The new state, and the set of side-effects triggered, is a pure function of the previous state and the event.
the set of side-effects is, conceptually, unordered, since the transition is atomic. Therefore, Mechanical has to be a concurrent language, which is to say, all imperative statements "run" concurrently:
// in theory, these may print in any order:
Do console_log('hello 1')
Do console_log('hello 2')
// these sequences happen concurrently:
Do cmd:
Do console_log('foo 1')
Future wait = Do timeout(1000)
~ After Got wait ~
Do console_log('foo 2')
Do cmd:
Do console_log('bar 1')
Future wait = Do timeout(1000)
~ After Got wait ~
Do console_log('bar 2')
// in theory, that could print any of:
foo 1, bar 1, foo 2, bar 2
foo 1, bar 1, bar 2, foo 2
bar 1, foo 1, foo 2, bar 2
bar 1, foo 1, bar 2, foo 2
After Got
statements (see below)
witness variables:
Get x = Do foo()
Get y = Do bar(x)
The bar(x)
command of course has to happen after foo()
,
because it uses x
, which comes from doing the foo()
command.
Return
statements are only allowed in "tail position",
"early returns" aren't allowed. In theory the position shouldn't
matter but it helps avoid confusion:
// valid:
cmd:
Do some_cmd
Return Do some_other_cmd
cmd:
If x > 0:
Return Do some_cmd(x)
Else:
Do some_other_cmd
Return #error "x must be positive"
cmd:
Match x:
#name str ->
Return str
#year num ->
Return num.as_string()
// invalid:
cmd:
Return Do some_cmd
Do some_other_cmd
// ^ some_other_cmd *looks* like it should be skipped by the
// "early return", but under Mechanical semantics they both
// run concurrently, which is confusing
cmd:
If x > 0:
Return Do some_cmd(x)
Do some_other_cmd
Return #error "x must be positive"
There are no looping statements. Instead, use higher-order functions like map and reduce:
// parallel HTTP requests
Future pages = urls.map(url => Do fetch(url))
// sequential HTTP requests
Future pages = urls.reduce(
from: [],
update: (pages_so_far, next_url) => (Do cmd:
~ After Got pages_so_far ~
Future next_page = Do fetch(next_url)
~ After Got next_page ~
Return [...pages_so_far, next_page]
)
)
Compare with equivalent JavaScript:
// parallel HTTP requests
const pages = Promise.all(urls.map(url => fetch(url)));
// sequential HTTP requests
const pages = urls.reduce(update, []);
async function update(pagesSoFar, nextUrl) {
pagesSoFar = await pagesSoFar;
const nextPage = await fetch(nextUrl);
return [...pagesSoFar, nextPage];
}
// or in ES5:
var pages = urls.reduce(update, Promise.resolve([]));
function update(pagesSoFar, nextUrl) {
return pagesSoFar.then(pagesSoFar => {
return fetch(nextUrl).then(nextPage => {
return [...pagesSoFar, nextPage];
});
});
}
(TODO: consider making this easier with a for-loop construct based on for-await-of semantics)
the only compound statements are 2 branching statements:
If
statements:
If x > 0:
Do some_cmd(1)
Else If x < 0:
Do some_cmd(-1)
Else:
Do some_cmd(0)
The Else If
and Else
clauses are optional. The conditional must
be an explicit boolean expression, there is no coercion to boolean,
no truthy and falsy values.
Match
statements, for pattern-matching:
// you usually pattern-match on tagged values:
Match x:
#const_tag ->
// some tags have no parameter
Pass
#tag val ->
Do some_cmd_using(val)
#point { x, y, z } ->
// can destructure records
Do some_cmd_using(x**2 + y**2 + z**2)
#tag1 | #tag2 ->
// can match multiple tags
Pass
#year num | #date { year: num } ->
// when destructuring multiple tags, variables must have
// consistent types
Do some_other_cmd_using(num)
#tagA var | #tagB ->
// or if a variable isn't in every pattern, it may be #none
If var == #none:
// x must be #tagB
Pass
Else:
// x must be #tagA
Pass
#another_tag _ ->
// use _ to ignore a value when destructuring
Pass
_ ->
// wildcard: match anything
Pass
// you can also pattern-match on records:
Match y:
{ prop1: #tag1, prop2: val } ->
Do some_cmd_using(val)
{ prop1: #tag2, prop2: { thing: val } } ->
Do some_cmd_using(val)
(TODO: I haven't decided if patterns should be required to be mutually exclusive (except for the wildcard pattern, ofc))
in a non-cmd:
arrow function, the only leaf statements allowed are
pure Let
statements and Return
statements:
foo = x =>
Let y = foo(x)
Return bar(y)
in a cmd:
block or When
-handler, there are 4 kinds of imperative
leaf statements:
plain Do
statements, which don't expect a return value and can
Do
any type of command including void commands:
Do any_command
Get…Do
statements, for synchronous commands:
Get y = Do sync_cmd(x)
Get y = pure_fn(Do sync_cmd(x))
Get y = sync_cmd(x).Do!.pure_fn()
and for getting the Current
value of state variables (or Initial
,
during initialization):
Get now = Current system.unix_time
Get age = age_from_birthday(Current birthday_field.value)
Future…Do
statements and After Got
statements, for async
commands:
Future page = Do fetch(url)
// At this point, you can't use `page`, because it doesn't have
// the result of the `fetch()` yet, it has a promise that will
// eventually resolve to the result of the `fetch()`.
// In Mechanical, promises aren't first-class values, the only
// thing you can do with a promise is "await" it with an
// After-Got-statement (the ~'s (tildes) are optional):
~ After Got page ~
// Now `page` is the result of the `fetch()`, and we can use it:
Change help_pane.contents to page.body
// Note that at this point, we're in the future. Any number of
// events might have happened, handlers might have run, state
// might have changed, side-effects might have been performed
// since before the `~ After got page ~` statement.
//
// For comparison, consider JavaScript's `await` operator:
//
// foo(bar(), await baz(), qux())
//
// In this expression, `bar()` and `baz()` (which are on either
// side of the `await` keyword) happen immediately, whereas `foo()`
// and `qux()` (also on either side of the `await` keyword) happen
// in the future, after the promise resolves.
//
// By contrast, in Mechanical, it's clearer and more explicit
// what happens immediately, vs what happens `After` the promise
// resolves.
//
// (Callbacks would also be more explicit, but `After Got` is more
// convenient and easier to read than `.then(page => { ... })`
// callbacks or worse, nested callback hell.)
// Another point of comparison, consider:
//
// const thing1 = await fetch(thing1_url);
// const thing2 = await fetch(thing2_url);
// const thing3 = await fetch(thing3_url);
//
// make_use_of(thing1, thing2, thing3);
//
// This is a very natural way to use `await`, but is also a
// well-known anti-pattern!
// It makes the 3 HTTP requests sequentially, when in most cases
// it would be better to parallelize them:
//
// let thing1 = fetch(thing1_url);
// let thing2 = fetch(thing2_url);
// let thing3 = fetch(thing3_url);
//
// [thing1, thing2, thing3] =
// await Promise.all([thing1, thing2, thing3]);
//
// make_use_of(thing1, thing2, thing3);
//
// By contrast, in Mechanical, the better, parallel way is also
// the more natural way to use `After Got`:
Future thing1 = Do fetch(thing1_url)
Future thing2 = Do fetch(thing2_url)
Future thing3 = Do fetch(thing3_url)
~ After Got thing1, thing2, thing3 ~
Do make_use_of(thing1, thing2, thing3)
// If you *deliberately* want to sequentialize them, you can:
Future thing1 = Do fetch(thing1_url)
~ After Got thing1 ~
Future thing2 = Do fetch(thing2_url)
~ After Got thing2 ~
Future thing3 = Do fetch(thing3_url)
~ After Got thing3 ~
Do make_use_of(thing1, thing2, thing3)
// TODO: also explain why promises aren't first-class values
// by explaining how Future pages = urls.map(Do fetch(~)) works
// and showing how that's more convenient than JS
(TODO: should we have a ~ After Got Future y = Do x ~
statement,
for convenience?)
Change…To/By
statements, which update state:
Change name To "Chuck Finley"
// the following are equivalent:
Change counter To (Current counter) + 1
Change counter By c => c + 1
Change counter By ~ + 1
It's an error to update the same state multiple times in the same event handler
// ERROR: `counter` changed by multiple commands
Change counter By ~ + 1
Change counter By ~ + 1
// valid:
Change counter By ~ + 1
Future x = Do foo
~ After Got x ~
Change counter By ~ + 1
This is even tracked for first-class command values:
Let inc_counter = cmd:
Change counter By ~ + 1
When Event:
Do inc_counter
Change counter By ~ + 1
// ^ ERROR: `counter` changed by multiple commands
(This is tracked by the type system, pretty conservatively, with no escape hatch. TODO: is there any use case for an escape hatch?) You can update separate properties of the same record separately though, even nested:
Change player.loc.x By 10 * ~
Change player.loc.y By 5 * ~
You can also update separate items of the same array, with caveats:
// valid:
Change an_array[0] To "thing"
Change an_array[1] To "another thing"
Change other_array[i] To "thing"
Change other_array[i+1] To "another thing"
// invalid:
Change an_array[0] To "thing"
Change an_array[0] To "another thing"
Change other_array[5] To "thing"
Change other_array To ["another thing", ...Current an_array]
// compiler can't tell if valid, requires annotation:
Change an_array[0] To "thing" ^assert_no_conflict
Change an_array[i] To "another thing" ^assert_no_conflict
Change an_array[i] To "thing" ^assert_no_conflict
Change an_array[j] To "another thing" ^assert_no_conflict
// the ^assert_no_conflict annotation is only allowed for
// multiple commands in the same `When` handler.
// Different `When` handlers for the same event aren't
// allowed to update possibly-conflicting items even with
// the annotation, as a matter of style.
// TODO: should we allow it? Let's open a discussion ticket
the only other statement is Pass
, which is both a no-op statement and
a no-op command value (TODO: bikeshed name)
and that's it! No more statements. No while/for-loops, no break/continue, no throw statement, because remember, Mechanical doesn't have control flow
whereas statements are allowed in cmd:
-blocks and When
-handlers,
declarations are only allowed at the top-level
there are 2 imperative statements allowed at the top-level, plain Do
and Get…Do
statements, to perform side-effects during initialization
to use initial values of state, use Initial
, not Current
Future…Do
and After Got
aren't allowed at top-level because
we don't want to block initialization, but you can Do
a cmd:
which can have anything a cmd:
can have:
Do cmd:
Future y = Do foo(x)
~ After Got y ~
Do stuff_with(y)
Change
statements aren't allowed at top-level because state should
just be initialized to the right value in the first place
Let
is redundant with just defining regular top-level constants
Return
obviously can't be allowed outside of a function or command
unless top-level Pass
is useful for macros or something, there
seems no reason to allow it
the 2 branching statements (If
and Match
) are also allowed at the
top-level, and inside them, Let
and Pass
are allowed
state is the beating heart of Mechanical's state-machine-based conceptual model of programming. Consider:
// irreducible mutable state:
State seconds = 0
// computed state:
minutes = floor(seconds/60)
When ClockTick:
Change seconds By ~ + 1
Do console_log(
`minutes: ${Current minutes}, seconds: ${Current seconds}`)
irreducible, essential mutable state like seconds
is declared
using State
computed state like minutes
is defined as a pure expression of
other state. It cannot be directly mutated, but whenever seconds
is mutated, Mechanical will also update minutes
to match.
For example:
State counters = [1, 2, 3]
doubled = counters.map(2 * ~)
increment = i => cmd:
Change counters[i] By ~ + 1
Compiles to:
```js
var counters = [1, 2, 3];
var doubled = counters.map(x => 2*x)
increment = i => {
counters[i] = counters[i] + 1;
doubled[i] = 2 * (doubled[i] + 1)
};
```
Note that this is only possible because Mechanical is purely
functional and statically typed.
(What are the limitations of this? Well, it should work as long
as you don't use a custom loop/recursive function on a list.
So using built-in map, filter, reduce, sort, slice, etc should
all be similarly transformable, but if you wrote your own sort
function, for example, the Mechanical compiler would give up and
re-run your custom sort whenever the upstream state updated.
You can still provide a custom mutative update function even in
that case, though, and Mechanical still helps you out, see the
Derive State…From
declaration below).
event handlers are declared using When
. They take a stream of
events and optionally a parameter name or destructuring expression,
and then a indent block of statements to run when the event happens.
Statements that update state are state transitions of the state
machine. Statements that perform side-effects are side-effects of a
state transition (possibly a transition to the same state).
Operationally, any number of When
handlers for the same event
stream are fine as long as they don't update conflicting state
(or even annotated possibly-conflicting state, for now, see section
on the Change
statement above).
As a matter of style, multiple When
handlers in the same module
for the exact same event stream aren't allowed, although different
modules can share an event stream and each define When
handlers
on it. Note that an event can still trigger multiple handlers in the
same module if included in multiple stream expressions, for example
if streams A
, B
, and C
each have their own event handler and
there's also a handler for any([A, B, C])
.
(TODO: maybe it should be allowed with an annotation? Let's open a
discussion ticket)
Advanced Feature: there's also a special When…Changes
handler:
When minutes Changes To new_minutes:
Do redraw_minute_hand(new_minutes % 60)
When floor(60*minutes) Changes To new_hour:
Do redraw_hour_number(new_hour % 12)
This is meant for building abstractions on state: the code that
updates seconds
doesn't have to worry about redrawing stuff at
all, and can pretend that the view is a pure function of state
(which it is! Conceptually. Actually updating the DOM is necessarily
imperative, though). Note that this allows the view layer to be
implemented entirely in userland (contrast with Elm where the view
layer can only be implemented as part of the language runtime).
When…Changes
runs when a state expression changes, and it can only
perform side-effects, it cannot update state. To update state
when a state expression changes, use Derive State…From
, below.
Advanced Feature: while computed state is derived from other state via a pure expression, you can also create state derived from other state via mutative updates. There are two versions of this:
(TODO) If computed state is conceptually a pure function of other state, but Mechanical is unable to automatically generate efficient mutative updates, you can do so manually:
// custom reverse function, for some reason
reverse = match ~:
[] -> []
[first, ...rest] -> [...rev(rest), first]
State counters = [1, 2, 3]
Derive State reversed From counters:
Pure: reverse(counters)
Update(counters: ops):
// for arrays, the update step gets a diff representing
// the mutative updates to the array. The diff itself
// is an array of actions a "cursor" performed on the
// array:
// #skip number (number of current items to skip)
// #remove number (number of current items to remove)
// #insert items (array of new items to insert)
// #replace items (array of new items with which to
// replace current items)
//
// For example, if the number 42 was inserted at index 3
// (0-indexed), then Update would be given the diff
// [#skip 3, #insert 42].
ops.reduce(
from: {i: 0, result: Current reversed},
update: ((so_far: {i, result}, next: op) =>
match op:
#skip n ->
{i: i+n, result}
#remove n ->
{i, result: result.slice(
result.length()-i-n, result.length()-i)}
#insert items ->
{
i: i+items.length(),
result: result.splice(
result.length()-i, 0, items),
}
#replace items ->
{
i: i+items.length(),
result: result.splice(
result.length()-i,
items.length(),
items),
}
),
)
Yeah, it's kind of a pain, but you know what's amazing?
You don't have to test it! Mechanical uses property-based
testing to automatically generate possible values of
counters
and possible diffs, and ensures that your Update
expression always results in the same thing that your Pure
expression says the result should be.
If computed state is not even conceptually a pure function of
other state, but rather the result of imperative commands,
you can do that, too. The view layer is the canonical example:
the set of event streams from the DOM is state, it changes
when you add elements to the DOM, which happens in response
to application state changing.
In other words, this is like a When…Changes
handler except it
can also update a single piece of state (which may be composed
of multiple pieces of state, like a record).
State counters = [1, 2, 3]
Derive State view From counters:
Initial: counters.map(Do make_li_elements(~))
Update(counters: ops):
// blah blah blah (TODO)
modules are how code is organized. Every .mech
file is automatically
a module. Modules can be declared internal to a file with a Module
declaration, which is useful for namespacing and encapsulation.
A directory can also be a module if it has a _module_.mech
file,
although unlike __init__.py
it's useless if it's empty because
Mechanical modules are encapsulated, so _module_.mech
has to export
something to be useful.
Note that modules form a tree, unlike in Node or Python where modules
are leaves and can't nest.
Export
declarations are like JavaScript without the braces, but
there's no export default
, and the order has been switched from
export…from…
to From…Export…
.
TODO: should we allow From…Export *
? It doesn't actually create
any names in the current namespace.
Import
declarations have slightly different syntax from JavaScript:
// switched around from JavaScript, and no braces {}
From './lib.mech' Import x, y As renamed_y, z
// to give a name to the module, use `From…As…` instead
// of `* as <name>`
From './lib mech' As library Import x, y, z
// or just import the module by itself
Import './lib.mech' As library
Circular imports are allowed and work. There is currently no dynamic
import of .mech
files.
Mechanical also allows you to import values from JavaScript modules,
but this works differently from FFI in other languages. In most
languages, a module you import could import another module, which
imports another module, which imports yet another module that you
don't know anything about and have no reason to trust, and all of
these transitive dependencies can run any code and do anything.
(Targeted attacks by transitive dependencies have occurred in the
real world, e.g. event-stream
, electron-native-notify
,
but you should be more worried about widespread accidental
vulnerabilities, e.g. ZipSlip which affected dozens of libraries
and projects across every major language, or similar attacks like
XXE, all instances of the more general confused deputy problem.)
By contrast, Mechanical's design encourages capability-based
security, most importantly, the Principle of Least Authority.
The key insight is that the only way for a Mechanical program to
do anything dangerous, or indeed have any effect on the world
(whether DOM, filesystem, network requests, etc), is to call out
to JavaScript (or use a stdlib command that calls out to JavaScript,
like the View layer).
So by design, Mechanical code has no intrinsic authority to affect
the outside world. To have any effect on the world, the code must
be given fine-grained authority to do so, preferably the minimum
necessary to accomplish its purpose. Concretely:
in many cases you won't even notice this restriction, because
many Mechanical command functions that wrap JS functions take
a "foreign entity reference" as an argument (e.g. an HTML element,
File
object, etc), and those can be imported and called without
restriction:
// wrapper.mech
From './lib.js' Import:
foo :: (*HTMLElement, string) => SyncCmd<*HTMLElement>
bar :: (*HTMLElement, attr: string, value: string) => VoidCmd
// untrusted.mech
From './lib.mech' Import foo, bar
Export qux = el => cmd:
Get el2 = Do el.foo('nested_child')
Do el2.bar(attr: 'title', value: 'Click me!')
Return el2
Those are unrestricted because you have to pass an HTML element
to them in order to use them, and that's fine-grained authority.
When calling out to JS functions, supported types for interchange
are basically JSON (but immutable) along with undefined,
functions, and opaque foreign entity references. null
and
undefined
map to #none
. Mutable objects and host objects
are foreign entities, so to Mechanical code they're opaque
references. JS functions map to Mechanical functions that return
commands, except JS functions with no arguments map to just a
command value.
File
object. But if thisFile
object simultaneously designatesSo how do you get a foreign entity reference ("ref" for short)
in the first place? Usually, the JS library has at least one
function whose arguments are Mechanical values, but whose return
value includes one or more refs. Unlike the above, these cannot
be Import
ed. Instead, they can only be Bootstrap
ed by the
entry point module (the one you call mechc
on), which has
otherwise the same syntax as Import
:
to call code that creates foreign entity references from nothing,
must be passed in via Init Params
Init
to bootstrap, Bootstrap
The exception is the entry point module, which can d which requires type declarations:
From './lib.js' Import: foo :: (*Element, string) => Command<*Element> bar :: (string, thing: number, another: string) => string baz :: string
Type syntax is similar to TypeScript but instead of parameter names, you provide label names, and only for parameters that need labels (so, for functions with >=3 params, labels for 2nd param and up). However,
Import/Export
%View
Module
Like Elm, Mechanical is statically typed, purely functional, and side-effects are referentially transparent because they're represented by values called "commands". Unlike Elm, Mechanical uses JavaScript-like rather than Haskell-like syntax, including convenient imperative-like syntax for commands.
Also, commands can be synchronous, allowing synchronous calls to JavaScript, whereas in Elm commands (and therefore calls to JavaScript) are required to be async.
Finally, in Elm, type annotations are optional, but type declarations are required for custom types. In Mechanical there are no type declarations even for custom types, thanks to scoped hashtags.
Like Flux, Mechanical is built around unidirectional data flow. Unlike Flux, Mechanical enforces that views and state updates are pure functions, and even the imperative-like syntax for side-effects is referentially transparent. And all of that—views, state, side-effects—are all typechecked without you having to write a single type annotation.
Like Redux, Mechanical is built around unidirectional data flow with a single source of truth. Unlike Redux, Mechanical enforces that reducers and views are pure functions, and even the imperative-like syntax for side-effects is referentially transparent. And all of that—reducers, views, side-effects—are all typechecked without you having to write a single type annotation.
Whereas Functional Core, Imperative Shell is just a pattern, Mechanical enforces a purely functional core model, and the imperative-like syntax is referentially transparent. Also, updating the UI needn't be part of the imperative shell, instead the UI is expressed declaratively as a pure function of state, which is compiled into imperative updates to the UI.
You may use Mechanical under either of our permissive licenses, the highly readable Blue Oak Model License, or the more standard MIT license.