Skip to main content

匿名数据结构

· 10 min read

背景和动机

  • 产品中临时类型爆炸
  • 临时类型不方便互相赋值
  • PageOf 不好表达

参考

详细设计

1. 定义

匿名数据结构是一种不带名字即时定义的类型,和具名数据结构类似,由固定数量的属性组成,每个属性都有自己的类型。

主要场景,用于即时定义某个变量或返回值:

  • 表达数据查询之后的聚合结果,或 Join 内置函数之后的结果。
  • 需要一个返回复合信息的逻辑,但不想定义新的全局数据结构。

2. 字面表达

下面是一个基本的示例,用户在 IDE 中直接看到的形式如下(:后面的)

let result: { asset: Asset; assetBudget: AssetBudget; totalAmount: Decimal };

3. 类型构造器

匿名数据结构可以理解为由AStructure类型构造器产生。

方案一

有 n 个固定属性的 AStructure 类型,需要有 n * 2 个构造器参数传入。

let result2: AStructure<'asset', Asset, 'assetBudget', AssetBudget, 'totalAmount', Decimal>;
IsSameType<typeof result, typeof result2>; // true

// 支持嵌套定义
let result2: AStructure<'asset', Asset, 'assetBudget', AStructure<'id', Long, 'assetId', Long, 'purchaseReason', String>, 'totalAmount', Decimal>;

方案二

有 n 个固定属性的 AStructure 类型,只需要 n 个构造器参数传入,属性名由构造器参数的类型名决定。

let result2: AStructure<Asset, AssetBudget, Decimal>;

let result2: AStructure<Asset, Asset>; // ?
let result2: AStructure<Asset, Decimal, Decimal>; // 如何表达 totalCount, totalAmount 等不同字段
let result2: AStructure<Asset, AStructure<Long, Long>>; // 如何嵌套

所以在语言上只有方案一,在 IDE 层面可以帮用户做一些快捷填充。

4. 属性的无序性

下面两种类型的属性定义时顺序虽然不一样,但类型相同:

IsSameType<{ asset: Asset; assetBudget: AssetBudget; totalAmount: Decimal }, { assetBudget: AssetBudget; totalAmount: Decimal; asset: Asset }>; // true

但定义时顺序建议在存储时还是保留,主要是用户习惯和映射 UI 时有一定的便利性。

5. 其他特点

由于没有名字,无法定义无限递归的场景,做树型列表那个需要注意:

{ category: Category, children: List<{ category: Category, children: ... }> }

6. 实现原理

采用类型擦除来实现多处定义或属性顺序调整的 AStructure 类型相同。目前 NASL 中的所有类型可以分为:

  • 无参数类型
    • Integer、Long、...
    • Student
    • ColorEnum
  • 带参数类型
    • 无序:AStructure<...>Union<...>
    • 有序:List<T>Map<K, V>

比如如何保证下面两个类型相同的:

let result3: {
int1: Integer;
student: Student;
color: ColorEnum;
subObject: {
id: Long;
name: String;
db1: Double;
};
resultOrError: Result | Error;
list: List<String>;
list2: List<{ id: Long; createdTime: DateTime }>;
map: Map<String, Student>;
map2: Map<String, { id: Long; name: String }>;
};

let result4: {
map: Map<String, Student>;
list: List<String>;
int1: Integer;
color: ColorEnum;
subObject: {
id: Long;
db1: Double;
name: String;
};
resultOrError: Error | Result;
student: Student;
map2: Map<String, { id: Long; name: String }>;
list2: List<{ createdTime: DateTime; id: Long }>;
};

IsSameType<typeof result3, typeof result4>; // true

1、先都统一成类型构造器的形式:

注意,这边属性名用了",是一种字符串字面量类型,要和后面的类型区分开。

