声明式编程:它是一个真实的东西?


/**
 * 谨献给Yoyo
 *
 * 原文出处:https://www.toptal.com/software/declarative-programming
 * @author dogstar.huang <chanzonghuang@gmail.com> 2016-05-15
 */

目前,声明式编程是诸如数据库,模板和配置管理这样广泛而多样领域的主导范式。

简而言之,声明式编程由需要指导一个程序需要做什么,而不是告诉它如何做到组成。在实践中,这种方法需要提供一个用于表达用户想要什么,并通过屏蔽底层结构(循环,条件,任务)实现期望的最终状态的领域特定语言(DSL)。

虽然这种模式在其必要的地方是有着显著的改善,但我主张,声明式编程有明显的局限性,此限制我会在本文中进行探讨。此外,我建议双管齐下,既抓住声明式编程的好处,同时又取代其局限性。

警告这篇文章是多年来个人在声明工具中奋斗的结果。许多我在这里的说法都是没有彻底证明的,有的甚至基于事实价值。一个适当的、批评的声明式编程会花费大量的时间,精力,并且我要回到过去使用很多这样的工具;我的心是不会给这样的承诺的。这篇文章的目的是毫无保留与大家分享一些想法,并展示为我工作的东西。如果你已经挣扎于声明性编程工具中,你可能会发现喘息的机会和选择。如果你喜欢它的范式及其工具,不要把我看得太重。

如果声明式编程对于你工作得很好,那么我没什么好说的了

对于声明式编程,你可以爱,可以恨,但不能忽略。

声明式编程的优点

在我们探讨声明式编程的限制前,有必要了解它的优点。

可以说最成功的声明性编程工具是关系数据库(RDB)。它甚至可能是第一个声明工具。在任何情况下,RDBS体现了我认为声明式编程原型应具备的的两个属性:

  • 领域特定语言(DSL):对于关系数据库通用的接口是一个叫做结构化查询语言的DSL,也就是最为大众所知的SQL。

  • DSL为用户隐藏了较低层级:自从埃德加·科德关于RDBS的最初论文后,很朴素地,这种模式的力量就把期望的查询从实现它们的基本回路,索引和访问路径分离出来。

在RDB前,大部分数据库系统是通过必要的代码来访问的,这严重依赖于底层的细节,如记录的顺序,索引和数据本身的物理路径。因为这些元素随着时间的推移而变化,由于数据结构中一些基础性的变化,代码经常会停止工作。由此产生的代码是很难写的,难以调试,难以阅读和难以维护。我会用我夸张的肢体动作告诉你,大部分的代码极度可能是,长长的、满是众所周知的大鼠条件句、重复的和微妙的、状态依赖的bug的。

在面对这种情况,RDB为系统开发人员提供了一个巨大的生产力的飞跃。现在,不用成千上万行必要的代码,你有一个明确定义的数据格式,再加上数百(甚至是数十)个查询。因此,应用程序只需要处理一个抽象的,有意义的并且持久化的数据,并通过一个强大却又简单的查询语言接入它。采用它们,RDB中可能提高了程序员,以及招聘他们的公司的生产效率。

一般声明式编程列出来的优点有哪些?

声明式编程的拥护者很快指出它的优势。然而,即使他们也会权衡地承认。

1、可读性/可用性:一个DSL通常比伪代码更接近自然语言(如英语),因此对于非程序员更具可读性,也更容易学习。

2、简洁:大部分模板是由DSL抽象,几行就做同样的工作。

3、重用:更容易创建可用于不同目的的代码;一些使用命令式结构时,那是出了名的难。

4、幂等性:可以与最终状态工作,并让程序来帮你搞定。例如,通过一个update操作,如果它不存在,你可以插入一行,或者如果它已经存在,就修改它,而不是编写代码来处理这两种情况。

5、错误恢复:很容易指定一个在第一个错误就停止的结构,而无需为每一个可能的错误添加错误侦听者。(如果你曾经在node.js中写了三层嵌套的回调,你就明白我的意思了。)

