声明式编程:这是真的吗?

本文概述

当前, 声明性编程是广泛而多样的域集(例如数据库, 模板和配置管理)的主要范例。

简而言之, 声明式编程包括在程序上指示需要执行的操作, 而不是告诉程序如何执行。实际上, 这种方法需要提供一种特定于域的语言(DSL)来表达用户想要的内容, 并将其与实现所需最终状态的低级构造(循环, 条件, 赋值)隔离。

尽管此范例相对于它所取代的命令式方法是一个显着的改进, 但我认为声明式编程具有很大的局限性, 这是我在本文中探讨的局限性。此外, 我提出了一种双重方法, 该方法既可以获取声明式编程的好处, 又可以取代其局限性。

CAVEAT:本文是由于使用声明性工具进行了多年的个人奋斗而产生的。我在这里提出的许多主张尚未得到充分证明, 甚至有一些是按面值提出的。对声明式编程进行适当的批判将花费大量的时间, 精力, 而我将不得不回头使用这些工具中的许多工具。我的心不在此。本文的目的是与你分享一些想法, 不加思索, 并说明对我有用的方法。如果你在使用声明式编程工具时遇到麻烦, 则可能会找到喘息的机会和替代方法。而且, 如果你喜欢该范式及其工具, 请不要对我太当真。

如果声明式编程很适合你, 那么我无法以其他方式告诉你。

你可以喜欢或讨厌声明式编程,但是你不能忽视它。

你可以喜欢或讨厌声明式编程, 但是你不能忽视它。

鸣叫

声明式编程的优点

在探讨声明式编程的局限性之前, 有必要了解其优点。

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

  • 领域特定语言(DSL):关系数据库的通用接口是名为结构化查询语言的DSL, 通常称为SQL。
  • DSL对用户隐藏了较低层:自Edgar F. Codd在RDB上发表原始论文以来, 很明显, 此模型的功能是将所需的查询与实现它们的基础循环, 索引和访问路径分离。

在RDB之前, 大多数数据库系统都是通过命令式代码访问的, 该命令式代码严重依赖于底层细节, 例如记录的顺序, 索引和数据本身的物理路径。由于这些元素随时间变化, 因此由于数据结构中的某些潜在变化, 代码通常会停止工作。生成的代码难以编写, 难以调试, 难以阅读且难以维护。我会费力地讲, 这段代码大部分是长期存在的, 充满了成名老鼠的条件嵌套, 重复和与状态有关的细微错误。

面对这种情况, RDB为系统开发人员带来了巨大的生产力飞跃。现在, 你有了一个明确定义的数据方案, 而不是数千行命令性代码, 外加数百个(甚至只有几十个)查询。结果, 应用程序只需要处理抽象, 有意义和持久的数据表示形式, 并通过功能强大而又简单的查询语言将其连接起来。 RDB可能将程序员和雇用他们的公司的生产力提高了一个数量级。

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

声明式编程的支持者很快指出了优点。但是,即使他们承认这也需要权衡。

声明式编程的支持者很快指出了优点。但是, 即使他们承认这也需要权衡。

鸣叫

  1. 可读性/可用性:DSL通常比伪代码更接近自然语言(例如英语), 因此更具可读性, 并且非程序员更容易学习。
  2. 简洁:许多样板都是DSL所抽象的, 只剩下较少的线路来完成相同的工作。
  3. 重用:创建可用于不同目的的代码更加容易;当使用命令式构造时, 这是很难做到的。
  4. 幂等:你可以使用最终状态, 让程序为你解决。例如, 通过upsert操作, 你可以插入一行(如果该行不存在), 或者对其进行修改(如果已经存在), 而不用编写代码来处理这两种情况。
  5. 错误恢复:很容易指定将在第一个错误处停止的构造, 而不必为每个可能的错误添加错误侦听器。 (如果你曾经在node.js中编写了三个嵌套的回调, 那么你知道我的意思。)
  6. 引用透明性:尽管此优点通常与函数式编程相关, 但实际上它对于任何将状态的手动处理最小化并依赖副作用的方法都是有效的。
  7. 可交换性:无需指定执行顺序的实际顺序即可表达最终状态的可能性。

