编程界的“二向箔”——Dart元编程

简介: 降维打击

作者:闲鱼技术-余玠
阅读过《三体》的同学肯定知道“降维打击”,从更高维度看问题,直接将对手KO。今天我们闲聊一下编程界的“二向箔”——元编程。

1. 什么是元编程

我们听过了太多太多的名词,耳朵似乎都有点名词麻痹症了。比如,有些名词为了装x(比如笔者的文章标题...)或者其本身的意义难以定义,就会加上一些似乎闪烁着光芒的前缀——如meta。计算机软件这行业就有meta data, meta model, meta programming。
今天我们装x的主角就是meta programming——元编程。
其实网络上也能搜出很多相关的文章,对于该词的定义参考wikipedia的一句话:

Metaprogramming is a programming technique in which computer programs have the ability to treat other programs as their data. It means that a program can be designed to read, generate, analyze or transform other programs, and even modify itself while running. In some cases, this allows programmers to minimize the number of lines of code to express a solution, in turn reducing development time. It also allows programs greater flexibility to efficiently handle new situations without recompilation.

简而言之,就是将程序作为数据,可以用于操作程序或者自身,而实现某些能力,比如将运行时工作移到编译时。
按照编译器发展的进程,元编程可实现如代码替换(通过宏实现),泛型编程(从宏演变而来,脱离类型,高度抽象逻辑,可减少代码量),或者在更高级的语言中,运行时通过内省/反射机制来操作代码逻辑,或者随着编译过程的解耦和开放,可以实现在中间语言阶段(AST,IL),操作语法树和中间语言,实现更可扩展性的能力。
元编程的几个阶段

Dart做为一门现代高级语言,除了模板化能力,也能基于中间语言来操作代码。本篇文章主要讨论如何基于其中间语言(dill),通过AST树的操作来进行元编程,并实现一些现有dart语法本身实现不了的能力。并且这种实现在编译时,对于程序在运行时的性能几乎没有影响。

2. Dart中的元编程简介

2.1 背景知识

我们知道,几乎任何语言中,代码在 "编译"(解释型语言在运行时也有编译的过程) 的过程中,都会生成一种树状的中间状态,这就是 AST(抽象语法树)。AST 描述了每个表达式/语句中的子语句的执行顺序和执行逻辑,因而它可以被很方便地翻译成目标代码 。基于这种抽象,能合理的将编译器拆分为三阶段:FrontEnd,Optimizer, Backend,从而实现能兼容各种语法形式的语言,更易于迁移并兼容不同架构的cpu。见下图:

3阶段编译

这三个阶段围绕这IL(intermediate language)进行。IL语言隔离了语法(能轻易适配各种新的语种),平台架构等的差异性。

3阶段编译架构

2.2 Dart的编译流程

Dart的设计也类似,其中间语言就是Dill。不同的是,这里的Dill不像java的IL或者DotNet的IL那样开放出来可以直接编写,而是通过程序的方式操作实现。
dart编译器

这种方式其实就是基于AST库对Dill进行manipulation。
dill manipulation

这个库内的组件包含了所有AST树涉及到的节点的定义和访问,将类型,函数,语句,声明,表达式等编程基本概念抽象成了对象。基于这些对象我们可以遍历整个AST树, 或者生成新的类型和函数,插入代码语句,实现新的逻辑。

2.3 几个栗子

入门其实很简单,看一下例子代码就可以啦。

2.3.1. 比如以下语句定义了一个新的Map变量,并且调用了它的构造函数:
//组装参数
Arguments mapFromArgs = Arguments.empty();
mapFromArgs.positional.add(MapLiteral([], keyType:keyInterType));
//调用from构造函数
StaticInvocation mapConstructor = StaticInvocation(MapFromFactoryProc, mapFromArgs);
//声明一个名字为jsonMap的Map类型变量
VariableDeclaration mapInstDecl = VariableDeclaration("jsonMap", type:mapInterType);
//相当于var jsonMap = new Map();
VariableSet set_mymap = VariableSet(mapInstDecl, mapConstructor);
2.3.2. 创建函数体

函数体其实就是Block。

Block bodyStatements = Block(List<Statement>());
bodyStatements.addStatement(mapInstDecl);
bodyStatements.addStatement(ExpressionStatement(inst));
2.3.3 创建函数