6、引用透明:尽管这个优势通常与函数式编程相关的,实际上对于任何最小化状态的手工处理以及副作用依赖的方法都是有效的。

7、交换性:不必指定将在其中实现的实际顺序,即可表达最终状态的可能性。

虽然上面这些经常会作为声明式编程的优势而引用,我想将它们凝结成两种属性,这将作为当我提出了一个替代方法时的指导原则。

  • 一个针对特定领域的高级层:声明式编程使用了其适用领域的信息来创建一个高级层。很显然,如果我们处理数据库,我们要的一组操作来处理数据。上面七个优点大部分来自精确针对特定问题领域的高级层的创建。

  • 防错(傻瓜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具有以下属性:

1、它包含一个非常小的函数集:JSON有两个主要功能,parsestringify
2、它的功能最常用是接收复杂和递归的参数:一个解析的JSON是数组或对象,通常包含进一步的数组和对象在内。
3、这些功能的输入非常符合指定的形式:JSON有明确和严格执行验证的模式以告诉有效或无效的结构。
4、这些功能的输入和输出都可以被编程语言包含和生成,而不需要单独的语法。

但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中不行。

从走到滑:如何从低到高展开

在这一节中,我将试图演示从其领域展开高层次工具的一种方式。简而言之,该方法包括以下步骤:

1、需要二到四个问题领域代表性的实例。这些问题应该是真实的。从低层到高层展开是一个归纳问题,所以你需要真实的数据以便能想出代表性的解决方案。

2、以最直接的方式,并不用工具解决问题。

3、退后一步,好好看看你的解决方案,而发现其中的通用模式。

4、寻找展示(高层)的模式。

5、寻找生成(底层)的模式。

6、用你的高层解决同样的问题,并验证解决方案确实是正确的。

7、如果你觉得可以很容易地使用你的展示模式代表所有的问题,并且对于这些每个实例的生成模式都能产生正确的实现,那么你就大功告成了。否则,回到白板前。

8、如果有新的问题出现,用此工具解决这些问题,并相应修改。

9、该工具应渐近收敛于一个完成状态,不管有多少问题要解决。换句话说,该工具的复杂性应保持不变,而不是随着它解决的问题数量越来越大。

现在,到底什么是展示的模式,什么是生成的模式?很高兴你这样问。展示的模式是,你应该能够表达一个涉及你工具的、属于该领域的问题。它是结构字符,让你能编写任何可能希望在其领域适用性内表达的模式。在一个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行左右的代码都做到了。

最后一个有待回答的问题:我说的从走到滑是什么意思?当我们处理一个问题领域时,我们希望使用一个工具能传递我们该领域讨厌的细节。换句话说,我们要把底层扫到地毯下,速度越快越好。从走到滑是的方式提出了完全相反的:花一些时间在底层。接受其怪癖,并且在面对一系列真实、多样、有用的问题,明白哪个是必要的,哪个是可以避免。

在底层走一段时间并解决有用问题后,你将对他们领域有一个足够深刻的理解。展示和生成模式自然就会慢慢浮现;它们完全派生于他们打算解决的问题的本质。然后,你可以编写雇用他们的代码。如果他们工作,你将能滑过你最近不得不走过的问题。滑过意味着很多东西:这意味着速度,精度和缺乏摩擦。也许更重要的是,这种品质是可以感受到;当使用这个工具解决问题时,你是觉得你是走过问题,还是觉得你是滑过问题?

也许关于一个展开的工具,最重要的不在于它使我们免于处理底层这一事实。而是,通过捕捉在底层的重复的经验模式,一个良好的高层次工具使我们能够充分了解此适用性的领域。

一个展开的工具不仅解决了一个问题 -- 它还会启发你问题的结构。

所以,不要从一个值得的问题跑开。首先围着它走,然后滑过它。

相关文章:并发编程简介:初学者指南

dogstar

一位喜欢翻译的开发人员,艾翻译创始人之一。

广州