[譯]聲明式編程:它是一個真實的東西?

聲明式編程:它是一個真實的東西?

/**
 * 謹獻給Yoyo
 *
 * 原文出處:https://www.toptal.com/software/declarative-programming
 * @author dogstar.huang <chanzonghuang@gmail.com> 2016-05-15
 */

目前,聲明式編程是諸如數據庫,模板和配置管理這樣普遍而多樣領域的主導範式。html

簡而言之,聲明式編程由須要指導一個程序須要作什麼,而不是告訴它如何作到組成。在實踐中,這種方法須要提供一個用於表達用戶想要什麼,並經過屏蔽底層結構(循環,條件,任務)實現指望的最終狀態的領域特定語言(DSL)。node

雖然這種模式在其必要的地方是有着顯著的改善,但我主張,聲明式編程有明顯的侷限性,此限制我會在本文中進行探討。此外,我建議左右開弓,既抓住聲明式編程的好處,同時又取代其侷限性。git

警告這篇文章是多年來我的在聲明工具中奮鬥的結果。許多我在這裏的說法都是沒有完全證實的,有的甚至基於事實價值。一個適當的、批評的聲明式編程會花費大量的時間,精力,而且我要回到過去使用不少這樣的工具;個人心是不會給這樣的承諾的。這篇文章的目的是毫無保留與你們分享一些想法,並展現爲我工做的東西。若是你已經掙扎於聲明性編程工具中,你可能會發現喘息的機會和選擇。若是你喜歡它的範式及其工具,不要把我看得過重。程序員

若是聲明式編程對於你工做得很好,那麼我沒什麼好說的了github

對於聲明式編程,你能夠愛,能夠恨,但不能忽略。sql

聲明式編程的優勢

在咱們探討聲明式編程的限制前,有必要了解它的優勢。shell

能夠說最成功的聲明性編程工具是關係數據庫(RDB)。它甚至多是第一個聲明工具。在任何狀況下,RDBS體現了我認爲聲明式編程原型應具有的的兩個屬性:數據庫

  • 領域特定語言(DSL):對於關係數據庫通用的接口是一個叫作結構化查詢語言的DSL,也就是最爲大衆所知的SQL。編程

  • DSL爲用戶隱藏了較低層級:自從埃德加·科德關於RDBS的最初論文後,很樸素地,這種模式的力量就把指望的查詢從實現它們的基本回路,索引和訪問路徑分離出來。數組

在RDB前,大部分數據庫系統是經過必要的代碼來訪問的,這嚴重依賴於底層的細節,如記錄的順序,索引和數據自己的物理路徑。由於這些元素隨着時間的推移而變化,因爲數據結構中一些基礎性的變化,代碼常常會中止工做。由此產生的代碼是很難寫的,難以調試,難以閱讀和難以維護。我會用我誇張的肢體動做告訴你,大部分的代碼極度多是,長長的、盡是衆所周知的大鼠條件句、重複的和微妙的、狀態依賴的bug的。

在面對這種狀況,RDB爲系統開發人員提供了一個巨大的生產力的飛躍。如今,不用成千上萬行必要的代碼,你有一個明肯定義的數據格式,再加上數百(甚至是數十)個查詢。所以,應用程序只須要處理一個抽象的,有意義的而且持久化的數據,並經過一個強大卻又簡單的查詢語言接入它。採用它們,RDB中可能提升了程序員,以及招聘他們的公司的生產效率。

通常聲明式編程列出來的優勢有哪些?

聲明式編程的擁護者很快指出它的優點。然而,即便他們也會權衡地認可。

一、可讀性/可用性:一個DSL一般比僞代碼更接近天然語言(如英語),所以對於非程序員更具可讀性,也更容易學習。

二、簡潔:大部分模板是由DSL抽象,幾行就作一樣的工做。

三、重用:更容易建立可用於不一樣目的的代碼;一些使用命令式結構時,那是出了名的難。

四、冪等性:能夠與最終狀態工做,並讓程序來幫你搞定。例如,經過一個update操做,若是它不存在,你能夠插入一行,或者若是它已經存在,就修改它,而不是編寫代碼來處理這兩種狀況。

