聲明式編程:這是真的嗎?
已發表: 2022-03-11目前,聲明式編程是數據庫、模板和配置管理等廣泛而多樣的領域的主導範式。
簡而言之,聲明式編程包括指示程序需要做什麼,而不是告訴它如何去做。 在實踐中,這種方法需要提供一種特定於域的語言 (DSL) 來表達用戶想要的內容,並將它們與實現所需最終狀態的低級構造(循環、條件、賦值)隔離開來。
雖然這種範式比它所取代的命令式方法有了顯著的改進,但我認為聲明式編程有很大的局限性,我將在本文中探討這些局限性。 此外,我提出了一種雙重方法,它既能獲得聲明式編程的好處,又能克服其局限性。
CAVEAT :這篇文章是個人多年與聲明性工具鬥爭的結果。 我在這裡提出的許多主張都沒有得到徹底的證實,有些甚至是表面上的。 對聲明式編程進行適當的批評需要相當多的時間和精力,而且我必須回去使用其中的許多工具; 我的心不在這樣的事業中。 本文的目的是與您分享一些想法,不費吹灰之力,並展示對我有用的方法。 如果您一直在為聲明式編程工具而苦惱,您可能會找到喘息的機會和替代方案。 如果你喜歡這個範式和它的工具,別把我當回事。
如果聲明式編程對您很有效,那麼我無法告訴您其他情況。
聲明式編程的優點
在我們探索聲明式編程的局限性之前,有必要了解它的優點。
可以說,最成功的聲明式編程工具是關係數據庫 (RDB)。 它甚至可能是第一個聲明性工具。 在任何情況下,RDB 都表現出我認為是聲明式編程原型的兩個屬性:
- 領域特定語言 (DSL) :關係數據庫的通用接口是一種名為結構化查詢語言的 DSL,通常稱為 SQL。
- DSL 對用戶隱藏了較低級別的層:自從 Edgar F. Codd 關於 RDB 的原始論文以來,很明顯,該模型的強大功能是將所需查詢與實現它們的底層循環、索引和訪問路徑分離。
在 RDB 之前,大多數數據庫系統都是通過命令式代碼訪問的,這在很大程度上依賴於低級細節,例如記錄的順序、索引和數據本身的物理路徑。 由於這些元素會隨著時間而變化,因此代碼通常會因為數據結構的某些潛在變化而停止工作。 生成的代碼難以編寫、難以調試、難以閱讀和難以維護。 我敢說,大部分代碼很可能很長,充滿了眾所周知的條件句、重複和微妙的、依賴於狀態的錯誤的老鼠巢。
面對這種情況,RDB 為系統開發人員提供了巨大的生產力飛躍。 現在,您不再需要數千行命令式代碼,而是擁有明確定義的數據方案,以及數百(甚至數十)條查詢。 因此,應用程序只需處理抽象的、有意義的和持久的數據表示,並通過強大而簡單的查詢語言進行接口。 RDB 可能將程序員和僱用他們的公司的生產力提高了一個數量級。
聲明式編程通常列出的優點是什麼?
- 可讀性/可用性:DSL 通常更接近自然語言(如英語)而不是偽代碼,因此更具可讀性並且非程序員也更容易學習。
- 簡潔:大部分樣板文件都被 DSL 抽像出來,只剩下更少的行來做同樣的工作。
- 重用:更容易創建可用於不同目的的代碼; 在使用命令式構造時,這是出了名的困難。
- 冪等性:您可以使用最終狀態並讓程序為您解決問題。 例如,通過 upsert 操作,您可以插入不存在的行,或者如果已經存在則修改它,而不是編寫代碼來處理這兩種情況。
- 錯誤恢復:很容易指定一個將在第一個錯誤處停止的構造,而不必為每個可能的錯誤添加錯誤偵聽器。 (如果你曾經在 node.js 中編寫過三個嵌套回調,你就會明白我的意思。)
- 引用透明性:雖然這個優勢通常與函數式編程相關聯,但它實際上適用於任何最小化手動處理狀態並依賴副作用的方法。
- 交換性:表達最終狀態的可能性,而不必指定將要實現的實際順序。
雖然以上都是聲明式編程的常見優點,但我想將它們濃縮為兩種品質,當我提出另一種方法時,它們將作為指導原則。
- 為特定領域量身定制的高級層:聲明式編程使用它所應用的領域的信息創建一個高級層。 很明顯,如果我們正在處理數據庫,我們需要一組操作來處理數據。 上述七個優勢中的大多數都源於創建了一個針對特定問題域精確定制的高級層。
- Poka-yoke (fool-proofness) :為領域量身定制的高級層隱藏了實現的必要細節。 這意味著您犯的錯誤要少得多,因為系統的低級細節根本無法訪問。 此限制消除了代碼中的許多類別的錯誤。
聲明式編程的兩個問題
在接下來的兩節中,我將介紹聲明式編程的兩個主要問題:分離性和缺乏展開。 每個批評都需要它的怪物,所以我將使用 HTML 模板系統作為聲明式編程缺點的具體示例。
DSL 的問題:分離性
想像一下,您需要編寫一個包含大量視圖的 Web 應用程序。 將這些視圖硬編碼為一組 HTML 文件不是一種選擇,因為這些頁面的許多組件都發生了變化。
最直接的解決方案是通過連接字符串來生成 HTML,這看起來太可怕了,以至於您很快就會尋找替代方案。 標準解決方案是使用模板系統。 儘管存在不同類型的模板系統,但出於分析的目的,我們將迴避它們的差異。 我們可以認為它們都是相似的,因為模板系統的主要任務是為使用條件和循環連接 HTML 字符串的代碼提供替代方案,就像 RDB 作為循環數據記錄的代碼的替代方案出現一樣。
假設我們使用標準模板系統; 你會遇到三個摩擦源,我將按重要性升序排列。 首先是模板必須位於與您的代碼分開的文件中。 因為模板系統使用的是DSL,語法不同,所以不能在同一個文件中。 在文件數量較少的簡單項目中,需要保留單獨的模板文件可能會使文件數量重複或增加三倍。
我為嵌入式 Ruby 模板 (ERB) 打開了一個例外,因為它們已集成到 Ruby 源代碼中。 對於以其他語言編寫的受 ERB 啟發的工具而言,情況並非如此,因為這些模板也必須存儲為不同的文件。
摩擦的第二個來源是 DSL 有自己的語法,與您的編程語言的語法不同。 因此,修改 DSL(更不用說編寫自己的)要困難得多。 要深入了解並更改工具,您需要了解標記化和解析,這很有趣且具有挑戰性,但很難。 我碰巧認為這是一個缺點。
你可能會問,“你到底為什麼要修改你的工具? 如果您正在做一個標準項目,那麼一個編寫良好的標準工具應該符合要求。” 也許是,也許不是。
DSL 從來沒有編程語言的全部功能。 如果是這樣,它就不再是 DSL,而是一門完整的編程語言。
但這不是 DSL 的全部意義所在嗎? 沒有可用的編程語言的全部功能,以便我們可以實現抽象並消除大多數錯誤來源? 也許是吧。 然而,大多數DSL 都是從簡單的開始,然後逐漸融合了越來越多的編程語言的功能,直到事實上它變成了一個。 模板系統就是一個很好的例子。 讓我們看看模板系統的標準特性以及它們如何與編程語言工具相關聯:
- 替換模板中的文本:變量替換。
- 模板的重複:循環。
- 如果不滿足條件,請避免打印模板:條件。
- 部分:子程序。
- Helpers :子例程(與 partials 的唯一區別是 helpers 可以訪問底層編程語言並讓您擺脫 DSL 緊身衣)。
這種認為 DSL 受到限制的論點是因為它同時覬覦和拒絕編程語言的力量,這與 DSL 的特性直接映射到編程語言的特性的程度成正比。 在 SQL 的情況下,這個論點很弱,因為 SQL 提供的大多數東西都與您在普通編程語言中找到的完全不同。 在光譜的另一端,我們發現幾乎所有功能都使 DSL 向 BASIC 收斂的模板系統。
現在讓我們退後一步,思考這三個典型的摩擦來源,用分離的概念來概括。 因為它是獨立的,所以 DSL 需要位於單獨的文件中; 它更難修改(甚至更難編寫你自己的),並且(通常但並非總是)需要你一個一個地添加你從真正的編程語言中遺漏的特性。
無論設計得多麼好,分離性都是任何 DSL 的固有問題。
我們現在轉向聲明性工具的第二個問題,它很普遍但不是固有的。
另一個問題:缺乏展開導致複雜性
如果我幾個月前寫了這篇文章,這部分會被命名為Most Declarative Tools Are #@!$#@! 複雜但我不知道為什麼。 在寫這篇文章的過程中,我找到了一種更好的表達方式:大多數聲明性工具比它們需要的複雜得多。 我將在本節的其餘部分解釋原因。 為了分析工具的複雜性,我提出了一種稱為複雜性差距的度量。 複雜性差距是使用工具解決給定問題與在工具打算替換的較低級別(可能是簡單的命令式代碼)中解決問題之間的差異。 當前一種解決方案比後者更複雜時,我們就存在復雜性差距。 更複雜,我的意思是更多的代碼行,更難閱讀、更難修改和更難維護的代碼,但不一定同時所有這些。
請注意,我們不是將較低級別的解決方案與最好的工具進行比較,而是與沒有工具進行比較。 這與“第一,不傷害”的醫學原則相呼應。
具有較大復雜性差距的工具的跡像是:
- 即使您知道如何使用該工具,使用該工具編寫代碼需要幾分鐘才能以命令式術語詳細描述。
- 你覺得你一直在圍繞這個工具工作,而不是在使用這個工具。
- 您正在努力解決一個直接屬於您正在使用的工具領域的問題,但是您找到的最佳 Stack Overflow 答案描述了一種解決方法。
- 當這個非常簡單的問題可以通過某個功能(工具中不存在)來解決時,您會在庫中看到一個 Github 問題,其中包含對所述功能的長時間討論,其中穿插了+1秒。
- 一種慢性的、發癢的、渴望放棄工具並在_for-loop_中自己完成所有事情的人。
我可能在這裡情緒激動,因為模板系統並不那麼複雜,但是這種相對較小的複雜性差距並不是他們設計的優點,而是因為適用領域非常簡單(請記住,我們只是在這裡生成 HTML )。 每當將相同的方法用於更複雜的領域(例如配置管理)時,複雜性差距可能會迅速將您的項目變成泥潭。
也就是說,一個工具比它打算替換的較低級別稍微複雜一些並不一定是不可接受的; 如果該工俱生成的代碼更易讀、更簡潔、更正確,那麼它是值得的。 當工具比它所替代的問題複雜幾倍時,這是一個問題; 這是完全不可接受的。 Brian Kernighan 有句名言:“控制複雜性是計算機編程的本質。 ” 如果一個工具給你的項目增加了很大的複雜性,為什麼還要使用它呢?
問題是,為什麼一些聲明性工具比它們需要的複雜得多? 我認為將其歸咎於糟糕的設計是錯誤的。 這種籠統的解釋,對這些工具的作者進行全面的人身攻擊,是不公平的。 必須有一個更準確和啟發性的解釋。
我的論點是,任何提供高級接口來抽象較低層次的工具都必須從較低層次展開這個較高層次。 展開的概念來自克里斯托弗·亞歷山大的代表作《秩序的本質》——尤其是第二卷。 總結這項具有里程碑意義的工作對軟件設計的影響,這超出了本文的範圍(更不用說我的理解)了。 我相信它的影響在未來幾年將是巨大的。 提供展開過程的嚴格定義也超出了本文的範圍。 我將在這里以啟發式的方式使用這個概念。
展開過程是以逐步的方式創建進一步的結構而不否定現有結構的過程。 在每一步,每一個變化(或分化,用亞歷山大的術語來說)都與任何先前的結構保持一致,而先前的結構只是過去變化的結晶序列。
有趣的是,Unix 是一個從低層次展開高層次的很好的例子。 在 Unix 中,操作系統的兩個複雜特性,批處理作業和協程(管道),只是基本命令的擴展。 由於某些基本的設計決策,例如使一切都成為字節流,shell 是用戶態程序和標準 I/O 文件,Unix 能夠以最小的複雜性提供這些複雜的功能。
為了強調為什麼這些是展開的優秀例子,我想引用 1979 年 Unix 的作者之一丹尼斯·里奇 (Dennis Ritchie) 的一篇論文的摘錄:
在批處理作業上:
…新的過程控制方案立即使一些非常有價值的特性變得微不足道; 例如分離進程(使用
&
)和遞歸使用 shell 作為命令。 大多數係統必須為不同於交互式使用的文件提供某種特殊的batch job submission
工具和特殊的命令解釋器。
關於協程:
Unix 管道的天才之處恰恰在於它是由以單工方式經常使用的相同命令構成的。
我認為,這種優雅和簡潔來自一個展開的過程。 批處理作業和協程是從以前的結構(在用戶空間 shell 中運行的命令)展開的。 我相信,由於創建 Unix 的團隊的極簡主義理念和有限的資源,系統逐步發展,因此,能夠合併高級功能而無需退回到基本功能,因為沒有足夠的資源來不這樣做。
在沒有展開過程的情況下,高層將比必要的複雜得多。 換句話說,大多數聲明性工具的複雜性源於這樣一個事實,即它們的高層次並沒有從它們打算替換的低層次展開。
如果您原諒新詞,這種缺乏展開通常是有必要保護用戶免受較低級別的影響。 這種強調防錯(保護用戶免受低級錯誤)的代價是巨大的複雜性差距會弄巧成拙,因為額外的複雜性會產生新的錯誤類別。 雪上加霜的是,這些類型的錯誤與問題域無關,而是與工具本身有關。 如果我們將這些錯誤描述為醫源性,我們不會走得太遠。
聲明式模板工具,至少在應用於生成 HTML 視圖的任務時,是高級別的典型案例,它背棄了它打算替換的低級。 為何如此? 因為生成任何非平凡的視圖都需要邏輯,而模板系統,尤其是無邏輯的系統,會通過大門排除邏輯,然後通過貓門將其中的一部分偷偷帶回。
注意:對於較大的複雜性差距的一個更弱的理由是,當一個工具被推銷為魔法或只是工作的東西時,低級別的不透明性應該是一種資產,因為魔法工具總是應該在你不理解的情況下工作為什麼或如何。 根據我的經驗,一個工具聲稱的越神奇,它就越快將我的熱情轉化為挫敗感。
但是關注點分離呢? 視圖和邏輯不應該保持分離嗎? 這裡的核心錯誤是將業務邏輯和表示邏輯放在同一個包中。 業務邏輯在模板中當然沒有位置,但表示邏輯仍然存在。 從模板中排除邏輯會將表示邏輯推送到服務器中,在那裡它被笨拙地容納。 我將這一點的清晰表述歸功於 Alexei Boronine,他在本文中為它提供了一個極好的案例。
我的感覺是,大約三分之二的模板工作存在於其表示邏輯中,而另外三分之一處理一般問題,例如連接字符串、結束標籤、轉義特殊字符等。 這是生成 HTML 視圖的兩面低級性質。 模板系統適當地處理後半部分,但它們在前半部分處理得不好。 無邏輯模板完全拒絕了這個問題,迫使你笨拙地解決它。 其他模板系統受到影響,因為它們確實需要提供一種非平凡的編程語言,以便它們的用戶可以實際編寫表示邏輯。
總結; 聲明性模板工具受到影響,因為:
- 如果他們要從他們的問題領域展開,他們將不得不提供生成邏輯模式的方法;
- 提供邏輯的 DSL 並不是真正的 DSL,而是一種編程語言。 請注意,其他領域,如配置管理,也缺乏“展開”。
我想用一個邏輯上與本文主線脫節的論點來結束批評,但與它的情感核心產生了深刻的共鳴:我們學習的時間有限。 人生苦短,除此之外,我們還需要努力。 面對我們的局限,我們需要花時間學習有用且經得起時間的東西,即使面對快速變化的技術。 這就是為什麼我建議您使用的工具不僅提供解決方案,而且實際上在其自身的適用範圍內提供了一個亮點。 RDB 教你數據,Unix 教你操作系統概念,但是由於工具不令人滿意,無法展開,我一直覺得我在學習次優解決方案的複雜性,同時對問題的本質一無所知它打算解決。
我建議您考慮的啟發式方法是,重視能夠闡明其問題領域的工具,而不是那些在聲稱的特徵背後掩蓋其問題領域的工具。
雙子方法
為了克服我在這裡提出的聲明式編程的兩個問題,我提出了一種雙方法:
- 使用數據結構領域特定語言 (dsDSL) 來克服分離性。
- 創建一個從較低層次展開的高層次,以克服複雜性差距。
數字DSL
數據結構 DSL (dsDSL) 是使用編程語言的數據結構構建的 DSL。 其核心思想是使用您可用的基本數據結構,例如字符串、數字、數組、對象和函數,並將它們組合起來以創建抽象來處理特定的域。
我們希望保持聲明結構或動作的能力(高級),而不必指定實現這些構造的模式(低級)。 我們希望克服 DSL 和我們的編程語言之間的分離,以便我們可以在需要時自由地使用編程語言的全部功能。 這不僅是可能的,而且通過 dsDSL 很簡單。
如果你在一年前問我,我會認為 dsDSL 的概念很新穎,然後有一天,我意識到 JSON 本身就是這種方法的完美示例! 已解析的 JSON 對象由以聲明方式表示數據條目的數據結構組成,以便獲得 DSL 的優勢,同時也使其易於在編程語言中解析和處理。 (可能還有其他 dsDSL,但到目前為止我還沒有遇到過。如果您知道其中一個,我將非常感謝您在評論部分提及它。)
與 JSON 一樣,dsDSL 具有以下屬性:
- 它由一組非常小的函數組成:JSON 有兩個主要函數,
parse
和stringify
。 - 它的函數最常接收復雜的遞歸參數:解析的 JSON 是一個數組或對象,其中通常包含更多的數組和對象。
- 這些函數的輸入符合非常特定的形式:JSON 有一個明確且嚴格強制執行的驗證模式來區分有效結構和無效結構。
- 這些函數的輸入和輸出都可以由編程語言包含和生成,而無需單獨的語法。
但是 dsDSL 在很多方面都超越了 JSON。 讓我們創建一個 dsDSL 用於使用 Javascript 生成 HTML。 稍後我會談到這種方法是否可以擴展到其他語言的問題(劇透:它肯定可以在 Ruby 和 Python 中完成,但在 C 中可能不行)。
HTML 是一種標記語言,由尖括號( <
和>
)分隔的tags
組成。 這些標籤可能有可選的屬性和內容。 屬性只是鍵/值屬性的列表,內容可以是文本或其他標籤。 對於任何給定的標籤,屬性和內容都是可選的。 我有點簡化,但它是準確的。
在 dsDSL 中表示 HTML 標記的一種直接方法是使用具有三個元素的數組: - 標記:字符串。 - 屬性:一個對象(普通的鍵/值類型)或undefined
的(如果不需要屬性)。 - 內容:字符串(文本)、數組(另一個標籤)或undefined
(如果沒有內容)。
例如, <a href="views">Index</a>
可以寫成['a', {href: 'views'}, 'Index']
。
如果我們想將此錨元素嵌入到帶有類links
的div
中,我們可以這樣寫: ['div', {class: 'links'}, ['a', {href: 'views'}, 'Index']]
.
要在同一級別列出多個 html 標籤,我們可以將它們包裝在一個數組中:
[ ['h1', 'Hello!'], ['a', {href: 'views'}, 'Index'] ]
相同的原則可以應用於在標籤內創建多個標籤:
['body', [ ['h1', 'Hello!'], ['a', {href: 'views'}, 'Index'] ]]
當然,如果我們不從中生成 HTML,這個 dsDSL 不會讓我們走得太遠。 我們需要一個generate
函數,它將獲取我們的 dsDSL 並生成一個帶有 HTML 的字符串。 因此,如果我們運行generate (['a', {href: 'views'}, 'Index'])
,我們將得到字符串<a href="views">Index</a>
。
任何 DSL 背後的想法是指定一些具有特定結構的構造,然後將其傳遞給函數。 在這種情況下,構成 dsDSL 的結構就是這個數組,它有 1 到 3 個元素; 這些數組具有特定的結構。 如果generate
徹底驗證了它的輸入(徹底驗證輸入既簡單又重要,因為這些驗證規則是 DSL 語法的精確模擬),它會準確地告訴你輸入出錯的地方。 一段時間後,您將開始認識到 dsDSL 中有效結構的區別,並且該結構將高度暗示它生成的底層內容。
現在,與 DSL 相對的 dsDSL 的優點是什麼?
- dsDSL 是代碼中不可或缺的一部分。 它可以減少行數、文件數和總體開銷的減少。
- dsDSL易於解析(因此更易於實現和修改)。 解析只是遍歷數組或對象的元素。 同樣,dsDSL 相對容易設計,因為您可以堅持使用您的編程語言的語法(每個人都討厭但至少他們已經知道),而不是創建新的語法(每個人都會討厭)。
- dsDSL 具有編程語言的所有功能。 這意味著如果使用得當,dsDSL 具有高級工具和低級工具的優點。
現在,最後一個主張是一個強有力的主張,所以我將在本節的其餘部分中支持它。 我所說的適當就業是什麼意思? 為了看到這一點,讓我們考慮一個示例,在該示例中,我們要構造一個表來顯示來自名為DATA
的數組的信息。
var DATA = [ {id: 1, description: 'Product 1', price: 20, onSale: true, categories: ['a']}, {id: 2, description: 'Product 2', price: 60, onSale: false, categories: ['b']}, {id: 3, description: 'Product 3', price: 120, onSale: false, categories: ['a', 'c']}, {id: 4, description: 'Product 4', price: 45, onSale: true, categories: ['a', 'b']} ]
在實際應用程序中, DATA
將從數據庫查詢中動態生成。
此外,我們有一個FILTER
變量,在初始化時,它將是一個包含我們要顯示的類別的數組。
我們希望我們的表:
- 顯示表格標題。
- 對於每個產品,顯示以下字段:描述、價格和類別。
- 不要打印
id
字段,而是將其添加為每一行的id
屬性。 替代版本:為每個tr
元素添加一個id
屬性。 - 如果產品正在銷售,請放置一個類
onSale
。 - 按降價對產品進行排序。
- 按類別過濾某些產品。 如果
FILTER
是一個空數組,我們將顯示所有產品。 否則,我們將僅顯示產品類別包含在FILTER
中的產品。
我們可以在大約 20 行代碼中創建符合此要求的表示邏輯:
function drawTable (DATA, FILTER) { var printableFields = ['description', 'price', 'categories']; DATA.sort (function (a, b) {return a.price - b.price}); return ['table', [ ['tr', dale.do (printableFields, function (field) { return ['th', field]; })], dale.do (DATA, function (product) { var matches = (! FILTER || FILTER.length === 0) || dale.stop (product.categories, true, function (category) { return FILTER.indexOf (category) !== -1; }); return matches === false ? [] : ['tr', { id: product.id, class: product.onSale ? 'onsale' : undefined }, dale.do (printableFields, function (field) { return ['td', product [field]]; })]; }) ]]; }
我承認這不是一個簡單的示例,但是,它代表了持久存儲(也稱為 CRUD)的四個基本功能的相當簡單的視圖。 任何不平凡的 Web 應用程序都會有比這更複雜的視圖。