尽管以上都是声明式编程的所有常被提及的优点, 但我想将它们浓缩为两种特质, 当我提出另一种方法时, 它们将作为指导原则。

  1. 为特定领域量身定制的高层:声明式编程使用它所应用的域的信息来创建高层。显然, 如果要处理数据库, 则需要一组用于处理数据的操作。以上七个优点中的大多数都来自于为特定问题领域量身定制的高级层的创建。
  2. Poka-yoke(防呆):领域定制的高级层隐藏了实现的必要细节。这意味着你提交的错误要少得多, 因为根本无法访问系统的低级详细信息。此限制消除了代码中的许多错误类别。

声明式编程的两个问题

在以下两节中, 我将介绍声明式编程的两个主要问题:分离性和缺乏展开性。每个批评都需要它的柏忌, 所以我将使用HTML模板系统作为声明性编程缺点的具体示例。

DSL的问题:分离

想象一下, 你需要编写一个包含大量视图的Web应用程序。不能将这些视图硬编码为一组HTML文件, 因为这些页面的许多组件都会更改。

最直接的解决方案是通过串联字符串来生成HTML, 它是如此可怕, 以至于你很快就会寻找替代方案。标准解决方案是使用模板系统。尽管模板系统的类型不同, 但出于分析目的, 我们将回避它们的差异。我们可以认为它们都是相似的, 因为模板系统的主要任务是为使用条件和循环连接HTML字符串的代码提供替代方法, 就像RDB替代了通过数据记录循环的代码一样。

假设我们使用标准的模板系统;你将遇到三个摩擦源, 我将按重要性的升序列出。首先是模板必须驻留在与代码分开的文件中。由于模板系统使用DSL, 因此语法不同, 因此不能位于同一文件中。在文件数量少的简单项目中, 需要保留单独的模板文件可能会使文件数量重复或增加三倍。

我为嵌入式Ruby模板(ERB)打开了一个例外, 因为它们已集成到Ruby源代码中。用其他语言编写的受ERB启发的工具不是这种情况, 因为这些模板也必须存储为不同的文件。

产生冲突的第二个原因是DSL拥有自己的语法, 这与你的编程语言不同。因此, 修改DSL(更不用说编写自己的DSL)要困难得多。要深入研究并更改工具, 你需要学习有关令牌化和解析的知识, 这既有趣又具有挑战性, 但很难。我碰巧看到这是一个缺点。

怎样才能使DSL充满活力?这并不容易,但是我们只能说DSL是底层构造之上的干净,有光泽的层。

怎样才能使DSL充满活力?这并不容易, 但是我们只能说DSL是底层构造之上的干净, 有光泽的层。

鸣叫

你可能会问:”为什么要修改工具?如果你正在执行标准项目, 那么编写精良的标准工具就可以满足要求。”可能是, 可能不是。

DSL从未具有编程语言的全部功能。如果可以的话, 它将不再是DSL, 而是一种完整的编程语言。

但这不是DSL的重点吗?是否没有可用的编程语言的全部功能, 以便我们可以实现抽象并消除大多数错误源?也许是的。但是, 大多数DSL从简单开始, 然后逐渐结合越来越多的编程语言功能, 直到实际上成为一种语言为止。模板系统就是一个很好的例子。让我们看看模板系统的标准功能, 以及它们与编程语言工具的关系:

  • 替换模板中的文本:变量替换。
  • 模板重复:循环。
  • 如果不满足条件, 请避免打印模板:条件。
  • Partials:子例程。
  • Helpers:子例程(partials的唯一区别是Helper可以访问底层的编程语言, 并让你摆脱DSL的束缚)。

DSL之所以受到限制是因为它同时渴望和拒绝编程语言的能力, 这一论点与DSL的功能可以直接映射到编程语言的功能的程度成正比。就SQL而言, 该参数很弱, 因为SQL提供的大多数功能都不像你在普通编程语言中所能找到的。在频谱的另一端, 我们找到了模板系统, 其中几乎每个功能都使DSL趋向BASIC。

现在让我们退后一步, 思考一下这三个典型的摩擦源, 并通过分离性概念进行总结。因为它是单独的, 所以DSL需要位于单独的文件中。很难修改(甚至更难编写自己的代码), 并且(通常但并非总是)需要你逐一添加真正的编程语言所缺少的功能。

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

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

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