五、錯誤恢復:很容易指定一個在第一個錯誤就中止的結構,而無需爲每個可能的錯誤添加錯誤偵聽者。(若是你曾經在node.js中寫了三層嵌套的回調,你就明白個人意思了。)

六、引用透明:儘管這個優點一般與函數式編程相關的,實際上對於任何最小化狀態的手工處理以及反作用依賴的方法都是有效的。

七、交換性:沒必要指定將在其中實現的實際順序,便可表達最終狀態的可能性。

雖然上面這些常常會做爲聲明式編程的優點而引用,我想將它們凝結成兩種屬性,這將做爲當我提出了一個替代方法時的指導原則。

  • 一個針對特定領域的高級層:聲明式編程使用了其適用領域的信息來建立一個高級層。很顯然,若是咱們處理數據庫,咱們要的一組操做來處理數據。上面七個優勢大部分來自精確針對特定問題領域的高級層的建立。

  • 防錯(傻瓜proofness):一個域量身定製的高級層隱藏了實現的必要細節。這意味着你犯更少的錯誤,由於該系統的低層細節簡直沒法訪問。這一限制消除了你代碼中的許多的錯誤。

聲明式編程的兩個問題

在接下來的兩節中,我將介紹聲明式編程的兩個主要問題:獨立性(separateness)缺少展開(lack of unfolding)。每一個批判須要其可怕之處,因此我會用HTML模板系統做爲聲明式編程的缺陷的具體例子。

DSL的問題:獨立性

試想一下,你須要編寫一個有很是很是多頁面的Web應用程序。把這些視圖硬編碼到一系列HTML文件不是一種好的選擇,由於這些網頁的不少組件都會改變。

最簡單的解決辦法,就是經過鏈接字符串來生成HTML,這彷佛很可怕,你很快就會尋找一種替代。標準解決方案是使用一個模板系統。雖然有不一樣類型的模板系統,出於本次分析的目的咱們將回避他們的差別。咱們能夠把他們所有類似地考慮成,他們在該模板系統的主要任務是提供使用條件和循環鏈接HTML字符串的替代方案,就像RDB經過數據記錄編寫代碼來循環做爲替代方案那樣。

假設咱們用了一個標準模板系統;你會遇到摩擦的三個來源,我將按重要程序升序列出。第一是模板必須駐留在與代碼分隔的獨立的文件。由於模板系統使用的是某個DSL,其語法是不一樣的,所以不能在同一個文件。在文件數量不多的簡單的項目裏,須要保持模板文件隔離可能會擴大三倍的文件的數量。

對於嵌入式Ruby模板(ERB),我打開了一個例外,由於這些都集成到了Ruby源代碼。這不是用其它語言編寫ERB-啓發工具的場景,由於這些模板也必須被存儲爲不一樣的文件。

摩擦的第二來源是DSL有其本身的語法,和你的編程語言有所不一樣。所以,修改DSL(更不用說編寫本身的)是至關困難。爲了走進幕後並改變工具,你須要瞭解標記化和解析,這是有趣且富有挑戰性的,但很難。我剛好把這看做是一個缺點。

如何才能可視化DSL?這並不容易,咱們只是說DSL是低層次結構之上乾淨,閃亮的層。

你可能會問,「究竟爲何你要修改你的工具?若是你正在作一個標準的項目,一個精心編寫的標準工具應該能符合要求。」也許是,也許不是。

DSL歷來都沒有編程語言的強大功能。若是有,它就再也不是一個DSL了,而是一個完整的編程語言。

可是,這不是DSL的關鍵所在嗎?沒有編程語言可用的強大功能,因此咱們能夠實現抽象並消除大部分缺陷的來源?也許是吧。然而,大多數的DSL開始簡單,而後逐步包含愈來愈多編程語言的設施,直到事實上,它變成了一個。模板系統是一個很好的例子。讓咱們來看看模板系統的標準功能,以及它們如何關聯到編程語言設施:

  • 在模板中替換文本:變量替換。

  • 模板重複:循環 。

  • 若是條件不知足,避免打印模板:條件語句。

  • 小部件(Partial):子程序。

  • 小助手(Helper):子程序(與小部件的惟一區別是,小助手能夠訪問底層的編程語言,讓你能夠跳出DSL)。

