作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
费德里科·佩雷罗的头像

费德里科•Pereiro

验证专家 in 工程

极简软件制造商. 设计、编写、测试、部署和维护真实的系统,解决真实的(如果无聊的话)问题.

工作经验

12

分享

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

简而言之, 声明性编程 由指令程序组成 什么 需要去做,而不是说出来 如何 去做. 在实践中,这种方法需要提供用于表达的领域特定语言(DSL) 什么 用户想要, 并保护它们不受低级结构(循环)的影响, 条件, 作业)实现期望的最终状态.

虽然这种范式相对于它所取代的命令式方法是一个显著的改进, 我认为声明式编程有很大的局限性, 这是我在本文中探讨的限制. 此外, 我提出了一种双重方法,既能抓住声明性编程的优点,又能克服它的局限性.

警告: 这篇文章是我多年来与声明性工具斗争的结果. 我在这里提出的许多说法都没有得到彻底的证实, 有些甚至以表面价值呈现. 对声明式编程进行适当的批评需要花费相当多的时间, 努力, 和 I would have to go back 和 use many of these 工具s; my heart is 不 in such an undertaking. 本文的目的是与您分享一些想法, 毫不留情, 向我展示什么对我有用. 如果您一直在与声明性编程工具作斗争,您可能会找到喘息的机会和替代方案. 如果你喜欢这个范例和它的工具,不要把我的话太当真.

如果声明式编程适合您, 我没资格跟你说别的.

您可以喜欢或讨厌声明性编程,但是您不能忽视它.

声明式编程的优点

在我们探讨声明性编程的局限性之前,有必要了解它的优点.

可以说,最成功的声明性编程工具是关系数据库(RDB)。. 它甚至可能是第一个声明性工具. 无论如何, rdb展示了我认为典型的声明性编程的两个属性:

  • 领域特定语言(DSL)关系型数据库的通用接口是一个名为 结构化查询语言,通常称为SQL.
  • DSL对用户隐藏了底层从那时起 埃德加F. Codd关于rdb的原始论文, 很明显,这个模型的强大之处在于将所需的查询与底层循环分离开来, 索引和实现索引的访问路径.

rdb之前, 大多数数据库系统都是通过命令式代码访问的, 这在很大程度上依赖于底层细节,比如记录的顺序, 索引和数据本身的物理路径. 因为这些元素会随着时间而改变, 代码经常因为数据结构的一些底层变化而停止工作. 结果代码难以编写,难以调试,难以阅读,难以维护. 我大胆地说,大部分代码都在, 所有的可能性, 长, 充满了众所周知的条件句的老鼠窝, 重复和微妙, 依赖错误.

面对这种情况,rdb为系统开发人员提供了巨大的生产力飞跃. 现在, 而不是成千上万行命令式代码, 你有一个明确定义的数据方案, 加上数百个(甚至只是数十个)查询. 结果是, 应用程序只需要处理抽象, 有意义且持久的数据表示, 并通过一个强大的, 然而简单的查询语言. RDB可能提高了程序员的生产力, 以及雇佣他们的公司, 一个数量级.

声明式编程通常列出的优点是什么?

下面列出了声明式编程的优点,但每个优点都有一个代表性的图标.

声明式编程的支持者很快就指出了它的优点. 然而,即使是他们也承认,这是有代价的.
  1. 可读性/可用性DSL通常更接近自然语言(如英语),而不是伪代码, 因此可读性更强,非程序员也更容易学习.
  2. 简洁:大部分样板都是由DSL抽象出来的,只留下更少的行来做同样的工作.
  3. 重用: it is easier to create code 那。 can be used for different purposes; something 那。’s 不oriously hard when using imperative constructs.
  4. 幂等性你可以和谁一起工作 结束状态 让程序帮你解决. 例如, 通过逆操作, 如果行不存在,则可以插入行, 或者修改它,如果它已经存在了, 而不是编写代码来处理这两种情况.
  5. 错误恢复:很容易指定一个构造,它将在第一个错误时停止,而不必为每个可能的错误添加错误侦听器. (如果您曾经在node . js中编写过三个嵌套回调.js,你知道我的意思.)
  6. 引用透明性虽然这个优点通常与 函数式编程, 它实际上适用于任何最小化手动处理状态和依赖于副作用的方法.
  7. 交换性表示最终状态的可能性,而不必指定其实现的实际顺序.

