最近在產品上實作計算欄位功能,和設計師討論後,決定先讓使用者直接打語法,而不是全部以 GUI 操作。於是我們得設計一個小小的程式語言,處理像 {name} + " is awesome"
或 {total} - 10
這種式子。
從這些式子出發,我們可以設想一種簡單的 syntax nodes :
export const NodeType = {
NUMBER: 'number',
STRING: 'string',
FIELD: 'field',
BINARY: 'binary',
ERROR: 'error',
};
export const Num = (value) => ({
type: NodeType.NUMBER,
value: value,
});
export const Str = (value) => ({
type: NodeType.STRING,
value: value,
});
export const Field = (name) => ({
type: NodeType.FIELD,
name: name,
});
export const Binary = (name, left, right) => ({
type: NodeType.BINARY,
name: name,
left: left,
right: right,
});
export const Err = (message) => ({
type: NodeType.ERROR,
message: message,
});
於是 {name} + " is awesome"
可以寫成:
const expr = Binary('+', Field('name'), Str(' is awesome'));
{total} - 10
可以寫成:
const expr = Binary('-', Field('total'), Num(10));
我們要面對的環境是一個類似 spreadsheet 的系統,在簡化後,可以這樣描述欄位與式子的關係:
const ctx = {
'name': Str('Matt'),
'title': Binary('+', Field('name'), Str(' is awesome')),
'total': Num(42),
'discount': Binary('-', Field('total'), Num(10)),
};
而希望經過計算後,變成:
const ctx = {
'name': Str('Matt'),
'title': Str('Matt is awesome')),
'total': Num(42),
'discount': Num(32),
};
為了達成這件事,首先要把環境變成充滿著「算一下才知道」的值,而不是一個一個語法樹:
const ctx = {
'name': (ctx) => Str('Matt'),
'title': (ctx) => {/* 一些程式 */},
'total': (ctx) => Num(42),
'discount': (ctx) => {/* 另一些程式 */},
};
而我們的計算程式在做的事情,就是不斷地看看現在要算的欄位,需不需要從環境 ctx
中取得其他欄位的資訊,再把結果求出來:
export function run(exprTable) {
// 把欄位變成好計算的樣子
let ctx = contextMap(table, construct);
// 實際計算它
ctx = contextLoeb(ctx);
// 把計算結果取出來
return contextMap(ctx, x => x());
}
function contextMap(ctx, f) {
let result = {};
for (let key in ctx) {
result[key] = f(ctx[key]);
}
return result;
}
// 神秘的 löb !
function contextLoeb(ctx) {
let results;
const go = (f) => () => f(results);
results = contextMap(ctx, go);
return results;
}
我們先不關心這個神秘的 löb 函數是什麼,有興趣的人可以讀看看 Löb and möb in JavaScript ,該文解釋得比較詳細。現在我們只要知道它會幫我們遞迴計算出不同欄位的值就好了。
我們關心的是怎麼做一個正確的 construct
函數,來處理不同的 syntax node :
function construct(expr) {
switch (expr.type) {
// 如果是個數字,那它就繼續當數字,是計算的終點,通常稱為「值」(value)
case NodeType.NUMBER: return () => expr;
// 如果是個字串,就繼續當字串,也是計算的終點
case NodeType.STRING: return () => expr;
// 錯誤也是一種值
case NodeType.ERROR: return () => expr;
case NodeType.FIELD: return (ctx) => {
// 欄位參考不是計算的終點,這種不是終點的東西,通常稱為「表達式」(term 或 expression)
// 可以把前面的 value 看成一種特殊的 expression
// 如果是個欄位參考,那就從 context 中取得「可以計算出這個欄位結果的函數」,有點類似 redux-thunk 中的 thunk -- 你要跑一下才能知道最後的結果
const thunk = ctx[expr.name];
// 要是這東西無法求值,就報錯
if (typeof thunk !== 'function') {
return Err(`field ${expr.name} not found`);
}
// 傳回計算結果
return thunk(ctx);
}
case NodeType.BINARY: return (ctx) => {
// 二元運算子
// 先到一個預先準備好的 function table 查到對應的實作
const func = functionTable[expr.name];
if (typeof func !== 'function') {
return Err(`operator ${expr.name} not found`);
}
// 對運算子左邊的東西求值
const left = construct(expr.left)(ctx);
// 對運算子右邊的東西求值
const right = construct(expr.right)(ctx);
/* 省略一些錯誤處理用的程式碼 */
// 算算看結果
return func.call(undefined, left, right);
}
default: return () => Err('unknown expression');
}
}
const functionTable = {
'+': function add(left, right) {
if (left.type === NodeType.NUMBER && right.type === NodeType.NUMBER) {
return Num(left.value + right.value);
}
if (left.type === NodeType.STRING && right.type === NodeType.STRING) {
return Str(left.value + right.value);
}
return Err('can\'t add things other than numbers or strings');
},
'-': function sub(left, right) {
if (left.type === NodeType.NUMBER && right.type === NodeType.NUMBER) {
return Num(left.value - right.value);
}
return Err('can\'t subtract things other than numbers');
},
};
可以看出來,改變 add
和 sub
的實作,甚至可以支援字串與數字相加,或是兩個字串相減這種不直覺但是有時候很有用的功能。
這就是目前 FormEditor 計算欄位的基本結構。實際上為了和產品整合在一起,還得加上更多種語法與函數,也得替各種語法準備對應的 UI 元件。有機會再分享實作細節。
叡揚資訊股份有限公司
Dec 10, 2024前置作業 跟 MIS 申請 Microsoft Partner Network(MPN)。申請方式如下: 打開瀏覽器並連到 mis.gss.com.tw 點選 9. 申請軟體授權(Office / Visual Studio) 軟體名稱:Visual Studio 2022 權限原因申請內容可參考我當時輸入的:因參與雲端產品 Vital CRM 開發,需要申請 Azure AD 帳號;配發 MPN - Visual Studio subscriptions。 送出表單 靜候佳音
Jun 12, 2023公司簡介 叡揚是一個 34 年的純軟體上市櫃公司(6752),約 600 人,產品種類主要為 B2B 軟體,包含公文、CRM、企業表單流程、知識管理、人事薪資差勤、會計⋯⋯等多種軟體,公司業務從代理產品、專案、套裝軟體、雲端服務都有。 https://www.gss.com.tw/ 關於我們團隊 我們是前端設計與開發部門,是一個持續成長年輕活潑的團隊,在大公司中有多元的發展空間,每 2 個禮拜會有一次內部的技術分享,目前前端開發人員有 5 人 你的同事是一群好棒棒又前衛的設計師們,可參考 Instagram 作品中的部分設計: https://www.instagram.com/uwillx/
Oct 20, 2022repo: https://git.gss.com.tw/FED/speed3.0 branch: develop Isaac 提醒要注意的事項 啟用 Kendo license 請見 Set Up Your KendoReact License Key 新舊版本並存,所以 CSS 可能會互相影響
Oct 18, 2022or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up