如果我几个月前写过这篇文章, 那么本节将被命名为”最声明式工具是#@!$#@!”。复杂, 但我不知道为什么。在撰写本文的过程中, 我发现了一种更好的表达方式:大多数声明性工具都比他们需要的复杂。我将在本节的其余部分中解释原因。为了分析工具的复杂性, 我提出了一种称为复杂性差距的措施。复杂性差距是使用工具解决给定问题与在工具打算替代的较低级别(大概是普通命令性代码)中解决问题之间的区别。当前者的解决方案比后者更复杂时, 我们将面临复杂性差距。更为复杂的是, 我指的是更多的代码行, 更难阅读, 更难修改和难以维护的代码, 但不一定所有这些都同时出现。

请注意, 我们并不是将底层解决方案与可能的最佳工具进行比较, 而是将其与任何工具进行比较。这与”首先, 不伤害”的医学原则相呼应。

具有较大复杂性差距的工具的迹象是:

  • 即使你知道如何使用该工具, 也需要花费几分钟来用命令性的术语进行详细描述, 这需要花费数小时才能完成。
  • 你会感到自己一直在围绕该工具而不是使用该工具工作。
  • 你正在努力解决一个直接属于所使用工具领域的直接问题, 但是找到的最佳堆栈溢出答案描述了一种解决方法。
  • 当这个非常简单的问题可以通过某个功能(该工具中不存在)解决时, 你会在库中看到一个Github问题, 其中对该功能进行了很长的讨论, 其中散布了+1。
  • 一个长期的, 渴望的, 渴望放弃工具并自己做整个事情的人。

因为模板系统不是那么复杂, 所以我可能会对此产生感动, 但是相对较小的复杂性差距并不是其设计的优点, 而是因为适用范围非常简单(请记住, 我们只是在此处生成HTML )。每当对更复杂的域(例如配置管理)使用相同的方法时, 复杂性差距可能很快使你的项目陷入困境。

话虽如此, 某种工具要比它打算替代的下层工具复杂一些, 这并不一定是不可接受的。如果该工具产生的代码更具可读性, 简洁性和正确性, 那么值得这样做。当这个工具比它所取代的问题复杂数倍时, 就会出现问题。这是完全不能接受的。布莱恩·克尼根(Brian Kernighan)著名地指出:”控制复杂性是计算机编程的本质。”如果工具给你的项目增加了极大的复杂性, 为什么还要使用它呢?

问题是, 为什么某些声明性工具比所需的工具复杂得多?我认为将其归咎于糟糕的设计是错误的。这种笼统的解释, 是对这些工具作者的全面抨击, 是不公平的。必须有一个更准确和启发性的解释。

折纸时间!具有到抽象较低层的高层接口的工具必须将较高层从较低层展开。

折纸时间!折纸时间!具有到抽象较低层的高层接口的工具必须将较高层从较低层展开。

鸣叫

我的争辩是, 任何提供高级界面以抽象较低层的工具都必须从较低层展开此较高层。展开的概念来自克里斯托弗·亚历山大(Christopher Alexander)的巨著, 《秩序的本质》-特别是第二卷。 (无可厚非)超出了本文的范围(更不用说我的理解了), 以总结这项艰巨的工作对软件设计的影响;我相信在未来的几年中它将产生巨大的影响。提供展开过程的严格定义也超出了本文的范围。我将在这里以启发式的方式使用该概念。

展开过程是以逐步的方式创建新结构而不破坏现有结构的过程。在每一个步骤中, 每一个变更(或用亚历山大的术语来区分)都与任何先前的结构保持一致, 而简单的说, 先前的结构是过去变化的结晶序列。

有趣的是, Unix是将较高的级别从较低的级别上展现出来的一个很好的例子。在Unix中, 操作系统的两个复杂功能, 即批处理作业和协程(管道), 仅仅是基本命令的扩展。由于某些基本的设计决策, 例如使所有内容都成为字节流, shell是用户界面程序和标准I / O文件, Unix能够以最小的复杂性提供这些复杂的功能。

为了强调这些为什么是很好的例子, 我想引用Unix的作者之一Dennis Ritchie在1979年发表的论文中的一些摘录:

在批处理作业上:

…新的过程控制方案立即提供了一些非常有价值的, 微不足道的实现功能;例如, 分离的进程(带有&)以及将Shell作为命令递归使用。大多数系统必须提供某种特殊的批处理作业提交工具和特殊的命令解释器, 用于与交互式使用的文件不同的文件。

在协程上:

Unix管道的精妙之处在于, 它是由不断以单纯形方式使用的相同命令构成的。

UNIX先驱Dennis Ritchie和Ken Thompson创建了一个强大的演示,说明了它们在OS中的发展。他们还使我们摆脱了反乌托邦的全Windows未来。

UNIX先驱Dennis Ritchie和Ken Thompson创建了一个强大的演示, 说明了它们在OS中的发展。他们还使我们摆脱了反乌托邦的全Windows未来。

鸣叫

我认为, 这种优雅和简单来自不断发展的过程。批处理作业和协程从先前的结构中展开(命令在userland shell中运行)。我相信, 由于创建Unix的团队的极简主义哲学和有限的资源, 该系统逐步发展, 因此能够合并高级功能, 而又不会将其重新启用为基本功能, 因为没有足够的资源来进行开发。否则。

在没有展开过程的情况下, 高级别将比必需的复杂得多。换句话说, 大多数声明性工具的复杂性源于以下事实:它们的高层次并没有从它们打算取代的低层次上发展出来。

如果你原谅新词, 那么这种缺乏表现的做法通常是因为有必要使用户免受较低级别的攻击。这种对poka-yoke的强调(保护用户免受低级错误的侵害)是以较大的复杂性差距为代价的, 而这种缺陷会自毁, 因为额外的复杂性会生成新的错误类别。更糟的是, 这些错误类别与问题域无关, 而与工具本身无关。如果我们将这些错误描述为医源性, 我们就不会太过分。

声明性的模板工具, 至少在应用于生成HTML视图的任务时, 是高级别的典型案例, 而将其转变为打算替换的低级别。为何如此?因为生成任何非平凡的视图都需要逻辑, 并且要对模板系统(尤其是无逻辑的视图系统)进行模版处理, 所以请通过主门排除逻辑, 然后再通过猫门将其走私回去。

注意:对于较大的复杂性差距, 甚至更弱的理由是, 当一种工具以魔术的形式或以某种形式起作用时, 低水平的不透明性被认为是一种资产, 因为魔术工具总是应该在你不了解的情况下起作用为什么或如何。以我的经验, 工具看起来越神奇, 它越快将我的热情转化为挫败感。

但是关注点分离又如何呢?观点和逻辑不应该分开吗?这里的核心错误是将业务逻辑和表示逻辑放在同一个包中。业务逻辑在模板中当然没有位置, 但是表示逻辑仍然存在。从模板中排除逻辑会将表示逻辑推送到笨拙地容纳它的服务器中。我对这一点的明确表述归功于Alexei Boronine, 他在本文中对此做了很好的论证。

我的感觉是, 模板的工作大约有三分之二驻留在其表示逻辑中, 而另外三分之一则涉及一般问题, 例如连接字符串, 关闭标签, 转义特殊字符等等。这是生成HTML视图的两方面的低层次本质。模板系统在下半部分处理得当, 但在上半部分效果不佳。完全没有逻辑的模板将其拒之门外, 迫使你笨拙地解决它。其他模板系统之所以受苦, 是因为它们确实需要提供非平凡的编程语言, 以便其用户可以实际编写表示逻辑。

总结一下;声明性模板工具之所以受苦是因为:

  • 如果要从问题领域中发展出来, 他们将不得不提供产生逻辑模式的方法。
  • 提供逻辑的DSL实际上不是DSL, 而是一种编程语言。请注意, 其他域(如配置管理)也遭受缺乏”扩展”的困扰。

我想用一种在逻辑上与本文的主题无关的论点来结束评论, 但在其情感核心方面引起深刻共鸣:我们的学习时间有限。生命短暂, 最重要的是, 我们需要工作。面对我们的局限性, 即使面对日新月异的技术, 我们也需要花费时间来学习有用且经得起时间的学习。这就是为什么我劝你使用不仅提供解决方案而且实际上在其自身适用性领域上更具亮点的工具的原因。 RDB教你有关数据的知识, 而Unix教给你有关OS概念的知识, 但是由于无法令人满意的工具无法解决, 我一直感到自己正在学习次优解决方案的复杂性, 而对问题的本质一无所知它打算解决。