虽然以上都是声明性编程的常用优点, 我想把它们概括为两点, 当我提出另一种方法时,哪些将作为指导原则.

  1. 为特定领域量身定制的高级层声明性编程使用它所应用的领域的信息创建一个高级层. 很明显,如果我们在处理数据库,我们需要一组处理数据的操作. 上述七个优点中的大多数都源于创建一个高级层,该层可以精确地针对特定的问题领域进行定制.
  2. 防错 (fool-proofness)领域定制的高级层隐藏了实现的必要细节. 这意味着您犯的错误要少得多,因为系统的底层细节根本无法访问. 这个限制消除了许多 代码中的错误.

声明式编程的两个问题

在接下来的两节中,我将介绍声明式编程的两个主要问题: 分离缺乏展开. 每一种批评都需要它的妖魔鬼怪, 因此,我将使用HTML模板系统作为声明式编程缺点的具体例子.

dsl的问题:分离性

假设您需要编写一个包含大量视图的web应用程序. 将这些视图硬编码到一组HTML文件中是不可行的,因为这些页面的许多组件都会发生变化.

最直接的解决方案, 这是通过连接字符串生成HTML, 看起来很可怕,你会很快寻找另一个选择. 标准的解决方案是使用模板系统. 虽然有不同类型的模板系统, 为了进行分析,我们将回避它们之间的差异. 我们可以认为它们都是相似的,因为模板系统的主要任务是提供一种替代使用条件和循环连接HTML字符串的代码, 就像rdb作为循环遍历数据记录的代码的替代方案而出现一样.

Let’s suppose we go with a st和ard templating system; you will encounter three sources of friction, 我将按重要性的先后顺序列出. 首先,模板必须驻留在与代码分开的文件中. 因为模板系统使用DSL,所以语法是不同的,所以它不能在同一个文件中. 在简单的项目中, 哪里的文件数量少, 需要保留单独的模板文件可能会使文件数量重复或增加两倍.

我为嵌入式Ruby模板(ERB)打开一个异常,因为 那些 集成到Ruby源代码中. 对于用其他语言编写的受erbb启发的工具,情况并非如此,因为这些模板也必须作为不同的文件存储.

第二个原因是DSL有自己的语法, 与你的编程语言不同. 因此,修改DSL(更不用说编写自己的DSL了)相当困难. 到引擎盖下面去换工具, 您需要学习标记化和解析, 哪个有趣又有挑战性, 但是困难. 我恰好认为这是一种劣势.

您可能会问,“您究竟为什么要修改您的工具?? 如果您正在做一个标准项目,那么编写良好的标准工具应该符合要求.“也许是,也许不是.

DSL永远不具备编程语言的全部功能. 如果是这样的话,它就不再是DSL了,而是一种完整的编程语言.

但这不正是DSL的意义所在吗? To 是否具备编程语言的全部功能, 这样我们就可以实现抽象并消除大多数bug的来源? 也许,是的. 然而, 大多数 dsl开始时很简单,然后逐渐包含越来越多的编程语言的功能,直到, 事实上, it 成为一个. 模板系统就是一个很好的例子. 让我们来看看模板系统的标准特性,以及它们是如何与编程语言功能相关联的:

  • 替换模板中的文本:变量替换.
  • 模板的重复:循环.
  • 如果不满足条件,请避免打印模板:条件.
  • 分音:子程序.
  • 助手子例程(与partial的唯一区别是,帮助程序可以访问底层编程语言,并让您摆脱DSL的束缚).

