Early feedback from @adamk, @domenic, @slightlyoff, @erights, @waldemarhowart, @bterlson and @rwaldron (click here to send feedback).
This is a very early stage 1 exploration of a syntactical simplication (heavily inspired by Kotlin, Ruby and Groovy) that enables domain specific languages to be developed in userland.
It is a syntactic simplification that allows, on function calls, to omit parantheses around the last parameter when that's a lambda.
For example:
// ... this is what you write ...
a(1) {
// ...
}
// ... this is what you get ...
a(1, () => {
// ...
})
Functions that take just a single block parameter can also be called parentheses-less:
// ... this is what you write ...
a {
// ...
}
// ... this is what you get ...
a(() => {
// ...
})
We want to enable the ability to nest block params (e.g. to enable paired block params like select/when, builders and layout), and we are currently exploring using a sigil (e.g. possibly consistent with the bind operator ::
) to refer to the parent block param:
// ... this is what you write ...
a(1) {
::b(2) {
}
}
// ... this is somewhat (with some TBD symbol magic) you get ...
a (1, (__parent__) => {
__parent__.b(2, (__parent__) => {
})
})
Arguments can be passed to the block param:
// ... this is what you write ...
a(1) do (foo) { // syntax TBD
// ...
}
// ... this is what you get ...
a(1, (foo) => {
...
})
To preserve Tennent's Corresponde Principle, we are exploring which restrictions apply inside the block param (e.g. because these are based on arrow functions, break
and continue
aren't available as top level constructs and return
may behave differently).
While a simple syntactical simplification, it enables an interesting set of userland frameworks to be built, taking off presure from TC39 to design them (and an extensible shadowing mechanism that enables to bake them natively when/if time comes):
Here are some interesting scenarios:
- flow control (e.g. lock, unless, guard, defer, foreach, select)
- builders (e.g. map, dot, data)
- layout (e.g. html, android)
- configuration (e.g. node, makefiles)
- others (e.g. regexes, graphql, testing)
And interesting applications in DOM construction:
This is early, so there are still a lot of areas to explore (e.g. continue
and break
, return, bindings and this
) as well as strategic problems to overcome (e.g. forward compatibility) and things to check feasibility (e.g. completion values).
There is a polyfill, but I wouldn't say it is a great one quite yet :)
It is probably constructive to start reading from the prior art section.
A random list of possibilities collected from kotlin/groovy (links to equivalent idea in kotlin/groovy at the headers), somewhat sorted by most to least compelling.
lock (resource) {
resource.kill();
}
unless (expr) {
// statements
}
- aka assert
assert (document.cookie) {
alert("blargh, you are not signed in!");
}
- aka run
defer (100) {
// internally calls setTimeout(100)
alert("hello world");
}
// works on arrays, maps and streams
foreach (array) do (item) {
console.log(item);
}
let a = select (foo) {
::when (bar) { 1 }
::when (hello) { 2 }
::otherwise { 3 }
}
using (stream) {
// stream gets closed automatically.
}
// ... and sets ...
let a = map {
::put("hello", "world") {}
::put("foo", "bar") {}
}
let a = graph("architecture") {
::edge("a", "b") {}
::edge("b", "c") {}
// ...
}
let data = survey("TC39 Meeting Schedule") {
::question("Where should we host the European meeting?") {
::option("Paris")
::option("Barcelona")
::option("London")
}
}
let body = html {
::head {
::title("Hello World!") {}
}
::body {
::div {
::span("Welcome to my Blog!") {}
}
for (page of ["contact", "guestbook"]) {
::a({href: `${page}.html`}) { span(`${page}`) } {}
}
}
}
let layout =
VerticalLayout {
::ImageView ({width: matchParent}) {
::padding = dip(20)
::margin = dip(15)
}
::Button("Tap to Like") {
::onclick { toast("Thanks for the love!") }
}
}
}
const express = require("express");
const app = express();
server (app) {
::get("/") do (response) {
response.send("hello world" + request().get("param1"));
}
::listen(3000) {
console.log("hello world");
}
}
job('PROJ-unit-tests') {
::scm {
::git(gitUrl) {}
}
::triggers {
::scm('*/15 * * * *') {}
}
::steps {
::maven('-e clean test') {}
}
}
// NOTE(goto): inspired by https://github.com/MaxArt2501/re-build too.
let re = regex {
::start()
::then("a")
::then(2, "letters")
::maybe("#")
::oneof("a", "b")
::between([2, 4], "a")
::insensitively()
::end()
}
// NOTE(goto): hero uses proxies/getters to know when properties
// are requested. depending on the semantics of this proposal
// this may not be possible to cover.
let heroes = hero {
::name
::height
::mass
::friends {
::name
::home {
::name
::climate
}
}
}
// mocha
describe("a calculator") {
val calculator = Calculator()
::on("calling sum with two numbers") {
val sum = calculator.sum(2, 3)
::it("should return the sum of the two numbers") {
shouldEqual(5, sum)
}
}
}
One of the most interesting aspects of this proposal is that it opens the door to statement-like structures inside expressions, which are most notably useful in constructing the DOM.
For example, instead of:
let html = `<div>`;
for (let product of ["apple", "oranges"]) {
html += `<span>${product}</span>`;
}
html += `</div>`;
or
let html = `
<div>
${["apple", "oranges"].map(product => `<span>${product}</span>`).join("\n")}
</div>
`;
One could write:
let html = `
<div>
${foreach (["apple", "orange"]) do (item) {
`<span>${item}</span>`
}}
</div>
`;
For example, instead of:
// JSX
var box =
<Box>
{
shouldShowAnswer(user) ?
<Answer value={false}>no</Answer> :
<Box.Comment>
Text Content
</Box.Comment>
}
</Box>;
One could write:
// JSX
var box =
<Box>
{
select (shouldShowAnswer(user)) {
::when (true) {
<Answer value={false}>no</Answer>
}
::when (false) {
<Box.Comment>
Text Content
</Box.Comment>
}
}
}
</Box>;
This can open a stream of future extensions that would enable further constructs to be added. Here are some that occurred to us while developing this.
These are listed here as extensions because I believe we don't corner ourselves by shipping without them (i.e. they can be sequenced independently).
From @erights:
To enable something like
if (arg1) {
...
} else if (arg2) {
...
} else {
...
}
You'd have to chain the various things together. @erights proposed something along the lines of making the chains be passed as parameters to the first function. So, that would transpile to something like
if (arg1, function() {
...
},
"else if", arg2, function {
...
},
"else", function () {
...
})
Another notable example may be to enable try { ... } catch (e) { ... } finally { ... }
From @erights:
To enable control structures that repeat over the lambda (e.g. for-loops), we would need to re-execute the stop condition. Something along the lines of:
let i = 0;
until (i == 10) {
...
i++
}
We would want to turn expr
into a function that evaluates expr
so that it could be re-evaluated multiple times. For example
let i = 0;
until (() => i == 10, function() {
...
i++
})
TODO(goto): should we do that by default with all parameters?
These are some areas that we are still exploring.
To preserve tennent's correspondence principle as much as possible, here are some considerations as we decide what can go into block params:
return
statements inside the block should either throwSyntaxError
(e.g. kotlin) or jump to a non-local return (e.g. kotlin's inline functions non-local returns)break
,continue
should either throwSyntaxError
or control the lexical flowyield
can't be used as top level statements (same strategy as() => { ... }
)throw
works (e.g. can be re-thrown from function that takes the block param)- the completion values are used to return values from the block param (strategy borrowed from kotlin)
- as opposed to arrow functions,
this
can be bound.
If we bake this in, do we corner ourselves from ever exposing new control structures (e.g. unless () {})?
That's a good question, and we are still evaluating what the answer should be. Here are a few ideas that have been thrown around:
- user defined form shadows built-in ones
- sigils (e.g. for! {})
In this formulation, we are leaning towards the former.
It is important to note that the current built-in ones can't be shadowed because they are reserved keywords
. So, you can't override for
or if
or while
(which I think is working as intended), but you could override ones that are not reserved keywords (e.g. until
or match
).
Like Kotlin, it is desirable to make the block params return values to the original function calling them. We aren't entirely sure yet what this looks like, but it will most probably borrow the same semantics we end up using in do expressions and other statement-like expressions.
let result = foreach (numbers) do (number) {
number * 2 // gets returned to foreach
}
There are certain block params that go together and they need to be somehow aware of each other. For example, select
and when
would ideally be described like this:
select (foo) {
when (bar) {
...
}
}
How does when
get resolved?
The global scope? If so, how does it connect with select
to test bar
with foo
?
From select
? If so, how does it avoid using the this
reference and have with
-like performance implications? perhaps @@this?
From @bterlson:
It would be great if we could make return
to return from the lexically enclosing function.
Kotlin allows return
from inlined functions, so maybe semantically there is a way out here.
One challenge with return
is for block params that outlive the outer scope. For example:
function foobar() {
run (100) {
// calls setTimeout(1, block) internally
return 1;
}
return 2;
}
foobar() // returns 2
// after 100 ms
// block() returns 1. does that get ignored?
Note that Java throws a TransferException
when that happens. SmallTalk allows that too, so the intuition is that this is solvable.
continue
and break
are interesting because their interpretation can be defined by the user. For example:
for (let i = 0; i < 10; i++) {
unless (i == 5) {
// You'd expect the continue to apply to the
// lexical for, not to the unless
continue;
}
}
Whereas:
for (let i = 0; i < 10; i++) {
foreach (array) do (item) {
if (item == 5) {
// You'd expect the continue here to apply to
// the foreach, not the lexical for.
continue;
}
}
}
It is still unclear if this can be left as an extension without cornering ourselves.
We are exploring other alternatives here.
From @bterlson:
There are a variety of cases where binding helps. For example, we would want to enable something like the following:
foreach (map) do (key, value) { ... }
to be given by the foreach function implementation.
foreach (map) do (key, value) {
// ...
}
To be equivalent to:
// ... is equivalent to ...
foreach (map, function(key, value) {
})
Exactly which keyword we pick (e.g. in
or with
or :
etc) and its position (e.g. foreach (item in array)
or foreach (array with item)
) TBD.
Another alternative syntax could be something along the lines of:
foreach (map) { |key, value|
// ...
}
Or
foreach (let {key, value} in map) {
// ...
}
We probably need to do a better job at exploring the design space of use cases before debating syntax, hence leaving this as a future extension.
This is currently polyfilled as a transpiler. You can find a lot of examples here.
npm install -g @docscript/docscript
npm test
You really don't want to use this right now. Very early prototype.
The following is a list of previous discussions at TC39 and related support in other languages.
- block lambdas and discussion
- Allen's considerations on break and continue
- javascript needs blocks by @wycats
def iffy(condition)
if (condition) then
yield()
end
end
iffy (true) {
puts "This gets executed!"
}
iffy (false) {
puts "This does not"
}
for i in 0..1
puts "Running: #{i}"
iffy (i == 0) {
# This does not break from the outer loop!
# Prints
#
# Running: 0
# Running: 1
break
}
end
for i in 0..1
iffy (i == 0) {
# This does not continue from the outer loop!
# Prints
#
# Running: 0
# Running: 1
next
}
puts "Running: #{i}"
end
def foo()
iffy (false) {
return "never executed"
}
iffy (true) {
return "executed!"
}
return "blargh, never got here!"
end
# Prints "executed!"
foo()
fun main(args: Array<String>) {
unless (false) {
println("foo bar");
"hello" // "this expression is unused"
"world" // "this expression is unused"
1 // last expression statement is used as return value
// "return is not allowed here"
// return "hello"
//
// "break and continue are only allowed inside a loop"
// continue;
//
// throwing is allowed.
// throw IllegalArgumentException("hello world");
};
var foo = "hello";
switch (foo) {
case ("hello") {
}
case ("world") {
}
}
}
fun unless(expr: Boolean, block: () -> Any) {
if (!expr) {
var bar = block();
println("Got: ${bar}")
}
}
fun switch(expr: Any, block: Select.() -> Any) {
var structure = Select(expr);
structure.block();
}
fun case() {
println("hi from global case");
}
class Select constructor (head: Any) {
var result = null;
fun case(expr: Any, block: () -> Any) {
if (this.head == expr) {
println("hi from case");
result = block();
}
}
}
for eachEntry(String name, Integer value : map) {
if ("end".equals(name)) break;
if (name.startsWith("com.sun.")) continue;
System.out.println(name + ":" + value);
}