我建议你考虑的启发式方法是:可以阐明其问题领域的价值工具, 而不是使它们隐藏在所声称功能背后的问题领域的工具。

双胞胎方法

为了克服我在这里介绍的声明式编程的两个问题, 我提出了一种双重方法:

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

DSL专线

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

我们希望保留声明结构或动作(高级)的能力, 而不必指定实现这些构造的模式(低级)。我们想克服DSL和编程语言之间的差异, 以便我们在需要时可以自由使用编程语言的全部功能。通过dsDSL, 这不仅可能而且很直接。

如果你在一年前问我, 我会以为dsDSL的概念是新颖的, 那么有一天, 我意识到JSON本身就是这种方法的完美示例!解析的JSON对象由以声明方式表示数据条目的数据结构组成, 以便获得DSL的优势, 同时还可以轻松地从编程语言内部进行解析和处理。 (可能还有其他dsDSL, 但到目前为止我还没有碰到。如果你知道其中一个, 非常感谢你在评论部分中提到它。)

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

  1. 它由很少的一组函数组成:JSON有两个主要函数, 即parse和stringify。
  2. 它的函数最常接收复杂和递归的参数:解析的JSON是一个数组或对象, 通常在其中包含其他数组和对象。
  3. 这些函数的输入遵循非常特定的形式:JSON具有显式且严格执行的验证架构, 以区分无效结构中的有效结构。
  4. 这些功能的输入和输出都可以由编程语言包含和生成, 而无需单独的语法。

但是dsDSL在许多方面都超越了JSON。让我们创建一个dsDSL, 以使用Java脚本生成HTML。稍后, 我将探讨该方法是否可以扩展到其他语言(扰流器:可以肯定地用Ruby和Python来完成, 但可能不能用C来完成)。

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

在dsDSL中表示HTML标签的一种直接方法是使用具有三个元素的数组:-标签:字符串。 -属性:对象(纯类型, 键/值类型)或未定义(如果不需要属性)。 -内容:字符串(文本), 数组(另一个标签)或未定义(如果没有内容)。

例如, <a href=”views”>索引</a>可以写为[‘a’, {href:’views’}, ‘Index’]。

如果要将此锚元素嵌入到具有类链接的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中有效结构的原因, 并且该结构将高度暗示其生成的基础内容。

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

  • 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属性。
  • 如果产品正在销售, 请在销售上放置一个类。
  • 按降序对产品分类。
  • 按类别过滤某些产品。如果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。

如果match为false, 我们将返回一个空数组(因此我们不打印此产品)。否则, 我们将返回具有正确ID和类的<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之外, 我们还可能具有drawHeader, drawFooter和其他类似功能, 所有这些功能都将返回dsDSL。

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

如果你不喜欢上面的代码的样子, 那么我说的话不会说服你。这是最好的dsDSL。你不妨停止阅读本文(也不要发表刻薄的评论, 因为如果你到目前为止已经获得了这样做的权利, 那么你就可以这样做!)。但是, 严重的是, 如果上面的代码没有给你带来优雅的效果, 那么本文中没有其他内容。

对于那些仍然与我在一起的人, 我想回到本节的主要主张, 即dsDSL具有高和低水平的优点:

  • 低级别的优势在于, 只要我们愿意, 就可以编写代码, 从而摆脱DSL的束缚。
  • 高级别的优势在于使用表示我们要声明的文字, 并让工具的功能将其转换为所需的最终状态(在这种情况下, 是带有HTML的字符串)。

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

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

由于表达式的所有重复模式都可以轻松抽象, 因此由大多数表达式组成并且将大多数语句封装在函数内的代码非常简洁。你可以编写任意代码, 只要该代码返回符合非常特定的, 非任意形式的文字即可。

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

超越Java单一真语言创建dsDSL可能吗?我认为, 只要该语言支持, 这是有可能的:

  • 用于以下内容的文字:数组, 对象(关联数组), 函数调用和lambda。
  • 运行时类型检测
  • 多态和动态返回类型