这个论点, DSL是有限的,因为它同时觊觎和拒绝编程语言的能力, 与DSL的特性直接映射到编程语言的特性的程度成正比. 就SQL而言, 这个论证很弱,因为SQL提供的大多数功能都与普通编程语言中所提供的功能完全不同. 在光谱的另一端, 我们发现在模板系统中,几乎所有的特性都使DSL趋同 基本.

现在让我们退后一步,思考这三个摩擦的典型来源, 的概念总结 分离. 因为它是独立的, a DSL needs to be located on a separate file; it is harder to modify (和 even harder to write your own), 和(通常, 但并不总是)需要你添加, 一个接一个, 真正的编程语言所缺少的特性.

分离性是任何DSL的固有问题,无论设计得多么好.

现在我们转向声明性工具的第二个问题,它很普遍,但不是固有的.

另一个问题:缺乏展开导致复杂性

如果我几个月前写这篇文章,这一节就会被命名 大多数声明性工具都是#@!$#@! 复杂但我不知道为什么. 在写这篇文章的过程中,我发现了一种更好的表达方式: 大多数声明性工具都比它们需要的复杂得多. 我将在本节的其余部分解释原因. 为了分析工具的复杂性,我提出了一种称为 复杂性的差距. 复杂性差距是使用工具解决给定问题与在较低级别(假设是)解决问题之间的差异, 该工具打算替换的纯命令式代码. 当前一种解决方案比后一种解决方案更复杂时,我们就存在复杂性差距. By 更复杂的, 我的意思是更多的代码行, 难以阅读的代码, 更难修改,更难维护, 但并非所有这些都是同时发生的.

请注意,我们并没有将较低级别的解决方案与最好的工具进行比较, 而是反对 no 工具. 这与医学原理相呼应 “首先,不要伤害他人。”.

具有较大复杂性差距的工具的标志是:

  • 一些用命令式语言详细描述需要几分钟的东西将需要几个小时来使用该工具进行编码, 即使你知道如何使用这个工具.
  • 您会觉得您一直在围绕工具工作,而不是与工具一起工作.
  • 您正在努力解决一个直接的问题,该问题完全属于您正在使用的工具的领域, 但是你找到的最好的Stack Overflow答案描述了一个 解决方案.
  • 当这个非常简单的问题可以通过某个功能(工具中不存在)来解决,并且您在库中看到Github问题,其中包含对该功能的长时间讨论 +1年代点缀.
  • 一种慢性的,痒的,渴望抛弃工具,在一个循环中自己做整个事情.

由于模板系统并非如此,我可能在这里受到了情绪的影响 那。 复杂的, 但是这种相对较小的复杂性差距并不是它们设计的优点, 而是因为适用的领域非常简单(记住, 这里只是生成HTML). 当同样的方法用于更复杂的领域(比如配置管理)时,复杂性的差距可能很快将您的项目变成一个泥潭.

也就是说, it is 不 necessarily unacceptable for a 工具 to be some什么 更复杂的 than the lower level it intends to replace; if the 工具 yields code 那。 is more readable, 简明、正确, 这是值得的. It’s an issue when the 工具 is several times 更复杂的 than the problem it replaces; this is flat-out unacceptable. Brian Kernighan说过一句名言:控制复杂性是计算机程序设计的本质.“如果一个工具给你的项目增加了显著的复杂性,为什么还要使用它呢?

问题是,为什么一些声明性工具比它们需要的复杂得多? 我认为将其归咎于糟糕的设计是错误的. 这样一个笼统的解释,对这些工具的作者进行人身攻击,是不公平的. 必须有一个更准确、更有启发性的解释.

我的观点是,任何提供高级接口来抽象低级接口的工具都必须 展开 这个高的层次从低的层次开始. 的概念 展开 来自克里斯托弗·亚历山大的巨著 秩序的本质 - -特别是第二卷. It is (hopelessly) beyond the scope 本文简介 (不 to mention my underst和ing) to summarize the implications of this monumental work for software design; I believe its impact will be huge in years to come. 提供展开过程的严格定义也超出了本文的范围. 我将在a中使用这个概念 启发式方法.