這種的說法,即一個DSL是有限的,由於它同時渴望和拒絕編程語言的能力,是直接正比於所述DSL的特徵,是直接可映射到一個編程語言的特徵的。在SQL的狀況下,參數是弱的,由於大多數SQL提供的事情不會像你在一個正常的編程語言找到那樣。在光譜的另外一端,咱們發現那裏幾乎每個功能使得DSL模板系統朝着BASIC收斂。

如今讓咱們退一步考慮摩擦的三個典型的來源,由獨立性的概念總結出來的。由於它是分離的,DSL須要放置在一個單獨的文件;這是很難修改(甚至更難寫你本身的),和(常常,但不老是)須要你來一個接一個地添加缺乏的一個真正編程語言的功能。

不管如何精心設計,獨立性是任何DSL與生俱來的問題。

咱們如今轉到聲明工具的第二個問題,這是廣泛存在的,但並非與生俱來。

另一個問題:缺少展開信息的複雜性

若是幾個月前我寫了這篇文章,這部分將被命名爲:大多數聲明工具是#@!$#@!複雜的,但我不知道爲何。在寫這篇文章過程當中,我找到了一個更好的名字:大多數聲明工具比須要他們的還要複雜得多。我會在這部分剩下的部分解釋爲何。爲了分析工具的複雜性,我提出了一個所謂的複雜性間隙(complexity gap) 度量。複雜性間隙是指解決一個給定的問題,與在工具打算取代的低級別(據推測,普通的必要代碼)解決問題的工具之間的差別。當前者比後者更復雜時,咱們就陷入了複雜性間隙。對於更復雜,個人意思是更多的代碼行,這些代碼難以閱讀,難以修改和難以維護,但所有這些不必定老是須要的。

請注意,咱們不是把較低級的解決方案和可能的最佳工具相比,而是和沒有工具相比。這反映了醫療原則「第一,不傷害」

具備較大複雜性間隙的工具的跡象有:

  • 當務之急須要幾分鐘描述豐富的細節的東西,使用工具將須要花幾個小時來編碼,甚至當你知道如何使用工具時。

  • 你以爲你老是不斷在圍繞工具工做,而使用工具工做。

  • 你在苦苦解決正屬於你正在使用工具的領域一個簡單的問題,但你找到最好的Stack Overflow答案描述了一個解決方法

  • 當這個很是簡單的問題能夠經過通用特徵來解決(這並不在存在於工具中),並你在Github的issue上看到這個類庫有着此特徵漫長的討論,以及+1s 引用。

  • 一個慢熱型,撓癢,渴望挖坑的工具,並要在你本身的for-loop中作了所有的事情。

這裏可能會一些騙人的情緒,由於模板系統並不複雜,但這種比較小的複雜性差距並非其設計的優勢,而是由於適用領域很是簡單(記住,這裏咱們只生成HTML)。每當一樣的方法被用於更復雜的領域時(如配置管理),複雜性間隙可能很快就讓您的項目陷入泥潭。

這就是說,一個工具在某種程度上比它打算取代的低級更加複雜,並不必定是不能接受的;若是該工具的代碼更易讀,更簡潔,更正確,它是值得的。當工具比它要替換的問題還要複雜好幾倍時,這是一個問題;這是不可接受的。正如Brian Kernighan的名言說道,「控制複雜性是計算機編程的本質。」 若是一個工具明顯增長了你項目的複雜性,爲何還要用它呢?

那麼問題是,爲何一些聲明式工具比他們須要的還要複雜得多?我認爲將其將其歸咎於設計缺陷是錯誤的。這樣通常性的解釋,對這些工具的做者進行的人身攻擊,是不公平的。必須有一個更精確的和啓發性的解釋。

摺紙時間!摺紙日期!具備高層接口的、抽象低級別的工具須要能從較低級別展開較高級別。

個人論點是,任何提供了一個高層接口以抽象較低級的工具必須能從較低級展開較高級。此展開的概念來自克里斯托弗 亞歷山大的鉅著,秩序的性質 - 卷二。它是(絕望地)超出了本文的範疇(更不用提個人理解)來總結這一鉅著對於軟件設計的影響範圍;我相信在隨後的幾年其影響是巨大的。提供展開過程的嚴格定義也超出了本文的範疇。我將在這裏以啓發式的方式使用此概念。

