如何创造一门上万人使用的语言
如果无法正常显示,请先停止浏览器的去广告插件。
1. 如何创造一门上万人使用的语言
腾讯问卷DSL实践之路
李泽帆 LZANE 腾讯CDC高级研发工程师
2. 腾讯CDC
产品技术团队
LZANE - 李泽帆
3. 1. 我们做了一个什么东西?
目录
2. 适合DSL落地的场景
3. 编译器实现
4. 语法设计
5. 配套设施
4. 01
我们做了一个什么东西?
5. 腾讯问卷自定义逻辑
6. 腾讯问卷自定义逻辑
7. 腾讯问卷自定义逻辑
8. 腾讯问卷自定义逻辑
9. 腾讯问卷自定义逻辑
DSL语句 功能效果
if Q1A1 then show Q2 第1题选了第一个选项,显示第二题
if Q1A1 and Q2A2 then show Q3 第一题选了第一个选项,并且第二题选中了第二个选项,显示第三题
if Q1A1 then branch from Q1 to END 第一题选了第一个选项,跳转到结束页(甄别题)
random show 1 from Q1~3 第1-3题中随机抽取1道题显示
shuffle Q1~3 第1-3题随机排序
replace "XXX" in Q2 title with Q1 第二道题题目中的XXX文本,替换为第一道题的答案
10. 腾讯问卷自定义逻辑
11. 02
适合DSL落地的场景
12. 复杂的问卷逻辑
13. 定制问卷
定制开发
编译构建
构建产物
• 每份定制问卷花费3人天,花费大量研发资源
14. 一份定制问卷制作的故事
接着第三题问使用过哪些平台,选项是各种各样的平台,包
括数学、语文、英语等,这里同一种类的选项要随机排序,
比如说数学1、2、3要随机出现; 然后选择综合就要显示下面
第一组题目,选择数学显示下面第二组题目,依此类推;但
是怕用户用户需要答的题目太多,这几组题目一个用户只会
随机出现2组,出现的概率为1:5:2:10,能理解么?
enen,这些还好
(卧槽,又来)好啊,说一下这次的需求
好的,这次是一份调查K12补习班的问卷,首先会在第一题询
问有没有孩子,如果没有孩子,就直接跳转到结束页;然后
第二题询问小孩上过补习班没,没有也跳转到结束页。
Hey, 帮我搞一份定制问卷
15. 一份定制问卷制作的故事
这个地方没懂
这个地方没懂
这个地方没懂
这个地方没懂
这个地方没懂
这个地方没懂
这个地方没懂
这个地方没懂
这个地方没懂
这个地方没懂
好的,那我再解释一下
好的,那我再解释一下
好的,那我再解释一下
好的,那我再解释一下
好的,那我再解释一下
好的,那我再解释一下
好的,那我再解释一下,就是如果不做限制,那用户如果数
学、语文、音乐都选了,那问卷就会变得很长,所以我们一
个用户只会随机出现2组题目,然后按照概率出现;然后接下
来这道题目过没有选数学的选项的话,就会显示为什么不上
数学这道题;然后后续对每个平台的打分,比如说音乐1小于
3分时,就显示询问不喜欢的原因这道题。这些能理解不?
16. 沟通成本巨大
领域专家清楚的知道逻辑细节,但他不会写代码
17. 定制问卷的全流程
用研 用研 > 开发 开发 测试 开发 用研
编写问卷 沟通需求 理解并开发 测试逻辑 构建并发版 投放问卷
18. 定制问卷的全流程
用研 用研 > 开发 开发 测试 开发 用研
编写问卷 沟通需求 理解并开发 测试逻辑 构建并发版 投放问卷
涉及的人员多,沟通成本大
19. 定制问卷的全流程
用研 用研 > 开发 开发 测试 开发 用研
编写问卷 沟通需求 理解并开发 测试逻辑 构建并发版 投放问卷
重复性工作多,每次都要重新定制
20. 定制问卷的全流程
用研 用研 > 开发 开发 测试 开发 用研
编写问卷 沟通需求 理解并开发 测试逻辑 构建并发版 投放问卷
每次都要重新构建,希望能从“构建时”后移到“运行时”
21. 定制问卷的全流程
用研 用研 > 开发 开发 测试 开发 用研
编写问卷 沟通需求 理解并开发 测试逻辑 构建并发版 投放问卷
用研 用研 用研
编写问卷 DSL 投放问卷
22. 适合DSL的落地场景
•
•
•
•
•
重复性工作多
沟通成本大,参与角色多
“构建时”后移到“运行时”
领域专家很清楚逻辑细节
过多的GUI操作
23. 适合DSL的落地场景
GUI普通逻辑
DSL
VS
243次鼠标操作
1行DSL
24. 03
DSL编译器实现
25. 编译器
代码
编译器
Compiler
?
26. 编译器
c++
编译器
Compiler
0011
27. 编译器
ES6
编译器
Compiler
ES5
28. 编译器
解析器 Parser
AST
ES6
转换器
Transformer
AST
代码生成器
CodeGen
ES5
29.
30. 解析器生成器 parser generator
语法 grammar
解析器生成器
parser
generator
解析器 Parser
31. 案例:表达2分钟之前
JS: new Date(Date.now()-2*60*1000)
DSL: 2 mins ago
32. const PEG = require("pegjs")
const grammar = `
Start
= i:Integer _ "mins" _ "ago"
Integer "integer"
= _ [0-9]+
_ "whitespace"
= [ ]*
`
const parser = PEG.generate(grammar)
const result = parser.parse(process.argv[2])
console.log(result)
33. const PEG = require("pegjs")
const grammar = `
Start
= i:Integer _ "mins" _ "ago"
Integer "integer"
= _ [0-9]+
_ "whitespace"
= [ ]*
`
const parser = PEG.generate(grammar)
const result = parser.parse(process.argv[2])
console.log(result)
34. const PEG = require("pegjs")
const grammar = `
Start
= i:Integer _ "mins" _ "ago"
Integer "integer"
= _ [0-9]+
_ "whitespace"
= [ ]*
`
const parser = PEG.generate(grammar)
const result = parser.parse(process.argv[2])
console.log(result)
35. const PEG = require("pegjs")
const grammar = `
Start
= i:Integer _ "mins" _ "ago"
Integer "integer"
= _ [0-9]+
_ "whitespace"
= [ ]*
`
const parser = PEG.generate(grammar)
const result = parser.parse(process.argv[2])
console.log(result)
36. const PEG = require("pegjs")
const grammar = `
Start
= i:Integer _ "mins" _ "ago"
Integer "integer"
= _ [0-9]+
_ "whitespace"
= [ ]*
`
$
[
const parser = PEG.generate(grammar)
const
result
= parser.parse(process.argv[2])
node demo.js "2
mins
ago"
[ [], [ '2' ] console.log(result)
], [ ' ' ], 'mins', [ ' ' ], 'ago' ]
37. const PEG = require("pegjs")
const grammar = `
Start
= i:Integer _ "mins" _ "ago"
Integer "integer"
= _ [0-9]+
_ "whitespace"
= [ ]*
`
$
[
const parser = PEG.generate(grammar)
const
result
= parser.parse(process.argv[2])
node demo.js "2
mins
ago"
[ [], [ '2' ] console.log(result)
], [ ' ' ], 'mins', [ ' ' ], 'ago' ]
38. const PEG = require("pegjs")
const grammar = `
Start
= i:Integer _ "mins" _ "ago"
Integer "integer"
= _ [0-9]+ {
return parseInt(text(), 10)
}
_ "whitespace"
= [ ]*
`
const parser = PEG.generate(grammar)
const result = parser.parse(process.argv[2])
console.log(result)
39. const PEG = require("pegjs")
const grammar = `
Start
= i:Integer _ "mins" _ "ago"
Integer "integer"
= _ [0-9]+ {
return parseInt(text(), 10)
}
_ "whitespace"
= [ ]*
`
$ node demo.js "2 mins ago"
const parser
[ 2, [ ' ' ], 'mins',
[ ' = ' PEG.generate(grammar)
], 'ago' ]
const result = parser.parse(process.argv[2])
console.log(result)
40. const PEG = require("pegjs")
const grammar = `
Start
= i:Integer _ "mins" _ "ago" {
return new Date(Date.now()-i*60*1000)
}
Integer "integer"
= _ [0-9]+ {
return parseInt(text(), 10)
}
_ "whitespace"
= [ ]*
`
const parser = PEG.generate(grammar)
const result = parser.parse(process.argv[2])
console.log(result)
41. const PEG = require("pegjs")
const grammar = `
Start
= i:Integer _ "mins" _ "ago" {
return new Date(Date.now()-i*60*1000)
}
Integer "integer"
= _ [0-9]+ {
return parseInt(text(), 10)
}
_ "whitespace"
= [ mins
]*
$ node demo.js "2
ago"
`
2021-06-20T12:07:03.960Z
const parser = PEG.generate(grammar)
const result = parser.parse(process.argv[2])
console.log(result)
42. const PEG = require("pegjs")
const grammar = `
Start
= i:Integer _ u:Unit _ "ago"{
return new Date(Date.now()-i*u)
}
Unit "unit"
= "mins" {
return 60*1000
}
/ "hours" {
return 60*60*1000
}
Integer "integer"
= _ [0-9]+ {
return parseInt(text(), 10)
}
_ "whitespace"
= [ ]*
`
const parser = PEG.generate(grammar)
const result = parser.parse(process.argv[2])
43. const PEG = require("pegjs")
const grammar = `
Start
= i:Integer _ u:Unit _ "ago"{
return new Date(Date.now()-i*u)
}
Unit "unit"
= "mins" {
return 60*1000
}
/ "hours" {
return 60*60*1000
}
Integer "integer"
= _ [0-9]+ {
$ node demo.js "2
hours
ago"
return parseInt(text(), 10)
2021-06-20T10:38:11.304Z
}
_ "whitespace"
= [ ]*
`
const parser = PEG.generate(grammar)
const result = parser.parse(process.argv[2])
44. https://pegjs.org/online
45. start = SurveyLogic
// skip
_
= [ \\t\\n\\r]*
// Program
SurveyLogic "SurveyLogic"
= _ body:SourceElements? _{
return {
type: "SurveyLogic",
body: body || []
}
}
SourceElements
= se:(_ SourceElement _)* {
return se.reduce(function(acc, element) {
if(element[1]) acc = acc.concat(element[1])
return acc
},[]);
}
SourceElement
= v:(ConditionStatement / Action) _ Comment? {
return v
}
/ Comment {
return null
}
Action "Action"
= HideAction
/ ShowAction
/ ReplaceAction
/ ShuffleAction
/ BranchToAction
46. 编译器的实现
语法 grammar
解析器生成器
parser
generator
DSL
解析器
parser
AST
转换器
transformer
问卷逻辑对象
运行器
runner
问卷
47. 开发顺序?先有DSL or 先有实体模型
DSL
实体模型
问卷逻辑对象
业务逻辑代码
48. 开发顺序?先有DSL or 先有实体模型
DSL
实体模型
问卷逻辑对象
隔离
业务逻辑代码
49. 04
DSL语法设计
50.
51. 取值
条件语句 函数调用
${q-1-KsxT::o-2-gaN6} => show(q-3-oMjX)
选项id
题目id
52. 领域专家优先原则
53.
54.
55.
56.
57.
58.
59. 最终的语法
新语法 旧语法
if Q1A2 then branch from Q1 to END ${q-1-abcd:o-2-abcd} => branchTo(q-1-abcd, END)
if Q4A1 or Q4A2 then show Q5~8 ${q-4-abcd:o-1-abcd} || ${q-4-abcd:o-2-abcd} => show([q-5-abcd, q-6-
abcd, q-7-abcd, q-8-abcd])
shuffle Q1A1~3 shuffle(q-1-abcd, [o-1-abcd, o-2-abcd, o-3-abcd])
replace "XXX" in Q2 title with Q1 replace($[q-2-abcd].title, "XXX", ${q-1-abcd})
60. 开发顺序?先有DSL or 先有实体模型
DSL
实体模型
问卷逻辑对象
隔离
业务逻辑代码
61. test('if statement', function () {
const ast = {
"type": "SurveyLogic",
"body": [
{
"type": "ConditionStatement",
"condition": {
"type": "GetValue",
"item": {
"type": "Question",
"id": "q-4-lTp9"
}
},
"action": {
"type": "HideAction",
"param": [{
"type": "Question",
"id": "q-3-ql6u"
}]
}
}
]
};
expect(parser.parse('${q-4-lTp9} => hide(q-3-ql6u)')).toEqual(ast);
expect(parser.parse('if `q-4-lTp9` then hide `q-3-ql6u`')).toEqual(ast);
})
62. test('if statement', function () {
const ast = {
"type": "SurveyLogic",
"body": [
{
"type": "ConditionStatement",
"condition": {
"type": "GetValue",
"item": {
"type": "Question",
"id": "q-4-lTp9"
}
},
"action": {
"type": "HideAction",
"param": [{
"type": "Question",
"id": "q-3-ql6u"
}]
}
}
]
};
expect(parser.parse('${q-4-lTp9} => hide(q-3-ql6u)')).toEqual(ast);
expect(parser.parse('if `q-4-lTp9` then hide `q-3-ql6u`')).toEqual(ast);
})
63. 05
DSL配套设施
64. 编辑器
65. 详尽文档
66. 语法高亮 / 自动补全 / 错误提示
67. 效果
3 days
10 mins
问卷的开发时间
47.3k 份
使用DSL的问卷数
20.9k 人
使用DSL的用户数
获得自由
68. 仅仅需要一个解析器?
69. 内部DSL?
案例:表达2分钟之前
JS: new Date(Date.now()-2*60*1000)
外部DSL 内部DSL
2 mins ago (2).mins().ago()
70. 内部DSL
> (2).mins().ago()
Uncaught TypeError: 2.mins is not a function
71. Number.prototype.mins = function(){
return this*60*1000
}
Number.prototype.ago = function(){
return new Date(Date.now()-this)
}
> (2).mins().ago()
2021-06-20T09:54:32.365Z
72. 总结
•
•
•
•
适合DSL落地的场景
解析器生成器 Parser generator
DSL语法设计
配套设施的搭建
73. 克制
74. 克制
除了DSL没有其他高效解决问题的方案
能不能够使用内部DSL
领域内是否已有标准的语法
75. 感谢倾听
欢迎加入CDC
大会官网