展开的过程是一个, 逐步地, 创建新的结构,但不否定现有结构. 每一步, 每个变化(或分化), 用亚历山大的话来说)与之前的结构保持和谐, 当先前的结构为, 简单的, 过去变化的结晶顺序.

有趣的是, Unix 是一个从较低层次展开到更高层次的好例子吗. 在Unix中, 操作系统的两个复杂特征, 批处理作业和协程(管道), 仅仅是基本命令的扩展吗. 因为某些基本的设计决策, 比如把所有东西都变成字节流, 壳是一个 用户态程序标准I/O文件, Unix能够以最小的复杂性提供这些复杂的特性.

为了强调为什么这些都是很好的展开例子, 我想引用几段 1979年的论文 Dennis Ritchie, Unix的作者之一:

关于批处理作业:

… the new 过程 control scheme instantly rendered some very valuable features trivial to implement; for example detached 过程es (with &)和递归使用shell作为命令. 大多数系统必须提供某种特殊的 批处理作业提交 工具和一个特殊的命令解释器,用于不同于交互式使用的文件.

在协同程序:

Unix管道的天才之处在于,它是由经常以单形方式使用的相同命令构建而成的.

我认为,这种优雅和简单来自于 展开 过程. 批处理作业和协程是从以前的结构中展开的(在用户shell中运行的命令). 我相信这是因为极简主义的理念和创建Unix的团队有限的资源, 这个系统是逐步发展的, 因此, 是否能够整合高级功能而不会因为没有足够的资源而放弃基本功能.

在没有展开过程的情况下, 高层将比必要的复杂得多. 换句话说, 大多数声明性工具的复杂性源于这样一个事实,即它们的高层并没有从它们打算取代的低层展开.

这种缺乏 展开ance, 请原谅我的用词, 是否有必要保护用户不受较低层次的影响. 这种对poka-yoke(保护用户不受低级错误的影响)的强调是以巨大的复杂性差距为代价的,这是弄巧成拙的,因为额外的复杂性将产生新的错误类别. 雪上加霜, 这类错误与问题域无关,而是与工具本身有关. 如果我们把这些误差描述为 医源性.

声明式模板工具, 至少在应用于生成HTML视图的任务时是这样, 是否有一个典型的高级别的案例背弃了它想要取代的低级别的案例. 怎么这么? 因为 生成任何重要的视图都需要逻辑, 模板系统, 尤其是那些没有逻辑的, 把逻辑从正门驱逐出去,然后从猫门偷偷带回来一些.

注意: 对于大复杂性差距的一个更弱的理由是,当一个工具被营销为 魔法之类的 只是工作, 低层次的不透明性被认为是一种资产,因为一个神奇的工具总是应该在你不知道为什么或如何工作的情况下工作. 根据我的经验, 一种工具声称的魔力就越大, 它越快把我的热情转化为沮丧.

但是关注点的分离呢? 视图和逻辑不应该分开? 这里的核心错误是将业务逻辑和表示逻辑放在同一个袋子中. 业务逻辑当然在模板中没有位置,但是表示逻辑仍然存在. 从模板中排除逻辑会将表示逻辑推到服务器中,而服务器本来就不适合它. 我把这一点的明确表述归功于阿列克谢·博罗宁,他对此提出了极好的理由 在本文中.

我的感觉是,模板大约三分之二的工作都在它的表示逻辑中, 而另外三分之一处理诸如连接字符串之类的通用问题, 结束标签, 转义特殊字符, 等等......。. 这是生成HTML视图的两种低级性质. 模板系统可以很好地处理后半部分,但它们不能很好地处理前半部分. 没有逻辑的模板完全背弃了这个问题,迫使您笨拙地解决它. 其他模板系统的问题在于,它们确实需要提供一种重要的编程语言,这样它们的用户才能真正编写表示逻辑.

To sum up; declarative templating 工具s suffer because:

  • 如果它们从问题域展开, 它们必须提供生成逻辑模式的方法;
  • 提供逻辑的DSL并不是真正的DSL,而是编程语言. 请注意,其他领域,如配置管理,也受到缺乏“展开”的困扰.”