这个例子是参考某个函数的声明形式来创建新函数,篇幅所限,一些参数从略。

static Procedure createProcedure(Procedure referProcedure ,Statement bodyStatements, DartType returnType) {
      FunctionNode functionNode = new FunctionNode(bodyStatements,
          //...参数从略
      );
      Procedure procedure = new Procedure(
        Name(referProcedure.canonicalName.name, referProcedure.name.library),ProcedureKind.Method, functionNode,
        //...参数从略
      );
      return procedure;
  }
//调用函数创建,并添加到类定义中
Procedure overridedToJsonFunc = createProcedure(jsonTypes.StaticBaseToJsonProc, bodyStatements, InterfaceType(mapClass));
youClazz.addMember(overridedToJsonFunc);
2.3.4 其他

基于AST还可以创建复杂的表达式和语句,如ForInStatement(for...in循环)等,语句和表达式还可以通过ExpressionStatement和BlockExpression互相转化。更多的技巧可参考AST的定义。

2.4 如何调试

编辑好的Dill似乎是个黑盒,除了看日志或者看异常堆栈,并不能进行单步调试,给开发带来了一些困难。但Dart提供了已将将kernel dill转成可阅读的文本的工具,方便调试:

$DartHome/dart ../../pkg/vm/bin/dump_kernel.dart /your/dill/file/path /output/dill/text/file.text

打开的text文件是类似于这样的:

    static method  __from_Json1(core::Map<dynamic, dynamic> m) → aop2::UserDataT {
      aop2::UserDataT inst;
      inst = new aop2::UserDataT::•();
      inst.name = m.[]("name") is core::String ?{core::String} m.[]("name") : null;
      inst.city = m.[]("city") is core::String ?{core::String} m.[]("city") : null;
      inst.age = m.[]("age") is core::int ?{core::int} m.[]("age") : null;
      inst.squres = m.[]("squres") is core::double ?{core::double} m.[]("squres") : null;
      inst.sex = m.[]("sex") is core::bool ?{core::bool} m.[]("sex") : null;
      inst.houses = m.[]("houses") is core::Map<dynamic, dynamic> ?{core::Map<dynamic, dynamic>} block {
        core::Map<core::String, core::String> mymap;
        mymap = col::LinkedHashMap::from<core::String, core::String>(<core::String, core::String>{});
        for (core::String item in (m.[]("houses") as core::Map<dynamic, dynamic>).keys) {
          mymap.[]=(item, (m.[]("houses") as core::Map<dynamic, dynamic>).[](item) is core::String ?{core::String} (m.[]("houses") as core::Map<dynamic, dynamic>).[](item) : null);
        }
      } =>mymap : null;
      return inst;
    }

3. 应用畅想

基于Dill的Manipulation,我们可以实现往代码中注入新的逻辑。比如闲鱼科技之前开源的AOP库AspectD的原理就是通过加载dill文件生成AST,然后遍历AST,寻找到已经annotation到的函数或语句,在dill层面操作后又生成dill参加到编译器后续的流程,最终实现了AOP。

类似的,我们知道Dart对于Json解析操作不是很方便,jsonDecode不能直接生成业务对象,而是Map或者List之类的集合,还需要用户自己手动代码遍历这些集合并装载对象。虽然官方开源了一个基于Source_gen的方案,但使用上也不友好(还有其他一些方案如Dason等,但依赖于Mirror,详见这里的比较)。其实遍历Map或者List并装配对象这样的操作逻辑很简单,我们也可以通过Dill Manipulation来做。

其使用方式简便,举例如下:

@JsonModel()
class UserData {
  String name;
  String city;
  UserData son;
}

void main(){
    var str = '''
    {
      "name": "Jim",
      "city": "hangzhou",
      "son":{
        "name": "Kong",
        "city":"Hangzhou"
      }
    }
    ''';

    UserData userData = JsonDrill.fromJson<UserData>(jsonDecode(str));
    var map = JsonDrill.toJson<UserData>(userData);
    print("$map");
  }

更深入的思考一下,Dart现有的mirror能力至今未推荐使用(原因分析可参考这篇文章),那我们是否可以基于Dill Manipulation实现一个简单轻量的LiteMirror库呢?并基于这个LiteMirror库实现更上层的Json解析和AOP甚至Hook能力?