let result3: AStructure<
'int1',
Integer,
'student',
Student,
'color',
ColorEnum,
'subObject',
AStructure<'id', Long, 'name', String, 'db1', Double>,
'resultOrError',
Union<Result, Error>,
'list',
List<String>,
'list2',
List<AStructure<'id', Long, 'createdTime', DateTime>>,
'map',
Map<String, Student>,
'map2',
Map<String, AStructure<'id', Long, 'name', String>>
>;

let result4: AStructure<
'map',
Map<String, Student>,
'list',
List<String>,
'int1',
Integer,
'color',
ColorEnum,
'subObject',
AStructure<'id', Long, 'db1', Double, 'name', String>,
'resultOrError',
Union<Error, Result>,
'student',
Student,
'map2',
Map<String, AStructure<'id', Long, 'name', String>>,
'list2',
List<AStructure<'createdTime', DateTime, 'id', Long>>
>;

2、对无序参数类型进行排序:

  • AStructure 按属性名排序、后面的类型跟随
  • UnionType 按类型名排序
let result3: AStructure<
'color',
ColorEnum,
'int1',
Integer,
'list',
List<String>,
'list2',
List<AStructure<'createdTime', DateTime, 'id', Long>>,
'map',
Map<String, Student>,
'map2',
Map<String, AStructure<'id', Long, 'name', String>>,
'resultOrError',
Union<Error, Result>,
'student',
Student,
'subObject',
AStructure<'db1', Double, 'id', Long, 'name', String>
>;

let result4: AStructure<
'color',
ColorEnum,
'int1',
Integer,
'list',
List<String>,
'list2',
List<AStructure<'createdTime', DateTime, 'id', Long>>,
'map',
Map<String, Student>,
'map2',
Map<String, AStructure<'id', Long, 'name', String>>,
'resultOrError',
Union<Error, Result>,
'student',
Student,
'subObject',
AStructure<'db1', Double, 'id', Long, 'name', String>
>;

这里已经可以比较类型了,可以发现两个类型一样。

提醒一下,实际开发需要补齐命名空间(上面为了简化描述,做了省略):

let result3: AStructure<
'color',
app.enums.ColorEnum,
'int1',
nasl.core.Integer,
'list',
nasl.collection.List<nasl.core.String>,
'list2',
nasl.collection.List<AStructure<'createdTime', nasl.core.DateTime, 'id', nasl.core.Long>>,
'map',
nasl.collection.Map<nasl.core.String, app.entities.Student>,
'map2',
nasl.collection.Map<nasl.core.String, AStructure<'id', nasl.core.Long, 'name', nasl.core.String>>,
'resultOrError',
Union<app.structures.Error, app.structures.Result>,
'student',
app.entities.Student,
'subObject',
AStructure<'db1', nasl.core.Double, 'id', nasl.core.Long, 'name', nasl.core.String>
>;

3、要翻译成 Nominal Typing 代码的话,再进一步做个擦除:

先压缩,去除空格、换行和尾逗号

let result3: AStructure<
'color',
ColorEnum,
'int1',
Integer,
'list',
List<String>,
'list2',
List<AStructure<'createdTime', DateTime, 'id', Long>>,
'map',
Map<String, Student>,
'map2',
Map<String, AStructure<'id', Long, 'name', String>>,
'resultOrError',
Union<Error, Result>,
'student',
Student,
'subObject',
AStructure<'db1', Double, 'id', Long, 'name', String>
>;

太长了,就直接生成一个唯一 hash 吧。

我用的是这个库,生成的 Hash 是:54de8c47

方案一:暂不支持子类型(考虑到当前的改造成本)

这样翻译成 Java 代码如下

class AStructure_ac10bef2 {
Double db1;
Long id;
String name;
}
class AStructure_2a019613 {
Long id;
String name;
}
class AStructure_67f3ff1b {
DateTime createdTime;
Long id;
}
class AStructure_54de8c47 {
ColorEnum color;
Integer int1;
List<String> list;
List<AStructure_67f3ff1b> list2;
Map<String, Student> map;
Map<String, AStructure_2a019613> map2;
Object resultOrError;
Student student;
AStructure_ac10bef2 subObject;
}