現在讓我們看看這段代碼在做什麼。 首先,它定義了一個函數drawTable
來包含繪製產品表的表示邏輯。 該函數接收DATA
和FILTER
作為參數,因此可以用於不同的數據集和過濾器。 drawTable
實現了partial 和 helper 的雙重角色。
var drawTable = function (DATA, FILTER) {
內部變量printableFields
是唯一需要指定哪些字段是可打印字段的地方,以避免在需求變化時出現重複和不一致。
var printableFields = ['description', 'price', 'categories'];
然後我們根據其產品的價格對DATA
進行排序。 請注意,不同的和更複雜的排序標準將很容易實現,因為我們擁有整個編程語言可供使用。
DATA.sort (function (a, b) {return a.price - b.price});
這裡我們返回一個對象字面量; 一個數組,其中包含table
作為其第一個元素,其內容作為第二個元素。 這是我們要創建的<table>
的 dsDSL 表示。
return ['table', [
我們現在創建一個帶有表頭的行。 為了創建它的內容,我們使用 dale.do,它是一個類似於 Array.map 的函數,但也適用於對象。 我們將迭代printableFields
並為它們中的每一個生成表頭:
['tr', dale.do (printableFields, function (field) { return ['th', field]; })],
請注意,我們剛剛實現了迭代,這是 HTML 生成的主力,我們不需要任何 DSL 構造; 我們只需要一個函數來迭代數據結構並返回 dsDSL。 類似的本機或用戶實現的功能也可以做到這一點。
現在遍歷DATA
中包含的產品。
dale.do (DATA, function (product) {
我們檢查該產品是否被FILTER
排除在外。 如果FILTER
為空,我們將打印產品。 如果FILTER
不為空,我們將遍歷產品的類別,直到找到包含在FILTER
中的類別。 我們使用 dale.stop 執行此操作。
var matches = (! FILTER || FILTER.length === 0) || dale.stop (product.categories, true, function (category) { return FILTER.indexOf (category) !== -1; });
注意條件的複雜性; 它完全根據我們的要求量身定制,我們完全可以自由地表達它,因為我們使用的是編程語言而不是 DSL。
如果matches
是false
,我們返回一個空數組(所以我們不打印這個產品)。 否則,我們返回一個<tr>
及其正確的 id 和類,然後我們遍歷printableFields
以打印字段。
return matches === false ? [] : ['tr', { id: product.id, class: product.onSale ? 'onsale' : undefined }, dale.do (printableFields, function (field) { return ['td', product [field]];
當然,我們會關閉我們打開的所有內容。 語法不好玩嗎?
})]; }) ]]; }
現在,我們如何將這張表融入更廣泛的背景中? 我們編寫了一個名為drawAll
的函數,它將調用所有生成視圖的函數。 除了drawTable
,我們可能還有drawHeader
、 drawFooter
和其他類似的函數,它們都會返回 dsDSLs 。
var drawAll = function () { return generate ([ drawHeader (), drawTable (DATA, FILTER), drawFooter () ]); }
如果你不喜歡上面代碼的樣子,我說什麼都不能說服你。 這是最好的 dsDSL 。 你不妨停止閱讀這篇文章(並放棄一個刻薄的評論,因為如果你做到了這一點,你已經獲得了這樣做的權利!)。 But seriously, if the code above doesn't strike you as elegant, nothing else in this article will.
For those who are still with me, I would like to go back to the main claim of this section, which is that a dsDSL has the advantages of both the high and the low level :
- The advantage of the low level resides in writing code whenever we want, getting out of the straightjacket of the DSL.
- The advantage of the high level resides in using literals that represent what we want to declare and letting the functions of the tool convert that into the desired end state (in this case, a string with HTML).
But how is this truly different from purely imperative code? I think ultimately the elegance of the dsDSL approach boils down to the fact that code written in this way mostly consists of expressions, instead of statements. More precisely, code that uses a dsDSL is almost entirely composed of:
- Literals that map to lower level structures.
- Function invocations or lambdas within those literal structures that return structures of the same kind.
Code that consists mostly of expressions and which encapsulate most statements within functions is extremely succinct because all patterns of repetition can be easily abstracted. You can write arbitrary code as long as that code returns a literal that conforms to a very specific, non-arbitrary form.
A further characteristic of dsDSLs (which we don't have time to explore here) is the possibility of using types to increase the richness and succinctness of the literal structures. I will expound on this issue on a future article.
Might it be possible to create dsDSLs beyond Javascript, the One True Language? I think that it is, indeed, possible, as long as the language supports:
- Literals for: arrays, objects (associative arrays), function invocations, and lambdas.
- Runtime type detection
- Polymorphism and dynamic return types
I think this means that dsDSLs are tenable in any modern dynamic language (ie: Ruby, Python, Perl, PHP), but probably not in C or Java.
Walk, Then Slide: How To Unfold The High From The Low
In this section I will attempt to show a way for unfolding a high level tool from its domain. In a nutshell, the approach consists of the following steps
- Take two to four problems that are representative instances of a problem domain. These problems should be real. Unfolding the high level from the low one is a problem of induction, so you need real data to come up with representative solutions.
- Solve the problems with no tool in the most straightforward way possible.
- Stand back, take a good look at your solutions, and notice the common patterns among them.
- Find the patterns of representation (high level).
- Find the patterns of generation (low level).
- Solve the same problems with your high level layer and verify that the solutions are indeed correct.
- If you feel that you can easily represent all the problems with your patterns of representation, and the generation patterns for each of these instances produce correct implementations, you're done. Otherwise, go back to the drawing board.
- If new problems appear, solve them with the tool and modify it accordingly.
- The tool should converge asymptotically to a finished state, no matter how many problems it solves. In other words, the complexity of the tool should remain constant, rather than growing with the amount of problems it solves.
Now, what the hell are patterns of representation and patterns of generation ? I'm glad you asked. The patterns of representation are the patterns in which you should be able to express a problem that belongs to the domain that concerns your tool. It is an alphabet of structures that allows you to write any pattern you might wish to express within its domain of applicability. In a DSL, these would be the production rules. Let's go back to our dsDSL for generating HTML.
The patterns of representation for HTML are the following:
- A single tag:
['TAG']
- A single tag with attributes:
['TAG', {attribute1: value1, attribute2: value2, ...}]
- A single tag with contents:
['TAG', 'CONTENTS']
- A single tag with both attributes and contents:
['TAG', {attribute1: value1, ...}, 'CONTENTS']
- A single tag with another tag inside:
['TAG1', ['TAG2', ...]]
- A group of tags (standalone or inside another tag):
[['TAG1', ...], ['TAG2', ...]]
- Depending on a condition, place a tag or no tag:
condition ? ['TAG', ...] : []
/ Depending on a condition, place an attribute or no attribute:['TAG', {class: condition ? 'someClass': undefined}, ...]
These instances can be represented with the dsDSL notation we determined in the previous section. And this is all you need to represent any HTML you might need. More sophisticated patterns, such as conditional iteration through an object to generate a table, may be implemented with functions that return the patterns of representation above, and these patterns map directly to HTML tags.
If the patterns of representation are the structures you use to express what you want, the patterns of generation are the structures your tool will use to convert patterns of representation into the lower level structures. For HTML, these are the following:
- Validate the input (this is actually is an universal pattern of generation).
- Open and close tags (but not the void tags, like
<input>
, which are self-closing). - Place attributes and contents, escaping special characters (but not the contents of the
<style>
and<script>
tags).
Believe it or not, these are the patterns you need to create an unfolding dsDSL layer that generates HTML. Similar patterns can be found for generating CSS. In fact, lith does both, in ~250 lines of code.
One last question remains to be answered: What do I mean by walk, then slide ? When we deal with a problem domain, we want to use a tool that delivers us from the nasty details of that domain. In other words, we want to sweep the low level under the rug, the faster the better. The walk, then slide approach proposes exactly the opposite: spend some time on the low level. Embrace its quirks, and understand which are essential and which can be avoided in the face of a set of real, varied, and useful problems.
After walking in the low level for some time and solving useful problems, you will have a sufficiently deep understanding of their domain. The patterns of representation and generation will then arise naturally; they are wholly derived from the nature of the problem they intend to solve. You can then write code that employs them. If they work, you will be able to slide through problems where you recently had to walk through them. Sliding means many things; it implies speed, precision and lack of friction. Maybe more importantly, this quality can be felt; when solving problems with this tool, do you feel like you're walking through the problem, or do you feel that you're sliding through it?
Maybe the most important thing about an unfolded tool is not the fact that it frees us from having to deal with the low level. Rather, by capturing the empiric patterns of repetition in the low level, a good high level tool allows us to understand fully the domain of applicability.
An unfolded tool will not just solve a problem - it will enlighten you about the problem's structure.
So, don't run away from a worthy problem. First walk around it, then slide through it.