解摺疊過程是指,以逐步的、不否認已有的方式建立進一步的結構。在每個步驟,每一次變化(或分化,用亞歷山大的術語就是)與以往任何的結構保持和諧,以往的結構,簡單地說,是過去變化的結晶序列。

有趣的是,Unix是從一個較低級展開較高級的一個很好的例子。在Unix中,操做系統中兩個複雜的功能,批處理做業和協同程序(管道),都只是基本的命令簡單的擴展。因爲固化的基本的設計決策,如讓一切皆爲字節流,shell是一個用戶級程序以及標準I/O文件,UNIX可以以最小的複雜性提供這些複雜的功能。

爲了強調爲何這些是展開很好的例子,我想引用丹尼斯里奇,UNIX的做者之一,在1979年的一些節選:

關於批做業

「......新的過程控制方案即刻渲染一些很是有價值的且易於實現的功能;例如分離進程(帶)和做爲一個命令遞歸使用shell。大多數系統都提供某種特殊的批處理做業提交設施,以及與交互使用徹底不一樣文件的特殊命令解釋器。」

關於協同

「Unix管道的天才之處偏偏在於它是從單純的方式不斷用一樣的命令構造。」

UNIX開拓者丹尼斯里奇和肯·湯普遜在他們的操做系統中創造了展開的有力證實。他們還把咱們從所有皆是Windows的反烏托邦的將來拯救了出來。

這種優雅和簡潔,我認爲,來自一個展開的過程。批處理做業和協同程序展開於以前的結構(在用戶端的shell中運行命令)。我認爲,正是極簡的哲學和資源的限制,團隊才建立了Unix,系統才得以逐步演變而來,也正由於如此,才得以兼併先進的功能而不背棄基本的功能,由於沒有足夠的資源來不這樣作。

在沒有展開的過程當中,高層將比須要的更遠爲複雜。換言之,大部分聲明工具的複雜性的緣由是,它們的高層沒有從他們打算替換的低層展開。

這種展開(unfoldance)的缺少,若是你原諒新詞,從低級別保護用戶一般是無必要的。這種防錯的強調(從低級錯誤上保護用戶)會弄巧成拙形成很大複雜性間隙是,由於額外的複雜度會產生新類的錯誤。雪上加霜的是,這些類的錯誤與領域無關,而是與工具自己有關。若是咱們把這些錯誤描述爲醫源性,咱們就不會走得太遠。

聲明性模板工具,至少當應用到生成HTML視圖任務時,是一個背棄它所意圖替換的低層次的高層次的原型狀況。 怎麼會這樣?由於生成任何不平凡的視圖須要邏輯,和模板系統,特別邏輯更少的,經過正門放逐邏輯而後經過貓門走私一些回來。

注意:對於大型複雜性間隙的一個更弱的理由是,當一個工具做爲魔法銷售時,或諸如只是工做的一些東西時,低級別的不透明被認爲是一種資產,由於魔法工具老是不須要你明白爲何或如何就能工做的。在個人經驗,更魔法的工具聲稱是,它會更快地把個人激情變成挫敗。

可是對於關注點分離是什麼?不該該把視圖和邏輯保持獨立嗎?這裏核心錯誤,是把業務邏輯和展現邏輯放在同一個包。業務邏輯確定在模板中沒有立錐之地,但展現邏輯不管怎樣都存在。從模板排除邏輯會推使展現邏輯進入笨拙地被容納的服務。關於這一點,我還欠Alexei Boronine一個明確的說法,他在這篇文章中爲它創造了一個超級棒的案例。

個人感受是,大約三分之二的模板的工做在於它的展現邏輯,而另外的三分之一則處理通常的問題,諸如鏈接字符串,閉合標籤,轉義特殊字符,等等。這是生成HTML視圖低級別本質的兩面。模板系統能適當處理第二部分的一半,但它們處理很差第一部分。更少邏輯的模板背棄了這個問題,迫使你來笨拙地解決它。其餘的模板系統受苦,由於他們真正須要提供一個不平凡的編程語言,使他們的用戶確實能夠編寫展現邏輯。

