首先,这个建议仅代表个人想法,没有参考价值,个人的目的在于抛砖引玉。
UI 语法糖的方向与意义
- Moonbit 作为一个针对 AI 优化语言,在适配 GUI 开发领域有非常重大的意义。目前的 GUI 相关的编程语言都是针对人类优化,对 AI 并不友好。Moonbit 一旦成功适配 GUI 开发,那么在 AI 时代,有潜力超车现有的 GUI 编程语言。
- 语法糖本身不捆绑任何 UI 开发框架,但目的是为生态提供一种“简化”写法,正如 JSX 这种语法的意义一样。
- 提升开发效率,这里的开发效率不单单是针对人类,对于 AI 同样有意义,使用更少的 token 输出更多的内容,目的能有效降低成本。
语法方案 1.0:
类似 kotlin 语法:最后一个必要参数如果是 函数,那么可以缺省(...,fn(){})
:
- 使用
foo{}
替代foo(fn(){})
- 或者使用
foo(...){}
替代foo(...,fn(){})
let view =
>| div {
>| h1(
>| style = Style()
>| ..font_size(20)
>| ..color("red")
>| ) {
>| text("Title:\{model.to_string()}")
>| }
>| button(click=Msg::Increment) { text("+") }
>| button(click=Msg::Decrement) { text("-") }
>| }
// 等价于
let view =
div(fn(){
h1(style = Style()
..font_size(20)
..color("red"), fn(){
text("Title:\{model.to_string()}")
});
button(click=Msg::Increment,fn(){ text("+") })
button(click=Msg::Decrement,fn(){ text("-") })
});
// 底层定义:
// 这里我返回Unit,只是想解释说这里的返回值是自由的,因为这里只是一个语法糖,并不是一定要构建个什么出来。
fn div(style ?: Map[String,String], children : ()->Unit)->Unit{
}
优点:
- 简单直观,易于实现
- 参考 Kotlin 的尾函数调用外置的风格,学习成本较低
- 参考 Moonbit 的多行字符串风格,可以有效隔离一些语法冲突,易于扩展
缺点:
- 不能完全做到 kotlin 那种隐式上下文的类型安全
比如 compose 的 LazyColumn 内,可以调用
item(index) { }
,而这个 item 函数,其实是 LazyColumn 的 content 回调函数提供的上下文。
我仔细考虑了一下,moonbit 可能不适合引入这种隐式上下文的能力 - 具有一定污染性,需要成片的使用,这会导致使用
>|
的时候,语法被一定程度被限制,导致使用的时候有些内容需要抽离>|
区域,会有一些麻烦最好的方案是不使用
>|
头,所以有以下1.1
方案
语法方案 1.1:
类似 html 语法,但底层原理和 1.0
类似,最后一个必要参数如果是 函数,那么可以缺省foo(...,fn(){})
:
- 使用
<foo>{}
替代foo(fn(){})
- 或者使用
<foo ...>{}
替代foo(...,fn(){})
let view =
<div> {
<h1 style = Style()
..font_size(20)
..color("red")> {
text("Title:\{model.to_string()}")
}
<button click=Msg::Increment> { text("+") }
<button click=Msg::Decrement> { text("-") }
}
优点:
- 参考了 html 语法,易于学习
- 没有
1.0
大范围污染的问题
缺点:
- 使用
<
/>
这些符号,可能会和 大于号、小于号 混淆
语法方案 2.0:
尝试构建一个 Tree
let tree =
>| Div {
>| H1 {
>| style: Style()
>| ..font_size(20)
>| ..color("red"),
>| text("Title:\{model.to_string()}")
>| }
>| Button {click:Msg::Increment, text("+") }
>| Button {click:Msg::Decrement, text("-") }
>| }
// 等价于
let view = Div::{
..Div::default(),
children: [
H1::{
..H1::default(),
style: Style()
..font_size(20)
..color("red")
children: [
text("Title:\{model.to_string()}") // 等价于 Text::{content="Title:\{model.to_string()}"}
]
},
Button::{
..Button::default(),
click: Msg::Increment, children: [ text("+") ]},
Button::{
..Button::default(),
click: Msg::Decrement, children: [ text("-") ]},
]
}
// 底层定义:
// 1. 需要明确要求 `struct` 有 `children:Array[?]` 这样的结构;
// 1. 如果 `struct` 提供了 Default 的实现,那么会进行调用,否则需要提供全量的属性
struct Div {
style : Map[String, String]
children : Array[HTMLElement]
} derive(Default)
优点:
- 本质是在构建一个 Tree,原理简单易懂,和原本的语法差别不大,只是将 children 属性作为最后一个属性,省去了一些书写
- 理论上同样可以复用
1.0
和1.1
的语法
缺点:
- 需要隐式地提供一个
children
属性,或者可以改成使用trait TreeNode
,但是目前TreeNode
好像不能提供泛型? - 目前 moonbit 没有联合类型,所以 children 也无法做到混合多种
enum/struct/trait
语法方案 3.0
提供宏插件,难度极高,需要极强的可扩展性,允许编译插件将“字符串”编译成 moobit 的 AST,同时提供 sourcemap 对象、高亮结构,使得 AST 可以反向映射回到源码片段。
let view = #HTML(
#| <div> {
#| <h1 style="font-size:20px;color:red;">
$| Title:\{model.to_string()}
#| </h1>
$| <button click=\{Msg::Increment}>+</button>
$| <button click=\{Msg::Decrement}>-</button>
#| </div>
,
options: Options() // 这里理论上可以提供 Options ,但是要求都是常量
)