我认为这意味着dsDSL在任何现代动态语言(例如Ruby, Python, Perl, PHP)中都是站得住脚的, 但可能不是C或Java。

先走后滑:如何从低处展现高处

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

  1. 取两个到四个问题作为问题域的代表实例。这些问题应该是真实的。从低到高展开是一个归纳的问题, 因此你需要真实的数据才能提出有代表性的解决方案。
  2. 无需任何工具即可以最直接的方式解决问题。
  3. 退后一步, 仔细看一下你的解决方案, 并注意其中的常见模式。
  4. 查找表示模式(高级)。
  5. 查找生成模式(低级别)。
  6. 解决高级层的相同问题, 并验证解决方案确实正确。
  7. 如果你认为可以轻松地用表示模式来表示所有问题, 并且每个实例的生成模式都能实现正确的实现, 那么你就完成了。否则, 返回到绘图板。
  8. 如果出现新问题, 请使用该工具解决并进行相应修改。
  9. 无论解决了多少问题, 该工具都应渐近收敛到完成状态。换句话说, 该工具的复杂性应保持不变, 而不是随着其解决的问题而增加。

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

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

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

鸣叫

HTML的表示模式如下:

  • 单个标签:[‘TAG’]
  • 具有以下属性的单个标签:[‘TAG’, {attribute1:value1, attribute2:value2, …}]
  • 带有内容的单个标签:[‘TAG’, ‘CONTENTS’]
  • 具有属性和内容的单个标签:[‘TAG’, {attribute1:value1, …}, ‘CONTENTS’]
  • 一个标签, 其中包含另一个标签:[‘TAG1’, [‘TAG2’, …]]
  • 一组标签(独立或在另一个标签内):[[‘TAG1’, …], [‘TAG2’, …]]
  • 根据条件, 放置标签或不放置标签:condition? [‘TAG’, …]:[] /根据条件, 放置属性或不设置属性:[‘TAG’, {class:condition? ‘someClass’:未定义}, …]

这些实例可以用我们在上一节中确定的dsDSL表示法表示。这就是你表示可能需要的所有HTML所需要的。可以使用返回上面表示形式的函数的函数来实现更复杂的模式, 例如通过对象的条件迭代以生成表, 并且这些模式直接映射到HTML标签。

如果表示模式是你用来表达所需内容的结构, 则生成模式就是你的工具用来将表示模式转换为较低级别结构的结构。对于HTML, 这些如下:

  • 验证输入(这实际上是通用的生成方式)。
  • 打开和关闭标签(但不是空标签, 例如<input>, 它们是自动关闭的)。
  • 放置属性和内容, 转义特殊字符(但不包括<style>和<script>标记的内容)。

信不信由你, 这些是创建可生成HTML的展开式dsDSL层所需的模式。可以找到类似的模式来生成CSS。实际上, lith可以在大约250行代码中完成这两项。

最后一个问题有待回答:走路然后滑行是什么意思?当我们处理问题域时, 我们想使用一种工具, 该工具可以使我们从该域的令人讨厌的细节中了解我们。换句话说, 我们要扫地毯下面的低位, 越快越好。走然后滑的方法提出了完全相反的建议:花一些时间在底层。接受它的怪癖, 并了解哪些是必不可少的, 并且在面对一系列实际, 多样且有用的问题时可以避免。

在低层行走了一段时间并解决了有用的问题后, 你将对它们的领域有足够的了解。代表和产生的模式自然就会产生。它们完全源于要解决的问题的性质。然后, 你可以编写使用它们的代码。如果它们起作用, 那么你将能够解决最近不得不解决的问题。滑动意味着很多事情;它意味着速度, 精度和无摩擦。也许更重要的是, 可以感觉到这种品质。使用此工具解决问题时, 你是觉得自己正在解决问题, 还是觉得自己正在解决问题?

也许展开的工具最重要的事情不是它使我们不必处理低级的问题。相反, 通过捕获低级重复的经验模式, 一个好的高级工具可以使我们充分了解适用范围。

展开的工具不仅可以解决问题, 还可以启发你解决问题的结构。

因此, 不要逃避一个值得解决的问题。首先绕过它, 然后滑过它。

相关:并发编程简介:入门指南

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?