背景和动机
- 产品中临时类型爆炸
- 临时类型不方便互相赋值
- PageOf 不好表达
参考
- OutSystems 的 Record 类型
- Rescript 的 Record 类型
- 匿名数据结构即平时口语里说的
Record
类型,和炜昕讨论,为了降低用户的理解成本,复用“数据结构”这个词。 - 另外注意口语里说的
Record
类型和 TypeScript 的 Record 类型也有区别,文中用 TypeScript 表达语义的地方统一用AStructure
(AnonymousStructure)表示
详细设计
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 操作
结论
由评审人员最后补充。