我想用一个逻辑上与本文主题无关的论点来结束我的评论, 但却与它的情感核心产生了深刻的共鸣:我们学习的时间有限. 人生苦短,最重要的是,我们需要工作. 面对我们的局限, 我们需要把时间花在学习有用和经得起时间考验的东西上, 即使面对快速变化的技术. 这就是为什么我建议您使用的工具不仅提供解决方案,而且实际上为其自身的适用性领域提供了明亮的光芒. rdb教你关于数据的知识, Unix教你操作系统的概念, 但用的是不令人满意的无法展开的工具, 我总是觉得自己在学习一个次优解决方案的复杂性的同时,却对它想要解决的问题的本质一无所知.

我建议你们考虑的启发式是, 能够阐明问题领域的价值工具, 而不是那些将问题领域隐藏在所谓的特性背后的工具.

双生方法

克服声明式编程的两个问题, 我在这里展示的, 我提出了两种方法:

  • 使用特定于数据结构领域的语言(dsDSL)来克服分离性.
  • 创建一个从较低层次展开的高层次,以克服复杂性差距.

dsDSL

数据结构DSL (dsDSL)是一种DSL 用编程语言的数据结构构建的. 核心思想是使用可用的基本数据结构, 比如字符串, 数字, 数组, 对象和函数, 并将它们组合起来以创建处理特定领域的抽象.

我们希望保持声明结构或操作的能力(高级),而不必指定实现这些结构的模式(低级)。. 我们希望克服DSL和编程语言之间的分离,以便在需要时可以自由地使用编程语言的全部功能. 通过dsdsl,这不仅是可能的,而且是直接的.

如果你一年前问我, 我本以为dsDSL的概念是新颖的, 然后有一天, 我意识到 JSON 它本身就是这种方法的一个完美例子! 解析后的JSON对象由数据结构组成,这些数据结构以声明方式表示数据条目,以便获得DSL的优点,同时也使其易于在编程语言中进行解析和处理. (可能还有其他的dsdsl,但到目前为止我还没有遇到过. 如果你知道一个,我将非常感谢你在评论部分提到它.)

与JSON一样,dsDSL具有以下属性:

  1. 它由一组非常小的函数组成:JSON有两个主要函数, 解析stringify.
  2. 它的函数通常接收复杂的递归参数:解析后的JSON是一个数组, 或对象, 其中通常包含进一步的数组和对象.
  3. 这些函数的输入符合非常特定的形式:JSON有一个显式的、严格强制的验证模式来区分有效和无效的结构.
  4. 这些函数的输入和输出都可以由编程语言包含和生成,而不需要单独的语法.

但是dsdsl在很多方面都超越了JSON. 让我们创建一个dsDSL,用于使用Javascript生成HTML. 稍后我将讨论这种方法是否可以扩展到其他语言(剧透:它绝对可以在Ruby和Python中完成), 但可能不是C项。.

HTML是一种由 标签 以尖括号(<>). 这些标记可能具有可选的属性和内容. 属性只是键/值属性的列表,内容可以是文本或其他标记. 对于任何给定的标记,属性和内容都是可选的. 我稍微简化了一下,但它是准确的.

在dsDSL中表示HTML标签的一种直接方法是使用包含三个元素的数组: —标签:字符串. —属性:一个对象(plain, key/value类型)或 未定义的 (如果没有必要的属性). —内容:字符串(文本)、数组(另一个标签)或 未定义的 (如果没有内容).

例如, Index 可以写成 ['a', {href: 'views'}, 'Index'].

如果我们想把这个锚元素嵌入到 div 与类 链接,我们可以这样写: [" div "{类:“链接”},[a, {href:“观点”},“索引”]].

要列出同一级别的多个html标签,我们可以将它们包装在一个数组中:

[
   (h1,你好!'],
   ['a', {href: 'views'}, 'Index']
]

同样的原理也适用于在一个标签内创建多个标签:

(“身体”,(
   (h1,你好!'],
   ['a', {href: 'views'}, 'Index']
]]

当然,如果我们不从中生成HTML,这个dsDSL也不会让我们走得太远. 我们需要一个 生成 函数,它将接受我们的dsDSL并产生一个带有HTML的字符串. 所以如果我们运行 生成(['a', {href: 'views'}, 'Index']),我们将得到字符串 Index.

任何DSL背后的思想都是指定一些具有特定结构的结构,然后将其传递给函数. 在这种情况下, 构成dsDSL的结构是这个数组, which has one to three 元素s; these 数组 have a specific structure. If 生成 彻底验证其输入(彻底验证输入既简单又重要), 因为这些验证规则是DSL语法的精确模拟), 它会准确地告诉你输入哪里出错了. 过了一会儿, 您将开始认识到dsDSL中有效结构的区别, 这个结构将高度暗示它所产生的潜在的东西.

现在,与DSL相比,dsDSL的优点是什么?

  • dsDSL是代码的一个组成部分. 它可以减少行数和文件数,并减少总体开销.
  • dsDSLs是 容易 解析(因此更容易实现和修改). 解析只是遍历数组或对象的元素. 同样的, dsDSLs相对容易设计,因为不用创建新的语法(每个人都会讨厌),你可以坚持使用编程语言的语法(每个人都讨厌,但至少他们已经知道了)。.
  • dsDSL具有编程语言的所有功能. 这意味着dsDSL, 如果使用得当, 高级工具和低级工具都有优点吗.

现在,最后一个主张是强有力的,所以我将用本节剩下的时间来支持它. 我的意思是什么 正确地使用? 来看看这是怎么回事, 让我们考虑一个示例,在这个示例中,我们希望构造一个表来显示来自名为 DATA.

var DATA = [
   {id: 1,描述:'产品1',价格:20,onSale: true,类别:['a']},
   {id: 2,描述:'产品2',价格:60,onSale: 假,类别:['b']},
   {id: 3,描述:'产品3',价格:120,onSale: 假,类别:['a', 'c']},
   {id: 4,描述:'产品4',价格:45,onSale: true,类别:['a', 'b']}
]

在实际应用中, DATA 会从数据库查询动态生成吗.

此外,我们有 过滤器 变量,初始化后,它将是一个包含我们想要显示的类别的数组.

我们希望我们的桌子:

  • 显示表头.
  • 对于每个产品,显示字段:描述,价格和类别.
  • 不要打印 id 字段,但将其添加为 id 属性设置为每行. 备选版本:添加一个 id 每个属性 tr 元素.
  • 放置一个类 onSale 如果产品在打折.
  • 按降序排列产品.
  • 按类别过滤某些产品. If 过滤器 是空数组,我们会显示所有的产品吗. 否则,我们将只显示包含该产品类别的产品 过滤器.

我们可以在大约20行代码中创建符合此要求的表示逻辑:

函数drawTable (DATA, 过滤器) {

   var printableFields = ['description', 'price', 'categories'];

   DATA.Sort(函数(a, b){返回a.价格- b.价格});

   返回['table', []
      (tr,戴尔.do (printableFields, function (field) {
         返回['th',字段];
      })],
      戴尔.do (DATA, function (product) {
         Var 匹配 = (! Filter || Filter.长度=== 0)|| 戴尔.停止(产品.类别,true, function (category) {
            回流过滤器.indexOf(一类) !== -1;
         });

         返回匹配 === 假 ? [] : ['tr', {
            id:产品.id,
            类:产品.onSale ? 'onsale':未定义
         },戴尔.do (printableFields, function (field) {
            返回['td', product [field]];
         })];
      })
   ]];
}

我承认这不是一个简单的例子, 然而, 它代表了持久存储的四个基本功能的一个相当简单的视图, 也被称为 CRUD. 任何重要的web应用程序都会有比这更复杂的视图.

