Skip to content

laughinghan/mechanical

Repository files navigation

Mechanical Chat on Gitter

A language that makes building webapps (and more!) as easy as building finite-state machines.

  • Best-of-each-world:
    • Define a state machine declaratively using purely functional logic, perform side-effects imperatively.
    • Statically typed, no type annotations. Algebraic data types without type declarations.
  • X% faster and Y% smaller than React: Instead of virtual DOM diffing, just compile to imperative, mutative JavaScript.
  • Fully interoperable: Compile to JavaScript modules that can call or be called by any JS library or browser/Node API.

(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!

Show Me Some Code Already

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)

Usage

Install:

npm install mechanical

Run the compiler:

npx mechc some_file.mech

Which will output: some_file.js

Differences from JS

Extensions

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.
    

Incompatibilities

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

    • Valid: this_is_totally_valid
    • Invalid: _foo, foo__bar, foo_, $foo
      (TODO: bikeshed, maybe allow foo__bar?)
  • Exponentiation isn't chainable

    • Should 2**3**2 be (2**3)**2 = 64, or 2**(3**2) = 512? JS and Python both say 512, but I think that's confusing because it's the other way around for other non-associative operators, e.g. 2/3/4 = (2/3)/4. In Mechanical, 2**3**2 is a syntax error
    • (Just like JS, -2**2 is also a syntax error. This actually differs from Python, where -2**2 = -(2**2) = -4, but that's confusing because it look like it could be (-2)**2 = 4)
  • Comparisons are chainable

    • In JS, 3 > 2 > 1 is false, which is confusing. In Mechanical, not only does a > b > c behave as expected, but so does a > b >= c == d, or a == b < c == d < e == f. This feature is inspired by Python, however unlike Python, constructions like a < b > c are prohibited (they have to point the same way), and != 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

    • We have none of ~, &, |, <<, >>, >>> built-in, but I hope to introduce a built-in macro
  • JS-specific things that don't make sense with Mechanical semantics:

    • No "strict in/equality" ===/!==. Regular in/equality ==/!= is already strict
    • No increment/decrement operators ++/--
    • No in or instanceof relations
    • No assignment operators =, +=, -=, *=, /=, etc
    • No comma operator

Semantics

(TODO: the previous section and this section are a bit too much of a brain-dump, and need to be entirely reorganized for readability)

  • there are no mutable data structures, instead mutable state variables may be changed from one immutable data value to another
  • data values
    • the 3 scalars, booleans, numbers, and strings, are the same as in JS
      • (TODO: Unicode is messed up in JS strings because of UTF-16 heritage, do we want to fix them to be UTF-8?)
    • arrays are similar to JS. Arrays of a single type (e.g. [1, 2, 3] or ['one', 'two', 'three'], aka monomorphic arrays) are exactly like in JS but immutable. An array with a mix of strings and numbers ([1, 'two', 3]) isn't allowed unless the values are hashtagged: [#year 1, #name 'two', #year 3]. When reading from the array, you can then pattern-match on the hashtags. It is recommended you choose descriptive hashtags. More on hashtags and pattern-matching below.
      • (TODO: we plan to eventually allow "dynamic-dispatch arrays" that allow mixing of types as long as they all support a given interface, like Rust's dynamic dispatch trait objects; for consistency, shouldn't we allow you to do that with our extensible sum type in addition to interfaces/traits, too? The type system can actually remain totally sound, and we can still add friction by requiring an extra keyword like mixed in the array declaration. TODO: open a ticket about this)
    • records are like TypeScript interfaces, i.e. JS objects with fixed fields and types, but every field is required, no optional fields although fields can potentially have the null value #none (more on hashtags below). Records are structurally typed (similar to TypeScript interfaces, except using row polymorphism like Elm). Field names aren't namespaced (unlike hashtags) so can't be private/opaque.
      • (Private/opaque record fields are compatible with the type system, is there any use case? I can't think of one, especially not one where an invisible field would make more sense than a visible field with an opaque hashtagged type. TODO: open a discussion ticket)
    • (hash)tagged unions are like extensible sum types (known in OCaml as polymorphic variants), except they're namespaced/scoped by file so you can create opaque types and newtypes with them.
      • I should write a blogpost on this, afaik this idea is totally unique to Mechanical. I think it's really cool because it provides the benefits of a nominative algebraic data type without name clashes, including opaque types and [newtype], while avoiding the need not only for type annotations but even for type declarations.
    • there are also global built-in #none and #error _ types (#error takes a parameter), auto-imported into every module. Whereas other hashtag types can only be unioned with other (non-opaque) hashtag types, these two can be unioned with any type at all including primitives and opaque types. T | #none is the equivalent of other language's Option or Maybe type, and T | #error E is the equivalent of other language's Result or Either type.
  • function and command values
    • functions' 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:

      • mathematically they're pure functions from state to new state and possibly a return value
      • or, in JS terms they're zero-argument side-effectful functions. That's actually how they're implemented in the first compilation pass, but TODO the immediately-invoked-zero-argument-function-value should be optimized away in later compiler passes
    • 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.

      (If this strikes you as basically like a Haskell monad, that's because it is. I think my way of describing it is easier to understand, at least coming from an imperative programming mindset, though.)
      • The obvious important difference from Haskell monads is that it's hardcoded to a specific monad that's like a combo IO and State monad. Instead of generalizing to other monads, we generalize by having all external side-effect functions be dependency-injected. This might sound unnatural, but it's actually a natural consequence of following capability-based security principles.
      • This model of dependency-injecting all external side-effect functions has the advantage of easily composing different side-effects without monad transformers and tediously "lifting" functions from one monad to another. This is kinda like algebraic effects in that way, however:
        • What are algebraic effects? They're an evolution of monads. Monads are cool because they can represent side-effects and control flow in a way that remains referentially transparent, and remains statically type-checked, and in fact even side-effects are statically checked and constraints on side-effects statically enforced, by encoding side-effects into the type system. (Contrast with most statically typed languages like Java, C#, or OCaml where the type system is solely concerned with parameter types and return types and entirely ignores side-effects.) Algebraic effects do all of that too, but allow different side-effects to compose more easily than monads.
          • (Without side-effects, after all, Haskell is a "completely useless language—you have this black box, and you press Go, and it gets hot, but there's no output!")
          • If you're coming from a Haskell background, the first few pages of the Edwin Brady paper presents an especially clear motivation for the problems with monad transformers and how algebraic effects can help.
            • The paper uses Brady's language Idris, which is like Haskell but with dependent types. There are also plain Haskell implementations, like Oleg Kiselyov's extensible-effects/free-er monad which instead of dependent types uses extensible sum types much like Mechanical's hashtagged unions (the paper calls them "open unions").
          • From a JavaScript, OCaml, or Elm background, I recommend reading the Koka overview Sections 2.1-2.9, and then checking out Section 1.4. (At least for Koka v1; Koka v2 is in development, please let me know if you notice the book has changed too.)
          • If your primary exposure to algebraic effects is Dan Abramov's Overreacted blogpost Algebraic Effects for the Rest of Us, then I especially recomend reading the Koka overview. Dan Abramov, coming from a React-focused, dynamically typed, imperative perspective, is focused on how effect handling impacts control flow (treating effects algebraically is actually irrelevant to his discussion).
            Mechanical, a statically typed language with no conventional control flow, instead draws inspiration from efforts in purely functional languages (which also have no conventional control flow) to use algebraic effects as a substitute for control flow, while remaining referentially transparent and statically (and algebraically) typed.
          • If your primary exposure to algebraic effects is Multicore OCaml then I also recommend reading the Koka overview. Multicore OCaml is also focused on how effect handling impacts control flow, because Multicore OCaml is impure. Mechanical has no conventional control flow and draws inspiration from efforts in purely functional languages (which also don't have control flow) to use algebraic effects as a substitute for control flow, while remaining referentially transparent.
          • A lot more literature—way more than I could read—available at: https://github.com/yallop/effects-bibliography
        • Algebraic effects introduce elements of imperative programming that I greatly dislike: control flow and pervasive implicit ordering.

          • In imperative languages, and even impure functional languages like OCaml or Lisp, sequence points are defined in virtually every language construct specifying that, for example, in foo(bar(), baz()), the order of evaluation is implicitly bar(), then baz(), then foo(). In purely functional languages and dataflow languages, order of evaluation doesn't matter, so you don't even have to think about that—except that the order of side-effects can matter, so algebraic effects impose a total ordering on effects by nesting continuation functions.
          • In my opinion this is throwing the baby out with the bathwater: just because the order of some side-effects can matter, doesn't mean we need to impose a total ordering on all side-effects, regardless of whether the order does matter. (This is a generalization of the implicit ordering of monads like Haskell's IO monad; by contrast, in Mechanical all side-effects are concurrent unless explicitly ordered, see below.)
        • Algebraic 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).

          • Multicore OCaml limits continuations to one-use-only for that reason (performance). Their version of algebraic effects are basically just typechecked coroutines, their design documents say their purpose is to allow userland scheduling of fibers, which is a very different purpose from what Mechanical would consider, which is an alternative to monads for modeling side-effects.
          • An example of such a parlor trick is the "ambiguous" effect handler from the Koka primer on effect handlers, which evaluates a computation for every possible return value of an effect "simultaneously" (so if the computation flips a coin twice and returns a tuple of the two results, then evaluating the computation with this handler returns [(Heads, Heads), (Heads, Tails), (Tails, Heads), (Tails, Tails)] or similar).
          • In fact, Koka is an informative contrast with Mechanical. Hardcoding a monad with exactly 2 effects (state mutation and foreign function calls) makes Mechanical isomorphic to a subset of JavaScript, which makes compilation very easy. By contrast, the Koka compiler represents such fully general effect handlers using CPS (continuation-passing style). Naive compilation of CPS to JavaScript would cause every stack frame to be allocated as a closure on the heap, so Koka uses a sophisticated [type-directed transform] to recover the information that was "lost" by generalizing to fully general effects, but that Mechanical has hardcoded.
            In theory, that should mean that Koka code that is equivalent to Mechanical's hardcoded effects compiles to equally performant JS, and only effects that needs to allocate continuations on the heap, like the "ambiguous" effect, will pay that performance penalty. (Whereas Mechanical's hardcoded monad can't represent such effects at all.)
            In principle, there's nothing stopping Mechanical from adopting the same approach. But it makes the compiler more complex and slower, so I'm still waiting to learn of a practical use case before I consider it.
      • The other important difference from Haskell monads is ordering. If you have a sequence of IO actions in a do-block, the resulting side-effects are guaranteed to be performed in that order.
        By contrast, Mechanical is a concurrent language. Do statements perform side-effects in no particular order unless one is specified, and in the future I'm hoping the compiler can auto-parallelize programs among Web Workers or even pthreads, although that's very far in the future.
        (The 2 ways to explicitly specify order being After Got statements and witness variables, see below.)
    • there 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)

          • TODO: should/can we unwrap pure commands without eval(), so that 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.

  • operators
    • TODO
  • pseudo-expressions: Do <expr>, Current <state>, Initial <state>
    • unlike other expressions, these aren't referentially transparent: a given pseudo-expression may evaluate to different things at different times, and may even perform side-efffects. Therefore they're really part of an imperative statement, see below
  • statements
    • 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
        
        • except for 2 ways to explicitly specify order:
          • 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

  • declarations
    • 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.

        The amazing thing is that Mechanical updates computed state with efficient mutations where possible, rather than recomputing it from scratch.

        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.

          • The main inconvenience here is that this prevents libraries from providing convenience functions that e.g. take in a filename string instead of a File object. But if this library is just supposed to act on one file, why are you giving it access to your entire filesystem? What if it's just supposed to parse YAML in a file, but it has a bug where malicious content in the file can execute arbitrary code?
            A core tenet of capability-based security is "don't separate designation from authority". In this example, the filename string designates a file, but comes with no authority to access the file, and instead, you implicitly authorize all code in every library you call (and every library they call, and so on) to access the entire filesystem, and much much more. By contrast, a File object simultaneously designates a file and authorizes access to it, which is fine-grained Least Authority.
        • So 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 Imported. Instead, they can only be Bootstraped 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

      • Bootstrap, Init
      • Declare
    • %View

    • Module

Comparisons

Elm

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.

Flux

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.

Redux

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.

Functional Core, Imperative Shell

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.

License: Blue Oak or MIT

You may use Mechanical under either of our permissive licenses, the highly readable Blue Oak Model License, or the more standard MIT license.

About

A language that makes building webapps as easy as finite-state machines

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published