總結一下;聲明模板工具受苦,是由於:

  • 若是他們是從他們的問題域展開,則不得不提供生成邏輯模式的方式;

  • 提供邏輯的DSL並非一個真正的DSL,而是一種編程語言。須要注意的是其餘領域,如配置管理,也遭受缺少「unfoldance」之苦。

我想以一個在邏輯上與這篇文章的線索無關,但在其情感核心上有着深深的共鳴的聲明來關閉此批判:咱們用來學習的時間是有限的。人生苦短,而最重要的是,咱們還要工做。在咱們的限制面前,咱們須要花時間學習有用而且經得起時間的東西,即便面對突飛猛進的技術。這就是爲何我勸你使用不僅是提供了一個解決方案,更對其自身適用性的領域域實際上有着耀眼光芒的工具。RDB教會你數據,而Unix教會你操做系統的概念,可是使用不可接受的未展開的工具,我老是以爲我是在學習一個次優解決方案的複雜性,而仍停留在它意圖解決的問題本質的黑暗之中。

我建議你要考慮的是啓發式的,照亮他們問題領域的、有價值的工具,而不是在聲稱的功能背後掩蓋問題領域的工具

雙子方法

爲了克服我在這裏提出的聲明式編程的兩個問題,我提出了一個雙子方法:

  • 使用數據結構領域特定語言(dsDSL),以克服獨立性。

  • 創造一個從低級別展開的高級別,以克服複雜間隙。

dsDSL

數據結構DSL(dsDSL)是一種由編程語言的數據結構構建的DSL。其核心思想是使用你已有的基本數據結構,如字符串,數字,數組,對象和函數,並結合他們以創造處理特定的領域的抽象。

咱們但願保持聲明結構的能力或行爲(高級別)而沒必要指定實現這些構建的模式(低級別)。咱們想克服DSL和咱們編程語言之間的獨立性,以便每當咱們須要它時能夠自由使用編程語言的強大功能。這不只是可能的,還可直接經過dsDSL。

若是你在一年前問我,我原本覺得dsDSL的概念是新的,而後有一天,我意識到JSON自己就是這種作法的一個很好的例子!解析過的JSON對象包括了聲明展現數據實體的數據結構,以便獲得DSL的優點,同時也使其在編程語言中易於分析和處理。(可能還有其餘的dsDSL,但到目前爲止,我尚未遇到過。若是你知道,我會很感激你在評論部分說起它)。

像JSON,一個dsDSL具備如下屬性:

一、它包含一個很是小的函數集:JSON有兩個主要功能,parsestringify
二、它的功能最經常使用是接收復雜和遞歸的參數:一個解析的JSON是數組或對象,一般包含進一步的數組和對象在內。
三、這些功能的輸入很是符合指定的形式:JSON有明確和嚴格執行驗證的模式以告訴有效或無效的結構。
四、這些功能的輸入和輸出均可以被編程語言包含和生成,而不須要單獨的語法。

但dsDSLs在許多方面超越JSON。讓咱們建立一個使用JavaScript生成HTML的dsDSL。稍候我會談到這種作法是否能夠延伸到其餘語言的問題(預告:它絕對能夠在Ruby和Python完成,但可能在C不行)。

HTML是由尖括號(<>)分隔tag組成的標記語言。這些標籤能夠有可選的屬性和內容。屬性是簡單的鍵/值對屬性的列表,而且內容能夠是文本或其它標籤。屬性和內容對於任何給定的標籤都是可選的。我有點簡化,但它是正確的。

在dsDSL中展現一個HTML標籤一個直接的方法是經過使用具備三個元素的一個數組:- 標籤:一個字符串;- 屬性:一個對象(扁平的,鍵/值類型)或是undefined(若是沒有屬性是必要的);- 內容:一個字符串(文本),一個數組(另外一個標籤)或undefined(若是沒有內容的話)。

例如,<a href="views">Index</a>能夠寫成:['a', {href: 'views'}, 'Index']

