Block Params S1
中文标题:块参数
提案速览
该提案探索了一种语法简化,允许在函数调用中,当最后一个参数是 lambda 时省略括号,从而支持用户域 DSL。提案讨论了流程控制、构建器、布局和配置等用例,并探讨了 Tennent 对应原则、前向兼容性、完成值和作用域等问题。已有 polyfill,但尚不推荐用于生产环境。
Note
以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。
来自 @adamk、@domenic、@slightlyoff、@erights、@waldemarhowart、@bterlson 和 @rwaldron 的早期反馈(点击 此处 发送反馈)。
块参数
这是对一种语法简化的非常早期的 第1阶段 探索(深受 Kotlin、Ruby 和 Groovy 的启发),使得可以在用户域中开发领域特定语言。
这是一种语法简化,允许在函数调用时,当最后一个参数是 lambda 时省略括号。
例如:
// ... 你写成这样 ...
a(1) {
// ...
}
// ... 你会得到这样 ...
a(1, () => {
// ...
})
只接受单个块参数的函数也可以省略括号调用:
// ... 你写成这样 ...
a {
// ...
}
// ... 你会得到这样 ...
a(() => {
// ...
})
我们希望支持嵌套块参数(例如,支持成对块参数,如 select/when、builders 和 layout),目前正在探索使用一个符号(例如,可能与绑定运算符 :: 一致)来引用父块参数:
// ... 你写成这样 ...
a(1) {
::b(2) {
}
}
// ... 这大致是(使用一些待定的符号魔法)你得到的 ...
a (1, (__parent__) => {
__parent__.b(2, (__parent__) => {
})
})
可以向块参数传递参数:
// ... 你写成这样 ...
a(1) do (foo) { // 语法待定
// ...
}
// ... 你会得到这样 ...
a(1, (foo) => {
...
})
为了保持 Tennent 对应原则,我们正在探索块参数内部有哪些 限制适用(例如,因为基于箭头函数,break 和 continue 不能作为顶层结构使用,return 的行为可能有所不同)。
虽然这是一个简单的语法简化,但它使得一系列有趣的用户域框架得以构建,减轻了 TC39 设计它们的压力(以及一个可扩展的 遮蔽机制,以便在时机成熟时本地化实现它们):
以下是一些有趣的场景:
以及在 DOM 构建 中的有趣应用:
这还很早期,所以仍然有很多 探索领域(例如 continue 和 break、return、绑定 和 this),以及需要克服的战略性问题(例如 前向兼容性)和需要检查可行性的事项(例如 完成值)。
有一个 polyfill,但我还不能说它很好 :)
从 先例 部分开始阅读可能更有建设性。
用例
从 kotlin/groovy 收集的随机可能性列表(在标题处链接到 kotlin/groovy 中的等价想法),大致按从最有吸引力到最不吸引人的顺序排列。
流程控制
lock (resource) {
resource.kill();
}
assert (document.cookie) {
alert("blargh, you are not signed in!");
}
defer (100) {
// 内部调用 setTimeout(100)
alert("hello world");
}
// 适用于数组、映射和流
foreach (array) do (item) {
console.log(item);
}
let a = select (foo) {
::when (bar) { 1 }
::when (hello) { 2 }
::otherwise { 3 }
}
using (stream) {
// stream 会自动关闭。
}
构建器
// ... 以及集合 ...
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') {}
}
}
杂项
// 注意(goto):灵感来自 https://github.com/MaxArt2501/re-build 也。
let re = regex {
::start()
::then("a")
::then(2, "letters")
::maybe("#")
::oneof("a", "b")
::between([2, 4], "a")
::insensitively()
::end()
}
// 注意(goto):hero 使用代理/获取器来知道何时请求属性。
// 取决于此提案的语义,这可能无法覆盖。
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)
}
}
}
应用
此提案最有趣的方面之一是它打开了在表达式中使用类似语句的结构的大门,这在构建 DOM 时特别有用。
模板字符串
例如,不写成:
let html = `<div>`;
for (let product of ["apple", "oranges"]) {
html += `<span>${product}</span>`;
}
html += `</div>`;
或者
let html = `
<div>
${["apple", "oranges"].map(product => `<span>${product}</span>`).join("\n")}
</div>
`;
可以写成:
let html = `
<div>
${foreach (["apple", "orange"]) do (item) {
`<span>${item}</span>`
}}
</div>
`;
JSX
例如,不写成:
// JSX
var box =
<Box>
{
shouldShowAnswer(user) ?
<Answer value={false}>no</Answer> :
<Box.Comment>
Text Content
</Box.Comment>
}
</Box>;
可以写成:
// JSX
var box =
<Box>
{
select (shouldShowAnswer(user)) {
::when (true) {
<Answer value={false}>no</Answer>
}
::when (false) {
<Box.Comment>
Text Content
</Box.Comment>
}
}
}
</Box>;
扩展
这可以开启一系列未来的扩展,以添加更多的构造。以下是在开发过程中我们想到的一些。
这些被列为扩展,因为我相信在不包含它们的情况下发布不会使我们陷入困境(即它们可以独立地按顺序进行)。
链式调用
来自 @erights:
为了支持类似
if (arg1) {
...
} else if (arg2) {
...
} else {
...
}
你需要将各种部分链接在一起。@erights 提议大致让链作为参数传递给第一个函数。因此,那将转译为类似
if (arg1, function() {
...
},
"else if", arg2, function {
...
},
"else", function () {
...
})
另一个值得注意的例子可能是启用 try { ... } catch (e) { ... } finally { ... }
函数化
来自 @erights:
为了启用重复执行 lambda 的控制结构(例如 for 循环),我们需要重新执行停止条件。大致像:
let i = 0;
until (i == 10) {
...
i++
}
我们希望将 expr 转换为一个计算 expr 的函数,以便可以多次重新计算。例如
let i = 0;
until (() => i == 10, function() {
...
i++
})
待办(goto):我们应该默认对所有参数这样做吗?
探索领域
以下是我们仍在探索的一些领域。
Tennent 对应原则
为了尽可能保持 Tennent 对应原则,以下是我们决定块参数中可以包含什么时的一些考虑:
- 块内的
return 语句应该抛出 SyntaxError(例如 kotlin)或跳转到 非局部返回(例如 kotlin 的内联函数 非局部返回)
break、continue 应该抛出 SyntaxError 或控制 词法流程
yield 不能用作顶层语句(与 () => { ... } 相同策略)
throw 可以工作(例如,可以从接受块参数的函数重新抛出)
- 完成值 用于从块参数返回值(策略借自 kotlin)
- 与箭头函数不同,
this 可以被绑定。
前向兼容性
如果我们内置此功能,我们是否会陷入无法再公开新控制结构(例如 unless () {})的困境?
这是个好问题,我们仍在评估答案应该是什么。以下是一些被抛出的想法:
- 用户定义的形态遮蔽内置的形态
- 符号(例如 for! {})
在这种表述中,我们倾向于前者。
重要的是要注意,当前的内置函数不能被遮蔽,因为它们是 保留关键字。所以,你不能覆盖 for 或 if 或 while(我认为这是预期行为),但你可以覆盖那些不是保留关键字的(例如 until 或 match)。
完成值
与 Kotlin 类似,希望让块参数向调用它们的原始函数返回值。我们还不完全确定这会是什么样子,但它很可能借用我们最终在 do 表达式 和 类似语句的表达式 中使用的相同语义。
let result = foreach (numbers) do (number) {
number * 2 // 返回给 foreach
}
作用域
有些块参数是成对出现的,它们需要以某种方式意识到彼此。例如,select 和 when 理想上应该这样描述:
select (foo) {
when (bar) {
...
}
}
when 如何解析?
全局作用域?如果是这样,它如何与 select 连接以用 foo 测试 bar?
从 select?如果是这样,如何避免使用 this 引用 并避免 with-式性能影响?也许 @@this?
return
来自 @bterlson:
如果我们能让 return 从词法上封闭的函数返回,那就太好了。
Kotlin 允许从内联函数 return,所以也许在语义上有出路。
return 的一个挑战是对于超出外部作用域的块参数。例如:
function foobar() {
run (100) {
// 内部调用 setTimeout(1, block)
return 1;
}
return 2;
}
foobar() // 返回 2
// 100 毫秒后
// block() 返回 1。这会被忽略吗?
注意,Java 在这种情况下抛出 TransferException。SmallTalk 也允许这样,所以直觉是这是可解的。
continue、break
continue 和 break 很有趣,因为它们的解释可以由用户定义。例如:
for (let i = 0; i < 10; i++) {
unless (i == 5) {
// 你期望 continue 应用于
// 词法 for,而不是 unless
continue;
}
}
而:
for (let i = 0; i < 10; i++) {
foreach (array) do (item) {
if (item == 5) {
// 你期望这里的 continue 应用于
// foreach,而不是词法 for。
continue;
}
}
}
目前尚不清楚这是否可以作为扩展保留而不使自身陷入困境。
我们正在 这里 探索其他替代方案。
绑定
来自 @bterlson:
在很多情况下绑定很有帮助。例如,我们希望启用类似以下内容:
foreach (map) do (key, value) { ... } 由 foreach 函数的实现提供。
foreach (map) do (key, value) {
// ...
}
等价于:
// ... 相当于 ...
foreach (map, function(key, value) {
})
具体选择哪个关键字(例如 in 或 with 或 : 等)及其位置(例如 foreach (item in array) 或 foreach (array with item))待定。
另一种替代语法可能类似:
foreach (map) { |key, value|
// ...
}
或者
foreach (let {key, value} in map) {
// ...
}
我们可能需要在争论语法之前更好地探索用例的设计空间,因此将其作为未来的扩展。
Polyfill
目前这是作为转译器进行 polyfill 的。你可以在 这里 找到很多示例。
npm install -g @docscript/docscript
测试
npm test
状态
你现在真的不应该使用这个。非常早期的原型。
先例
以下是 TC39 之前的讨论和其他语言中的相关支持的列表。
TC39
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) {
# 这不会从外层循环中断!
# 打印
#
# Running: 0
# Running: 1
break
}
end
for i in 0..1
iffy (i == 0) {
# 这不会从外层循环继续!
# 打印
#
# 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
# 打印 "executed!"
foo()
Groovy
Kotlin
fun main(args: Array<String>) {
unless (false) {
println("foo bar");
"hello" // "这个表达式未使用"
"world" // "这个表达式未使用"
1 // 最后一个表达式语句用作返回值
// "这里不允许 return"
// return "hello"
//
// "break 和 continue 只允许在循环内使用"
// continue;
//
// 允许抛出异常。
// 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);
}
相关工作