当然,聪明的你或许已经发现,Dill Manipulation不可避免的要对编译流程进行定制,这就要求比如在Flutter环境中,需要对Flutter Tool进行定制,以加入Dill再编辑的环节。剧透一下,闲鱼科技目前就已经实现了Json解析器,正在准备开源中,敬请期待

相关文章
|
7月前
|
缓存 API 数据库
Python黑魔法解密:深入探究元编程
【2月更文挑战第9天】在Python世界中,元编程是一种强大而神秘的技术。通过元编程,我们可以在运行时动态地创建、修改和操作代码,为程序增加灵活性和扩展性。本文将带您深入探究Python中的元编程,揭示其中的黑魔法,并展示其在实际应用中的威力。
53 3
|
5月前
|
程序员 C++
C++模板元编程入门
【7月更文挑战第9天】C++模板元编程是一项强大而复杂的技术,它允许程序员在编译时进行复杂的计算和操作,从而提高了程序的性能和灵活性。然而,模板元编程的复杂性和抽象性也使其难以掌握和应用。通过本文的介绍,希望能够帮助你初步了解C++模板元编程的基本概念和技术要点,为进一步深入学习和应用打下坚实的基础。在实际开发中,合理运用模板元编程技术,可以极大地提升程序的性能和可维护性。
|
6月前
|
Java 开发者 API
使用Groovy探索动态语言编程的魅力
【6月更文挑战第6天】Groovy,一种运行在JVM上的动态语言,融合了Java的全部功能与动态语言的便利性。其动态特性包括:动态类型(运行时确定变量类型)、动态方法调用(使用字符串作为方法名)和动态脚本执行(运行时加载执行脚本)。Groovy适用于简化配置、构建DSL和实现灵活插件系统,提供高效开发体验。通过Groovy,开发者能更好地发掘动态编程的魅力。
|
7月前
|
安全 算法 Swift
【Swift开发专栏】Swift中的泛型编程
【4月更文挑战第30天】Swift的泛型允许开发人员创建可重用的组件,操作多种数据类型,无需针对每种类型编写特定代码。本文分为三部分:1) 泛型基本概念,包括类型参数、泛型函数和类型;2) Swift中的泛型实现,如类型推断、关联类型和泛型约束;3) 泛型编程最佳实践,如明确使用场景、选择有意义的类型参数名称和避免过度泛化。通过理解并应用这些概念,开发者能编写出更灵活、安全和高效的代码。
67 2
|
7月前
|
存储 算法 编译器
【C++ 泛型编程 进阶篇】C++模板元编程深度解析:探索编译时计算的神奇之旅
【C++ 泛型编程 进阶篇】C++模板元编程深度解析:探索编译时计算的神奇之旅
1108 1
|
7月前
|
Rust 安全 开发者
Rust中的元编程概念与应用
本文将深入探讨Rust编程语言中的元编程概念,包括宏、特性、元组和元函数等,并展示它们在Rust中的实际应用。元编程允许开发者在编译时操纵代码,实现代码生成、条件编译、类型检查等高级功能。通过本文的讲解,读者将能够更好地理解元编程在Rust中的作用,并学会如何在项目中应用元编程技术。
|
设计模式 Dart uml
抽象工厂模式-原理到实战应用(基于Dart语言)
抽象工厂模式-原理到实战应用(基于Dart语言)
111 0
|
Dart JavaScript 前端开发
《深入浅出Dart》流程控制
流程控制 Dart语言提供了一套丰富的控制流语句,包括各种条件语句和循环语句,让你可以实现各种复杂的逻辑。 1. 条件语句 Dart中的条件语句主要有if和else。使用if和else可以根据特定的条件执行特定的代码。
55 0
|
Rust
Rust 语言基础 | 学习语言都应该快速得出印象
Rust 语言基础 | 学习语言都应该快速得出印象
170 0
Rust 语言基础 | 学习语言都应该快速得出印象
|
缓存 前端开发 API
ReactiveCocoa 进阶,轻松搞定函数式编程框架
函数式编程已经变得越来越流行,而且也有很大的优势,作为iOS开发者,函数式编程框架**ReactiveCocoa**到底怎么使用呢, 接下来我们来深入介绍**ReactiveCocoa**及其在**MVVM**中的用法。
199 0