若是咱們想把這個錨點元素嵌入到一個div並用類links,咱們能夠這樣寫:
['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的結構是這樣有一到三個元素的數組;這些數組具備一個特定的結構。若是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屬性添加到每一行。另外一個版本:添加一個id屬性到每一個tr元素。

  • 若是產品是在售的,就放置一個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。任何不平凡的網頁應用程序都有比這更復雜的視圖。

如今,讓咱們看看這些代碼作了什麼。首先,它定義了一個函數,drawTable,包含繪畫產品表的邏輯展現。此函數接收DATAFILTER做爲參數,因此它可用於不一樣的數據集和過濾器。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裏。

若是matchesfalse,咱們將返回一個空數組(因此不會打印此產品)。不然,咱們返回一個帶有其正確的id和class的<tr>,而且經過printableFields來遍歷打印這些字段。

return matches === false ? [] : ['tr', {
            id: product.id,
            class: product.onSale ? 'onsale' : undefined
         }, dale.do (printableFields, function (field) {
            return ['td', product [field]];

固然,咱們閉合了所有打開的東西。語法不是頗有趣嗎?

})];
      })
   ]];
}

如今,咱們如何將這一表格應用到更廣闊的上下文中?咱們編寫了一個叫作drawAll的函數來調用所有生成視圖的函數。除了drawTable,咱們可能還會有drawHeaderdrawFooter和其餘相似的函數,全部這些都會返回dsDSL

var drawAll = function () {  
   return generate ([
      drawHeader (),
      drawTable (DATA, FILTER),
      drawFooter ()
   ]);
}

若是你不喜歡上面代碼長的樣子,那麼我說什麼都不能說服你了。這是最好的一個dsDSL。你可能也會中止閱讀這篇文章(而且也會留下一個有意義的評論,由於若是你已經研究這一塊好久的話,你贏得了這樣作的權利)。但嚴肅來講,若是上面的代碼你以爲不優雅,那麼這篇文章的其餘東西也不會。

對於那些仍然和我在一塊兒的人,我會回到本章節的主要主張,即一個dsDSL同時具備高層和低層的優點

  • 低層的優勢在於無論什麼時候咱們想寫代碼,均可走出DSL的緊箍咒。

  • 高層的優勢在於使用字符表示咱們想聲明的東西,並讓工具的函數將其轉換成最終須要的狀態(在此是HTML字符串)。

可是,這和單純的命令式代碼真正有什麼不一樣?我以爲dsDSL方式最終的優雅歸結於這樣的事實:以這種方式編寫的代碼更可能是包含表達式,而不是聲明。更精確地說,使用dsDSL的代碼幾乎徹底組成於:

  • 映射到較低層結構字符。

  • 帶有這些字符結構的函數調用或者lambda表達式,返回相同類型的結構。

包括大部分表達式和封裝衆多聲明在函數中的代碼是很是簡潔的,由於全部重複的模式均可以很容易提取。只要該代碼返回符合一個很是具體的,非任意形式的字符,你即可以編寫任意代碼。

另外一個dsDSL(在這裏咱們沒有時間探索)的特色是使用類型來增長字符結構的豐富性和簡潔性的可能性。關於這個問題,我將會在後續的文章進行闡述。

有沒可能在Javascript以外建立dsDSL,用一個真正的語言?我認爲這的確是可能的,只要語言支持:

  • 對於字符:數組,對象(關聯數組),函數調用和lambda表達式。

  • 運行時類型聲明。

  • 多態性和動態的返回類型。

我認爲,這意味着,dsDSL能實現於任何現代的動態語言(即:Ruby,Python,Perl和PHP),但可能在C或Java中不行。

從走到滑:如何從低到高展開

在這一節中,我將試圖演示從其領域展開高層次工具的一種方式。簡而言之,該方法包括如下步驟:

一、須要二到四個問題領域表明性的實例。這些問題應該是真實的。從低層到高層展開是一個概括問題,因此你須要真實的數據以便能想出表明性的解決方案。

二、以最直接的方式,並不用工具解決問題。

三、退後一步,好好看看你的解決方案,而發現其中的通用模式。

四、尋找展現(高層)的模式。

五、尋找生成(底層)的模式。

六、用你的高層解決一樣的問題,並驗證解決方案確實是正確的。

