From 4a2c54ef3cbf1baaf1e76be620c57ef7a9723b45 Mon Sep 17 00:00:00 2001 From: Robert Borghese <28355157+RobertBorghese@users.noreply.github.com> Date: Thu, 5 Jan 2023 19:36:48 -0500 Subject: [PATCH 1/2] Create 0000-trailing-block-expr.md --- proposals/0000-trailing-block-expr.md | 152 ++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 proposals/0000-trailing-block-expr.md diff --git a/proposals/0000-trailing-block-expr.md b/proposals/0000-trailing-block-expr.md new file mode 100644 index 0000000..1a85ca2 --- /dev/null +++ b/proposals/0000-trailing-block-expr.md @@ -0,0 +1,152 @@ +# Trailing Block Expressions + +* Proposal: [HXP-NNNN](NNNN-filename.md) +* Author: [Robert Borghese](https://github.com/RobertBorghese) + +## Introduction + +Allow block expressions to be appended to macro calls as final argument. +```haxe +macro function ifNotNull(expr: Expr, trail: TrailingExpr): Expr { + return macro { + final val = $expr; + if(val != null) { + $trail; + } else null; + } +} + +// --- + +ifNotNull(generateData()) { + trace(val); +} +``` + +## Motivation + +This feature has two driving motivations: clarify, optimize, and simplify a somewhat common pattern in Haxe metaprogramming, and open oppurtunities for better Haxe integrated DSLs. + +### Simplify + +There are currently two ways to replicate this functionality currently. + +First is by placing the block within the macro call: `myCall({ ... })`. This syntax is identical to a normal function with a block expression being used as a value, so it could be confusing. Not to mention it's uglier and clunkier to write. The proposed new syntax clarifies the expression is being processed through a macro, as it cannot be used elsewhere. + +The second method is with metadata: `@myMeta { ... }`. It looks nearly identical to the proposed syntax, but modifying the attached expression requires iterating through the entire project's AST to find the metadata using global `@:build`. Furthermore, metadata is untyped, so there is no namespace/import control, and no way to easily find the source reading the code. Meanwhile, a macro function is imported, typed, and can be sourced easily with IDE tools, so it is an objective improvement. + +### DSL + +In addition to the technical capabilities provided to macros, allowing the Haxe parser to accept trailing block expressions becomes a feature within itself. While not the conclusive answer to all DSL support, it provides a fast, easy, Haxe-ified, type-safe alternative. + +In fact, a decent number of newer languages' support for "DSL" seemingly boils down to allowing the parser to accept trailing blocks. See the "Macros to implement DSLs" section on this [official Nimlang blog](https://nim-lang.org/blog/2021/11/15/zen-of-nim.html), or check out this [Kotlin tutorial](https://kotlinlang.org/docs/type-safe-builders.html#scope-control-dslmarker) and [Kotlin article](https://medium.com/kotlin-and-kotlin-for-android/kotlin-dsl-coding-a-dsl-6-ee355be81106). + +```haxe +// how an html builder in Haxe could look +return html { + head { + title { "My Page"; } + } + body { + div { + p(style="mystyle") { + "Hello World"; + } + + button(onclick=Statics.whenButtonPress) { + "Click me"; + } + } + } +} +``` + +
+ +## Detailed design + +### Structure Changes + +A new `ExprDef` case should be added. +```haxe +ETrailingBlock(e: Expr, blockExpr: Expr) +``` + +And a new typedef named `TrailingExpr` should be added to `haxe.macro.Expr`. This is so a macro function can choose to accept a trailing block vs normal expression, while also ensuring `TrailingExpr`s are compatible with macro reification. +```haxe +typedef TrailingExpr = Expr; +``` + +
+ +### Typing Rules + +A trailing block is a metaprogramming feature. All instances of `ETrailingBlock` should be converted prior to the typing phase either using macro functions or `@:build` macros. If the typer encounters `ETrailingBlock`, an error is thrown (though, other alternatives to how to handle this are listed below). + +As a result, there is no `TypedExpr` equivalent for `ETrailingBlock`. + +
+ +### Declaration Rules + +For a macro function to accept a trailing expression, the last argument must be `TrailingExpr`. +```haxe +macro function useTrail(num: Int, e: Expr, trail: TrailingExpr); // valid +macro function useTrail(trail: TrailingExpr, num: Int); // error: TrailingExpr must be last argument +``` + +`TrailingExpr` is incompatible with the new rest argument syntax (`...`). However, a macro function that uses both rest arguments and a trailing block expression can be created by making the second to last argument an `Array`, and the final argument `TrailingExpr`. +```haxe +macro function f(trail: TrailingExpr, ...e: Expr); // error: TrailingExpr must be last argument +macro function f(trail: TrailingExpr, args: Array); // error: TrailingExpr must be last argument + +macro function f(e: Expr, args: Array); // macro function with Expr rest arguments +macro function f(args: Array, e: Expr); // macro function that does NOT accept rest arguments +macro function f(args: Array, trail: TrailingExpr); // macro function that acceps rest arguments AND trailing block +``` + +`TrailingExpr` is only allowed in macro functions. +```haxe +// error: haxe.macro.Expr.TrailingExpr only allowed in macro functions. Use haxe.macro.Expr instead. +function f(trail: TrailingExpr); +``` + +
+ +### Syntax Rules + +A trailing block can be placed after any function call. Simply place a block expression directly after the closing parenthesis of the call. +```haxe +myCall(12, "string") { + // place block content here +} +``` + +If the call expression does not have any arguments (besides the `TrailingExpr`), the parenthesis can be omitted. +```haxe +onlyOneArg { + // place block content here +} +``` + +
+ +## Impact on existing code + +A new case is added to `haxe.macro.ExprDef`, so will break some macro code. + +The new syntax itself should not cause any issues with existing code, however. + +## Drawbacks + +Maybe certain syntax errors might not trigger the same? But the restrictive nature of the new syntax should ensure it's not an issue. + +## Alternatives + +See Motivation. + +## Unresolved questions + +Perhaps instead of using `TrailingExpr`, the trailing block can be used on any macro with an `Expr` as the final argument? + +Maybe the trailing expression can be used in normal functions, but when passing a function for the last argument similar to Kotlin? From 38eb06610166996fcc461ec2aab73a023f2fe2be Mon Sep 17 00:00:00 2001 From: RoBBoR <28355157+RobertBorghese@users.noreply.github.com> Date: Fri, 6 Jan 2023 09:11:57 -0500 Subject: [PATCH 2/2] Unresolved questions - `TrailingExpr` as abstract? --- proposals/0000-trailing-block-expr.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/proposals/0000-trailing-block-expr.md b/proposals/0000-trailing-block-expr.md index 1a85ca2..74001bd 100644 --- a/proposals/0000-trailing-block-expr.md +++ b/proposals/0000-trailing-block-expr.md @@ -147,6 +147,16 @@ See Motivation. ## Unresolved questions -Perhaps instead of using `TrailingExpr`, the trailing block can be used on any macro with an `Expr` as the final argument? +### Abstract vs Typedef -Maybe the trailing expression can be used in normal functions, but when passing a function for the last argument similar to Kotlin? +Instead of a typedef, `TrailingExpr` could be an abstract? That way some helpful functions can be provided: +```haxe +abstract TrailingExpr(Expr) to Expr { + // TrailingExpr is always an EBlock, so unwrap and return its contents + public function contents(): Array { ... } +} +``` + +### Trailing Lambdas + +Perhaps trailing blocks should function as last argument lambdas for normal functions, similar to how Kotlin works?