现在让我们看看这段代码在做什么. 首先,它定义了一个函数, drawTable,以包含绘制产品表的表示逻辑. 这个函数接收 DATA过滤器 作为参数,因此它可以用于不同的数据集和过滤器. drawTable 完成部分和助手的双重角色.

   var drawTable = function (DATA, 过滤器) {

内部变量, printableFields, 是唯一需要指定哪些字段是可打印字段的地方吗, 面对不断变化的需求,避免重复和不一致.

   var printableFields = ['description', 'price', 'categories'];

然后进行排序 DATA 根据其产品的价格. 请注意,不同的和更复杂的排序标准可以直接实现,因为我们可以使用整个编程语言.

   DATA.Sort(函数(a, b){返回a.价格- b.价格});

Here we return an object literal; an array which contains table 作为它的第一个元素,它的内容作为第二个元素. 的dsDSL表示

我们想要创造.

   返回['table', []

现在我们用表头创建一行. 要创建其内容,我们使用 戴尔.do 函数是什么样的 数组.map,但它也适用于对象. 我们会迭代 printableFields 并为每个表生成表头:

      (tr,戴尔.do (printableFields, function (field) {
         返回['th',字段];
      })],

注意,我们刚刚实现了迭代, 生成HTML的主力, 和 we didn’t need any DSL constructs; we only needed a function to iterate a data structure 和 return dsDSLs. 类似的本机或用户实现的函数也可以达到同样的效果.

中包含的产品进行迭代 DATA.

      戴尔.do (DATA, function (product) {

我们检查这个产品是否被遗漏了 过滤器. If 过滤器 是空的,我们会打印产品吗. If 过滤器 不是空的, 我们将遍历产品的类别,直到找到其中包含的类别 过滤器. 我们使用 戴尔.停止.

         Var 匹配 = (! Filter || Filter.长度=== 0)|| 戴尔.停止(产品.类别,true, function (category) {
            回流过滤器.indexOf(一类) !== -1;
         });

Notice the intricacy of the 条件al; it is precisely tailored to our requirement 和 we have total freedom for expressing it because we are in a programming language rather than a DSL.

If 匹配 is ,则返回一个空数组(因此不打印此乘积). 否则,返回a

使用适当的id和类,然后遍历 printableFields 打印字段.


         返回匹配 === 假 ? [] : ['tr', {
            id:产品.id,
            类:产品.onSale ? 'onsale':未定义
         },戴尔.do (printableFields, function (field) {
            返回['td', product [field]];

我们当然会关闭所有我们打开的东西. 语法不是很有趣吗??

         })];
      })
   ]];
}

现在,我们如何将这张表整合到更广泛的背景中? 我们写一个函数 drawAll 它将调用生成视图的所有函数. 除了 drawTable,我们可能也有 拉拔机机头, drawFooter 以及其他类似的函数,所有这些 会退回dsdsl吗.

var drawwall = function () {
   返回生成([
      拉拔机机头(),
      drawTable (DATA, 过滤器),
      drawFooter ()
   ]);
}

如果你不喜欢上面代码的样子,我说什么也说服不了你. 这是最好的dsDSL. 你最好停止阅读这篇文章(并写下刻薄的评论,因为如果你已经读到这里了,你已经有权利这样做了!). 但是说真的,如果上面的代码不能让您觉得优雅,那么本文中的其他内容也不会.

为那些还和我在一起的人, 我想回到这一节的主要主张, 那就是 dsDSL具有高层次和低层次的优点:

  • 低水平的优势 我们可以随时编写代码,摆脱DSL的束缚.
  • 优势水平高 存在于使用表示我们想要声明的内容的文字,并让工具的功能将其转换为所需的最终状态(在本例中为, 包含HTML的字符串).

但是这和纯粹的命令式代码有什么区别呢? 我认为dsDSL方法的优雅最终归结为这样一个事实 以这种方式编写的代码主要由表达式组成,而不是语句. 更准确地说,使用dsDSL的代码几乎完全由以下部分组成:

  • 映射到较低级结构的字面量.
  • 函数调用或lambda 在这些文字结构中 返回相同类型的结构.