七、若是你以爲能夠很容易地使用你的展現模式表明全部的問題,而且對於這些每一個實例的生成模式都能產生正確的實現,那麼你就大功告成了。不然,回到白板前。

八、若是有新的問題出現,用此工具解決這些問題,並相應修改。

九、該工具應漸近收斂於一個完成狀態,無論有多少問題要解決。換句話說,該工具的複雜性應保持不變,而不是隨着它解決的問題數量愈來愈大。

如今,到底什麼是展現的模式,什麼是生成的模式?很高興你這樣問。展現的模式是,你應該可以表達一個涉及你工具的、屬於該領域的問題。它是結構字符,讓你能編寫任何可能但願在其領域適用性內表達的模式。在一個DSL裏,這些能夠是產品規則。讓咱們回到咱們生成HTML的dsDSL。

不起眼的HTML標籤是展現模式一個很好例子。讓咱們一探這些基本模式的究竟。

對於HTML展現模式以下:

  • 一個單一標籤:['TAG']

  • 一個帶屬性的單一標籤:['TAG', {attribute1: value1, attribute2: value2, ...}]

  • 一個帶內容的單一標籤:['TAG', 'CONTENTS']

  • 一個帶屬性和內容的單一標籤:['TAG', {attribute1: value1, ...}, 'CONTENTS']

  • 一個帶有另外一個標籤在內的單一標籤:['TAG1', ['TAG2', ...]]

  • 一組標籤(獨立或者在另外一個標籤內):[['TAG1', ...], ['TAG2', ...]]

  • 依賴於一個條件,放置一個標籤或者無標籤:condition ? ['TAG', ...] : [] / 依賴於一個條件,放置一個屬性或者無屬性:['TAG', {class: condition ? 'someClass': undefined}, ...]

這些實例能夠用上一節中所肯定的dsDSL符號來表示。而這就是你須要用於表明你可能須要的任何HTML的。更復雜的模式,例如遍歷一個對象的條件以產生一個表格,能夠經過返回上述展現模式的函數來實現,而且這些模式直接映射到HTML標籤。

若是說展現模式是你用於表達你所想要的結構,那麼生成模式則是你工具將用於把展現模式轉換成較低的級結構的結構。對於HTML,這些有如下幾種:

  • 驗證輸入(實際上這是一個通用生成模式)。

  • 開啓和關閉標籤(但非空標籤,如<input>,即自閉合的)。

  • 放置屬性和內容,轉義特殊字符(但不是有stylescript這樣標籤的內容)。

無論你信不信,這些就是你須要建立一個生成HTML的展開dsDSL層的模式。能夠找到用於生成CSS的相似模式。事實上,這兩點,lith經過250行左右的代碼都作到了。

最後一個有待回答的問題:我說的從走到滑是什麼意思?當咱們處理一個問題領域時,咱們但願使用一個工具能傳遞咱們該領域討厭的細節。換句話說,咱們要把底層掃到地毯下,速度越快越好。從走到滑是的方式提出了徹底相反的:花一些時間在底層。接受其怪癖,而且在面對一系列真實、多樣、有用的問題,明白哪一個是必要的,哪一個是能夠避免。

在底層走一段時間並解決有用問題後,你將對他們領域有一個足夠深入的理解。展現和生成模式天然就會慢慢浮現;它們徹底派生於他們打算解決的問題的本質。而後,你能夠編寫僱用他們的代碼。若是他們工做,你將能滑過你最近不得不走過的問題。滑過意味着不少東西:這意味着速度,精度和缺少摩擦。也許更重要的是,這種品質是能夠感覺到;當使用這個工具解決問題時,你是以爲你是走過問題,仍是以爲你是滑過問題?

也許關於一個展開的工具,最重要的不在於它使咱們免於處理底層這一事實。而是,經過捕捉在底層的重複的經驗模式,一個良好的高層次工具使咱們可以充分了解此適用性的領域。

一個展開的工具不只解決了一個問題 -- 它還會啓發你問題的結構。

因此,不要從一個值得的問題跑開。首先圍着它走,而後滑過它。

相關文章:併發編程簡介:初學者指南


------------------------

相關文章
相關標籤/搜索