翻译成 TypeScript 在排序后加上__name保证没有父子类型:

let result5: {
__name: 'AStructure_54de8c47';
color: ColorEnum;
int1: Integer;
list: List<String>;
list2: List<{
__name: 'AStructure_67f3ff1b';
createdTime: DateTime;
id: Long;
}>;
map: Map<String, Student>;
map2: Map<
String,
{
__name: 'AStructure_2a019613';
id: Long;
name: String;
}
>;
subObject: {
__name: 'AStructure_ac10bef2';
db1: Double;
id: Long;
name: String;
};
resultOrError: Error | Result;
student: Student;
};

type IsSubType<A, B> = A extends B ? true : false;

IsSubType<{ __name: 'AStructure_2a019613'; id: Long; name: String }, { __name: 'AStructure_ac10bef2'; db1: Double; id: Long; name: String }>; // false

方案二:支持子类型

用 class 强转是不行了,会报cannot be cast to的错

翻译成 TypeScript 不需要加额外功能:

let result5: {
color: ColorEnum;
int1: Integer;
list: List<String>;
list2: List<{
createdTime: DateTime;
id: Long;
}>;
map: Map<String, Student>;
map2: Map<
String,
{
id: Long;
name: String;
}
>;
subObject: {
db1: Double;
id: Long;
name: String;
};
resultOrError: Error | Result;
student: Student;
};

IsSubType<{ id: Long; name: String }, { db1: Double; id: Long; name: String }>; // true

7. 语法树

复用 StructureProperty 节点

interface AnonymousStructureTypeAnnotation {
concept: 'TypeAnnotation';
typeKind: 'anonymousStructure';
properties: Array<StructureProperty>;
}

以这个嵌套匿名结构为例:

let result2: {
asset: Asset;
assetBudget: {
id: Long;
assetId: Long;
purchaseReason: String;
};
totalAmount: Decimal;
};
{
"concept": "TypeAnnotation",
"typeKind": "anonymousStructure",
"properties": [
{
"concept": "StructureProperty",
"name": "asset",
"typeAnnotation": {
"concept": "TypeAnnotation",
"typeKind": "reference",
"typeNamespace": "app.dataSources.defaultDS.entities.Asset",
"typeName": "Asset"
}
},
{
"concept": "StructureProperty",
"name": "assetBudget",
"typeAnnotation": {
"concept": "TypeAnnotation",
"typeKind": "anonymousStructure",
"properties": [
{
"concept": "StructureProperty",
"name": "id",
"typeAnnotation": {
"concept": "TypeAnnotation",
"typeKind": "primitive",
"typeNamespace": "nasl.core",
"typeName": "Long"
}
},
{
"concept": "StructureProperty",
"name": "assetId",
"typeAnnotation": {
"concept": "TypeAnnotation",
"typeKind": "primitive",
"typeNamespace": "nasl.core",
"typeName": "Long"
}
},
{
"concept": "StructureProperty",
"name": "purchaseReason",
"typeAnnotation": {
"concept": "TypeAnnotation",
"typeKind": "primitive",
"typeNamespace": "nasl.core",
"typeName": "String"
}
}
]
}
},
{
"concept": "StructureProperty",
"name": "totalAmount",
"typeAnnotation": {
"concept": "TypeAnnotation",
"typeKind": "primitive",
"typeNamespace": "nasl.core",
"typeName": "Decimal"
}
}
]
}

缺点

  • 用户创建一个 AStructure 类型,在交互上操作比较多
    • 建议是提供出来已有的 AStructure 类型让用户先选择,没有再自己创建
  • AStructure_{hash}的生成代码的可读性较差

未解决的问题

  • 暂时没有做 intersection 操作

结论

由评审人员最后补充。