大部分由表达式组成并将大部分语句封装在函数中的代码非常简洁,因为所有的重复模式都可以很容易地抽象出来. 您可以编写任意代码,只要该代码返回的文字符合非常特定的, 专制的形式.

dsdsl的另一个特征(我们在这里没有时间进行探讨)是可以使用类型来增加文字结构的丰富性和简洁性. 我将在以后的文章中详细阐述这个问题.

是否有可能在Javascript(唯一真正的语言)之外创建dsdsl? 我认为这是可能的,只要语言支持:

  • 字面量:数组、对象(关联数组)、函数调用和lambda.
  • 运行时类型检测
  • 多态性和动态返回类型

我认为这意味着dsdsl在任何现代动态语言中都是成立的.e.(Ruby, Python, Perl, PHP),但可能不会使用C或Java.

走,然后滑:如何从低点展开高点

在本节中,我将尝试展示一种从其领域展开高级工具的方法. 简而言之,该方法包括以下步骤

  1. 选取两到四个问题作为问题域的代表性实例. 这些问题应该是真实存在的. 从低层次展开高层次是一个归纳法问题, 所以你需要真实的数据来提出有代表性的解决方案.
  2. 用…解决问题 没有工具 用最直接的方式.
  3. 退后一步,好好看看你的解决方案,并注意其中的共同模式.
  4. 发现表示模式(高水平).
  5. 查找生成模式(低级).
  6. 用高级层解决相同的问题,并验证解决方案确实是正确的.
  7. 如果你觉得你可以很容易地用你的表达方式来表达所有的问题, 每个实例的生成模式生成正确的实现, 你完成. 否则,回到绘图板.
  8. 如果出现新的问题,请使用工具解决并进行相应的修改.
  9. 无论工具解决了多少问题,它都应该渐近地收敛到一个完成状态. 换句话说, 工具的复杂性应该保持不变, 而不是随着它解决的问题数量的增加而增长.

现在,到底是什么 表征模式生成模式? 我很高兴你这么问. 表示模式是您应该能够表达属于与您的工具相关的领域的问题的模式. 它是一个结构的字母表,允许您在其适用范围内编写您可能希望表达的任何模式. 在DSL中,这些就是产生规则. 让我们回到生成HTML的dsDSL.

分解HTML片段. “行”&lt;字体颜色= " # 3863 a0 "&gt; wxzjnt.com &lt; /字体&Gt;”有以下标签:“属性”上的单词“颜色”, 字符串“#3863A0”上的“value”, "content"上的字符串" total ".. Com”,“开始标签”在“toptal”之前.Com”,之后的所有内容都加上“结束标签”.

不起眼的HTML标记是表示模式的一个很好的例子. 让我们仔细看看这些基本模式.

HTML的表示模式如下:

  • 单个标签: ['标签']
  • 带有属性的单个标签: ['TAG', {attribute1: value1, attribute2: value2, ...}]
  • 带有内容的单个标签: (“标签”,“内容”)
  • 一个同时具有属性和内容的标签: ['TAG', {attribute1: value1, ...},“内容”)
  • 一个标签,里面有另一个标签: (“标签1”,(“标签2”, ...]]
  • 一组标签(独立的或在另一个标签内的): [[“标签1”, ...]、[标签2, ...]]
  • 根据条件,放置标签或不放置标签: 条件 ? (“标签”, ...] : [] /根据条件,放置属性或不放置属性: ['标签',{类:条件。 ? “someClass”:未定义的}, ...]

这些实例可以用我们在前一节中确定的dsDSL符号表示. 这就是表示可能需要的HTML所需的全部内容. 更复杂的模式, 如条件迭代通过一个对象来生成一个表, 可以用返回上述表示模式的函数实现吗, 这些模式直接映射到HTML标签.

如果表征模式是你用来表达你想要的东西的结构, 生成模式是工具将用于将表示模式转换为较低级结构的结构. 对于HTML,它们如下:

  • 验证输入(这实际上是一种通用的生成模式).
  • 打开和关闭标记(但不包括void标记,如 ,自动关闭).
  • 放置属性和内容,转义特殊字符(但不转义