Early feedback from @adamk, @domenic, @slightlyoff, @erights, @waldemarhowart, @bterlson and @rwaldron (click here to send feedback).
Block Params
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.
Use cases
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.
flow control
lock
lock (resource) {
resource.kill();
}
Perl's unless
unless (expr) {
// statements
}
Swift's guard
- aka assert
assert (document.cookie) {
alert("blargh, you are not signed in!");
}
Swift's defer
- aka run
defer (100) {
// internally calls setTimeout(100)
alert("hello world");
}
C#'s foreach
// works on arrays, maps and streams
foreach (array) do (item) {
console.log(item);
}
VB's select
let a = select (foo) {
::when (bar) { 1 }
::when (hello) { 2 }
::otherwise { 3 }
}
C#'s using
using (stream) {
// stream gets closed automatically.
}
builders
maps
// ... and sets ...
let a = map {
::put("hello", "world") {}
::put("foo", "bar") {}
}
dot
let a = graph("architecture") {
::edge("a", "b") {}
::edge("b", "c") {}
// ...
}
custom data
let data = survey("TC39 Meeting Schedule") {
::question("Where should we host the European meeting?") {
::option("Paris")
::option("Barcelona")
::option("London")
}
}
layout
kotlin's templates
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}`) } {}
}
}
}
android
let layout =
VerticalLayout {
::ImageView ({width: matchParent}) {
::padding = dip(20)
::margin = dip(15)
}
::Button("Tap to Like") {
::onclick { toast("Thanks for the love!") }
}
}
}
Configuration
node
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");
}
}
makefiles
job('PROJ-unit-tests') {
::scm {
::git(gitUrl) {}
}
::triggers {
::scm('*/15 * * * *') {}
}
::steps {
::maven('-e clean test') {}
}
}
Misc
regexes
// 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()
}
graphql
// 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
}
}
}
testing
// 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)
}
}
}
Applications
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.
Template Literals
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>
`;
JSX
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>;
Extensions
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).
chaining
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 { ... }
functization
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?
Areas of Exploration
These are some areas that we are still exploring.
Tennent's Correspondence Principle
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.
Forward Compatibility
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
).
Completion Values
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
}
scoping
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?
return
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, break
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.
bindings
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.
Polyfill
This is currently polyfilled as a transpiler. You can find a lot of examples here.
npm install -g @docscript/docscript
Tests
npm test
Status
You really don't want to use this right now. Very early prototype.
Prior Art
The following is a list of previous discussions at TC39 and related support in other languages.
TC39
- block lambdas and discussion
- Allen's considerations on break and continue
- javascript needs blocks by @wycats
SmallTalk
Ruby
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()
Groovy
Kotlin
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();
}
}
}
Java
for eachEntry(String name, Integer value : map) {
if ("end".equals(name)) break;
if (name.startsWith("com.sun.")) continue;
System.out.println(name + ":" + value);
}