1 概述

1.1 背景介绍

仓颉宏是一种编译时代码生成工具,允许开发者操作代码片段(Tokens)并生成新的代码逻辑,从而减少重复代码并提升抽象能力。而LINQ(Language Integrated Query,语言集成查询)是一种DSL(Domain Specific Language,领域特定语言),它是微软.NET框架中的一个关键技术,它允许开发者使用熟悉的编程语言(如C#和Visual Basic)来编写查询。

通过该案例,开发者可以了解体验仓颉宏的基本使用,和使用宏实现一个简单的LINQ语法,加深大家对仓颉宏特性的理解。

1.2 适用对象

  • 个人开发者
  • 高校学生

1.3 案例时间

本案例总时长预计20分钟。

1.4 案例流程

0c7e5abbaeb491525e6975593ee8b968.png

说明:

① 用户登录开发者空间,体验使用仓颉宏;
② 使用仓颉宏实现简单的语音集成查询LINQ。

1.5 资源总览

本案例预计花费总计0元。

资源名称 规格 单价(元) 时长(分钟)
开发者空间 - 云主机 鲲鹏通用计算增强型 kc2 | 4vCPUs | 8G | Ubuntu 免费 20

最新案例动态,请查阅 《仓颉宏实现语言集成查询LINQ》。小伙伴快来领取华为开发者空间,进入云主机桌面版实操吧!

2 操作步骤

2.1 仓颉宏快速入门

2.1.1 宏简介

仓颉宏是一种在编译时进行代码替换的元编程工具,允许开发者通过操作抽象语法树(AST)动态生成或修改代码。为了把宏的调用和函数调用区分开来,在调用宏时使用 @ 加上宏的名称。与运行时注解(如Java注解)不同,宏在编译期展开,无需运行时处理,因此性能更高。其核心特点包括:

  • 输入/输出为Tokens:宏接收代码片段(Tokens类型),处理后返回新的代码片段;
  • 独立宏包声明:宏需定义在通过macro package声明的独立包中,避免与普通函数冲突;
  • 属性与非属性宏:属性宏可接收额外参数(如@Cradle[console|logfile]),非属性宏仅处理代码片段。
2.1.2 相关概念

1、Tokens:

  • 定义:表示代码片段的词法单元序列,包含标识符、运算符、字面量等。
  • 操作:支持拼接、解析为语法树节点(如 parseExpr 解析表达式)。

2、quote 表达式:

  • 功能:用于在宏内构建新的代码片段,支持插值语法$(…)动态嵌入变量或表达式。
  • 示例:
let tokens = quote(  
   arr = $(intList)  // 插值嵌入数组  
   s = $(str)       // 插值嵌入字符串  
);  

3、语法节点:

  • 类型:包括表达式节点(Expr)、声明节点(Decl)等,用于代码结构化解析。
  • 应用:宏可通过解析 Tokens 为语法节点(如 parseDecl)实现复杂逻辑生成,例如自动生成类的属性。
2.1.3 快速入门

我们通过一个简单的示例体验一下仓颉宏。

如下示例代码实现了在调试过程中打印某个表达式的值,同时打印出表达式本身。

let x = 3
let y = 2
@dprint(x)        // 打印 "x = 3"
@dprint(x + y)    // 打印 "x + y = 5"

显然,dprint 不能被写为常规的函数,因为函数只能获得输入的值,不能获得输入的程序片段。但可以将 dprint 实现为一个宏。实现如下:

macro package demo.define
import std.ast.*
public macro dprint(input: Tokens): Tokens {
    let inputStr = input.toString()
    let result = quote(
        print($(inputStr) + " = ")	
        println($(input)))
    return result
}

在解释每行代码之前,先测试这个宏可以达到预期的效果。

首先,登录开发者空间,点击进入桌面进入到云主机。

776fa31ec636ade5a7bfd1be80f89858.png

在云主机桌面打开CodeArts IDE for Cangjie。

71da8eaa14d649a85a68b76c0fdaba7b.png

点击“新建工程”创建仓颉工程,名称定义为demo产物类型选择executable
产物类型说明

  • executable,可执行文件;
  • static,静态库,是一组预先编译好的目标文件的集合;
  • dynamic,动态库,是一种在程序运行时才被加载到内存中的库文件,多个程序共享一个动态库副本,而不是像静态库那样每个程序都包含一份完整的副本。
    fa09d5ce3e9ece182b9c6218a30a7e3d.png
    创建项目后,打开src目录下main.cj文件,替换成以下测试代码(代码替换后爆红可以不用处理,等main.cj和dprint.cj都修改/创建后,直接运行main.cj即可)。
package demo
import demo.define.*
main() {
    let x = 3
    let y = 2
    @dprint(x)
    @dprint(x + y)
}

bfa3a9359405494c20f4ab7da4beefea.png

在src目录下创建一个define 文件夹,并在define文件夹中创建 dprint.cj 文件,将上面宏示例代码内容复制到 dprint.cj

854a302fbd3c1ee73f335524488ae5f3.png

点击右上角运行按钮运行程序,查看运行结果看到dprint宏不仅返回了结果,也把代码片段返回了。

8910d548c48001a5fd711f8e9f732e69.png

选择main.cj,点击Build Cangjie Project,生成main.cj.macrocall文件,打开该文件可以看出到宏展开后的代码:

3d10f3035e68dba32ba3554e56138f4c.png

接下来我们通过dprint宏代码来了解一下一个简单的宏都是由哪些部分组成:

第 1 行:macro package demo.define

宏必须声明在独立的包中(不能和其他 public 函数一起),含有宏的包使用 macro package 来声明。这里在项目目录下声明了一个名为 define 的宏包。

第 2 行:import std.ast.*

实现宏需要的数据类型,例如 Tokens 和后面会讲到的语法节点类型,位于仓颉标准库的ast 包中,因此任何宏的实现都需要首先引入 ast 包。

第 3 行:public macro dprint(input: Tokens): Tokens

在这里声明一个名为 dprint 的宏。由于这个宏是一个非属性宏(之后会解释这个概念),它接受一个类型为 Tokens 的参数。该输入代表传给宏的程序片段。宏的返回值也是一个程序片段。

第 4 行:let inputStr = input.toString()

在宏的实现中,首先将输入的程序片段转化为字符串。在前面的测试案例中,inputStr 成为"x" 或 “x + y”。

第 5-7 行:let result = quote(…)

这里 quote 表达式是用于构造 Tokens 的一种表达式,它将括号内的程序片段转换为 Tokens。在 quote 的输入中,可以使用插值 $(…) 来将括号内的表达式转换为 Tokens,然后插入到 quote 构建的 Tokens 中。对于以上代码,$(inputStr) 插入 inputStr 字符串的值(包含字符串两端的引号),$(input) 插入 input,即输入的程序片段。因此,如果输入的表达式是 x + y,那么形成的Tokens为:

print("x + y" + " = ")
println(x + y)

第 8 行:return result

最后,将构造出来的代码返回,这两行代码将被编译,运行时将输出 x + y = 5。

体验了仓颉宏的简单使用后,下面我们使用宏做一个DSL的案例。

2.2 仓颉宏实现LINQ

2.2.1 需求分析

使用仓颉宏实现一个简单的 DSL(Domain Specific Language,领域特定语言)——LINQ。LINQ(Language Integrated Query,语言集成查询)是微软 .NET 框架的一个组成部分,它提供了一种统一的数据查询语法,允许开发者使用类似 SQL 的查询语句来操作各种数据源。在仓颉中,可通过宏将类似语法转换为高效编译期代码。

目标语法:

from <variable> in <list> where <condition> select <expression>

其中,from、in、where、select是关键字,variable 是一个标识符,list、condition 和 expression 都是表达式。语法示例:

# 效果:筛选出1-20中3的倍数,然后输出其平方
from x in 1..=20 where x % 3 == 0 select x * x
2.2.2 实现方案

在云主机桌面打开CodeArts IDE for Cangjie。

71da8eaa14d649a85a68b76c0fdaba7b.png

点击新建工程创建仓颉工程,名称定义为linqdemo产物类型选择executable

1fda428a547af0c01b6de6bde1ba0523.png

在src目录下创建一个define 文件夹,并在 define 文件夹中创建 linq.cj 文件,将下面代码内容复制到 linq.cj

基于2.2.1中目标语法的要求

from \<variable\> in \<list\> where \<condition\> select \<expression\>

我们实现宏的策略是先后提取标识符和表达式,同时检查中间的关键字是正确的。最后,生成由提取部分组成的查询结果。实现如下:

macro package linqdemo.define
import std.ast.*
public macro linq(input: Tokens) {
    // 定义标准错误提示模板
    let syntaxMsg = "Syntax is \"from <attrib> in <table> where <cond> select <expr>\""
    
    // ----------------- 语法结构校验 -----------------
    // 校验from关键字
    if (input.size == 0 || input[0].value != "from") {
        diagReport(DiagReportLevel.ERROR, input[0..1], syntaxMsg,
                   "Expected keyword \"from\" here.")
    }
    // 校验迭代变量标识符(如x)
    if (input.size <= 1 || input[1].kind != TokenKind.IDENTIFIER) {
        diagReport(DiagReportLevel.ERROR, input[1..2], syntaxMsg,
                   "Expected identifier here.")
    }
    let attribute = input[1]  // 捕获迭代变量名(如x)
    // 校验in关键字
    if (input.size <= 2 || input[2].value != "in") {
        diagReport(DiagReportLevel.ERROR, input[2..3], syntaxMsg,
                   "Expected keyword \"in\" here.")
    }
    // ----------------- 表达式解析 -----------------
    var index: Int64 = 3
    // 解析数据源表达式(如1..=20)
    let (table, nextIndex) = parseExprFragment(input, startFrom: index)
    // 校验where关键字
    if (nextIndex == input.size || input[nextIndex].value != "where") {
        diagReport(DiagReportLevel.ERROR, input[nextIndex..nextIndex+1], syntaxMsg,
                   "Expected keyword \"where\" here.")
    }
    index = nextIndex + 1  // 移动索引到where之后
    
    // 解析条件表达式(如x % 3 == 0)
    let (cond, nextIndex2) = parseExprFragment(input, startFrom: index)
    
    // 校验select关键字
    if (nextIndex2 == input.size || input[nextIndex2].value != "select") {
        diagReport(DiagReportLevel.ERROR, input[nextIndex2..nextIndex2+1], syntaxMsg,
                   "Expected keyword \"select\" here.")
    }
    index = nextIndex2 + 1  // 移动索引到select之后
    
    // 解析输出表达式(如x * x)
    let (expr, _) = parseExprFragment(input, startFrom: index)

    // ----------------- 代码生成 -----------------
    return quote(
        // 生成遍历逻辑:for (x in 1.to(20))
        for ($(attribute) in $(table)) {
            // 插入条件判断:if (x % 3 == 0)
            if ($(cond)) {
                // 插入输出表达式:println(x * x)
                println($(expr))
            }
        }
    )
}

8708aa5f89ef79953dcea04178b91696.png

在main.cj中调用linq宏(代码替换后爆红可以不用处理,等main.cj和dprint.cj都修改/创建后,直接运行main.cj即可)。

package linqdemo
import linqdemo.define.*

main() {
    println("筛选出1-20中3的倍数,然后返回其平方:")
    @linq(from x in 1..=20 where x % 3 == 0 select x * x)
}

这个例子从 1, 2, … 20 列表中筛选出3的倍数,然后返回其平方。输出结果为:

491f6e8c32361e9811b60643299776ed.png

宏展开后的代码同样可以参考2.1.3快速入门生成main.cj.macrocall文件查看。通过该案例可以看出,宏的实现的很大部分用于解析并校验输入的 tokens,这对宏的可用性至关重要。

至此,使用仓颉宏实现一个简单的LINQ案例就完成啦。
如果想要了解更多仓颉宏及仓颉编程语言知识可以访问: https://cangjie-lang.cn/
如果想要体验更多仓颉示例可以访问:https://gitcode.com/Cangjie/Cangjie-Examples

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