65.9K
CodeProject 正在变化。 阅读更多。
Home

我如何构建自己的简单 (DSL) 领域特定语言,以及你也可以!

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2014年7月28日

CPOL

7分钟阅读

viewsIcon

27616

简单 (DSL) 领域特定语言

仅供参考,我正在谈论这个……

http://www.youtube.com/embed/ZfdAwV0HlEU

前导码

所以一个项目要求我接受以下语句

"C#" or "Vb.net" and Not "Java"

并根据它创建一个过滤器,该过滤器等同于以下逻辑。

if((file.Contains("C#")||file.Contains("Vb.net"))&&(!file.Contains("Java")))
{
  SearchResult.Add(file);
}

这正是我们业内所说的迷你语言。本质上是一种特别小巧且(希望)易于理解的编程语言,我们将其用于非常特定的目的,是一种领域特定语言(简称DSL)。为了实现这一点,我们转向一个称为词法分析的过程。

  1. 将表达式分解为可管理的部分。被称为“扫描器”或“词法分析器”;这是词法分析过程的前半部分。
  2. 将这些部分转换为系统可以评估的形式,以便与“处理值”进行交互。(无论是数据库访问、编程逻辑、建模模式或其他)被称为“评估器”或“解析器”,这是词法分析过程的后半部分。

为什么选择这个?为什么是现在?

有很多方法可以做到这一点(我来回尝试了几种),这也不是我第一次有幸尝试它们,但它并不经常发生,所以我写这篇文章是为了巩固我的记忆,或者至少有一些随时可用的东西,这样我就不会“忘记记住”如何做这类事情。

你应该知道的事情

编程逻辑不包含任何明显特定于语言的内容(尽管我确实在某些地方使用Linq来过滤结果。这只相当于根据一组特定标准循环遍历一系列值并执行过滤操作)。我在此项目中使用的最专业的工具是正则表达式(一种功能异常强大的字符串搜索和评估DSL,我认为作为程序员,你真的应该对它们有所了解,因为它们在许多情况下完全帮了我大忙,节省了大量额外的时间和工作)。如果你的现有语言不支持它们或类似强大的工具,那么你将会过得很糟糕

计划

它会是什么样子?

在Linqpad中进行修改,以下表示了这种设想的方式。

void Main()
{
    //The user input expression
    var expression = "\"java\" and \"SQL\" and \"C#\" or \"VB.NET\""; 
    
    //The collection of tokens that we will use to evaluate the logic in the expression
    var tokens = Tokenize(expression);
    //tokens.Dump();
    
    //Our evaluated representation of the tokens; We can use this to 
    //convert the expression to whatever language we wish
    var ConditionNode = Eval(tokens);
    
    //Transforms our Nodes into our desired programming language
    var endResult = CreateQueryString(ConditionNode );
    
}

这就是我们通往天堂的道路。

The Tokenizer AKA Scanner's job is breaking down string expressions into manageable parts or "Tokens".

词法分析器(即扫描器)的工作是将字符串表达式分解成可管理的部分或“标记”。

词法分析器

标记化过程开始了。它相当于获取用户表达式

"C#" or "Vb.net" and Not "Java"

并将其分割成片段,这样我们就得到了一组看起来像这样的字符串。

  1. C#
  2. Vb.net
  3. Java

如果你还没有学习正则表达式(说真的,他妈的学习它们),这是它做什么的总结(或多或少)。
查找一个可选的“not”后面不跟空白字符,作为一个捕获组来存储“not”,然后是一个以双引号开头,包含任意数量字符并以双引号“(这是我们对字符串的表示)结尾的表达式,可选地后面跟着至少一个空白字符和一个逻辑操作,该操作标识条件与其右侧兄弟(如果存在)的关系。这将评估整个字符串的长度,并相应地捕获每个匹配的表达式。见第12行。

public static List<string> Tokenize(string expression)
{
    //essentiall replaces double quotes with &quot; and apostrophys with "&apos;"
    //I prefer dealing with searching for &quot; then " as they tend to be a pain in the regex
    var tempExpression = System.Security.SecurityElement.Escape(expression);
    string doubleQuote = "&quot;";
    
    //looks for an optional not and whitespace then an expression starting with a quote 
    //and ending with a quote" our representation of a string
    //followed by whitespace and an optional logical operation that identifies the 
    //relationship of the condition to it sibling on the right if there is one
    var regEx = string.Format("(not\\s+)?{0}(.+?){0}(and|or)?",doubleQuote);
    Regex RE = new Regex(regEx);
    
    //splits the expression by each capture group and trims the result 
    var result = (RE.Split(tempExpression)).Select (r => r.Trim());
    
    //While there shouldnt be any additional whitespace in the expression 
    //i make a second pass at removing it and converting the expression to a list 
    //(so I can use linq on it later)
    return result.Where (re => !String.IsNullOrWhiteSpace(re)).ToList();
}

有了它,我们就有了我们的标记集合。

做好必要的准备。因为我们将拂晓解析!

所以,在我们能将标记解析成有意义的东西之前,我们必须决定需要检索哪些数据以及需要如何处理它们。再次审视我们的需求和文本

"C#" or "Vb.net" and Not "Java"
if((file.Contains("C#")||file.Contains("Vb.net"))&&(!file.Contains("Java")))
{
  SearchResult.Add(file);
}

我们之前已经确定我们需要一个基本的字符串来存储我们正在过滤的内容,但我们还需要处理 NOT (!)OR (||)AND (&&)。在计算机编程以及自然语言(在这种情况下是英语)以及几乎任何其他语法中,这些都被称为逻辑连接词或更常见的逻辑运算符。逻辑运算符允许我们无需大量额外对话(即复合句)即可表达多个思想。没有它们,句子

“杰克和吉尔上山了。”

变为

“杰克上山了。”
“吉尔上山了。”

(哎呀……如果人们一直这样说话,我想我可能会有点抓狂。)

逻辑运算符的功能远不止于此,但就我们的要求而言,这应该足以让您了解它们在几乎所有形式的语言(无论是编程语言还是其他语言)核心中的重要性。

所以我们有了一个可以用来解析的类。

回顾一下:到目前为止我们所拥有的就是我们的“条件”。它赋予了我们的解析器存在的理由。

class Condition
- string expression
- bool HasNot
- LogicalOperator LogicalOperator

关于“LogicalOperator”类型,它只是我们关注的所有可能逻辑运算符的枚举,看起来像这样。

enum LogicalOperator
{
  None = 0,
  And = 1,
  Or = 2,
}

现在我们或许可以就此止步,只做一个“条件”集合,并且**可能**暂时蒙混过关。然而,那并不能准确反映集合中各项之间的关系,而且在长远来看,如果我们想实现更多的语言特性(例如额外的运算符)来使我们的微型 DSL 有朝一日成为一个强大而健全的语言,那么实现我们的解析器(以及随之而来的维护)将变得更加困难。正因为如此,我们将使用节点来构建数据结构。

节点……到处都是节点!!!!

就我们的目的而言,节点是一种数据结构,它可以理解其与相邻节点的关系。一组彼此之间存在关系的节点就构成了一棵树。现在,就像大多数讨论点一样,计算机科学的这个领域还有大量其他研究领域和讨论主题,我鼓励你继续探索人们用节点和树做的所有酷炫事情,但是,就像本文中涵盖的许多内容一样,我的目标是保持简单。

所以,我在这里有一个C#中的简单节点示例

public class Node
{
  public string Value{get;set;}
  public Node Right {get;set;}
}

本质上,就是这样。更有趣的部分是如何遍历这些东西。在C#中,这也没什么难的……

public static Node MoveRight(this Node node)
{
    return node.Right;
}

注意*这不包括空节点。它将在空节点上中断并抛出空引用异常。如果你打算导航到树中的最后一个节点,那只是一种递归。//进入树中的最后一个节点

public static Node GetRightMostNode(Node node)
{
  while(node.Value!=null)
  {
    node = node.MoveRight();
  }
  return node;
}

如果我想获取树中的所有值,也是一样

public static string GetValues(Node node)
{
  StringBuilder sb = new StringBuilder();
  while (node.Value!=null)
  {
    sb.Append(node.Value);
    node = node.Right;
}
  return sb.ToString();
}

将字符串集合转换为节点

public static void RecurseSet(this Node node, List<string> vals)
{
  Node tNode;
  for(var i=0;i&lt;vals.Count();i++)
  {
    tNode = new Node();
    tNode.Value = vals[i];
    node.Right = tNode;
    vals.Remove(vals[i]);
    RecurseSet(tNode,vals);
  }
}

这是在linqpad中编写的完整示例的链接。

把它带回家

有了额外的节点和树经验,让我们回到并对我们的条件类进行一些“增强”。

我们现在有了……

public class Condition
{
  public int Order {get;set;}
  public string expression {get;set;}
  public LogicalOperator LogicalOperator {get;set;}
  public bool HasNot{get;set;}
  public Condition Right {get;set;}
}

注意*顺序被用作验证节点树与标记集合顺序的一种方法,并且是完全可选的。遍历方法大致相同。

public static Condition MoveRight(this Condition node)
{
    return node.Right;
}

最后,我们只需要将我们的树转换为我们需要的任何形式。在这种情况下,我们将用它来构建我们的C#表达式的if语句……只是因为。

public static string CreateCode(Condition node)
{
    StringBuilder sb = new StringBuilder();
    sb.Append("if(");
    while (node!=null)
    {
        if(node.HasNot)
        {
            sb.Append("!");
        }
        
        sb.Append("val.Contains==");

        sb.Append("\""+node.expression+"\" ");

        if(node.LogicalOperator!=LogicalOperator.None)
        {
            sb.Append(GetLogicOperationSymbol(node.LogicalOperator) +" ");
        }
        node = node.Right;

    }
    sb.Append(")");
    sb.AppendLine("");
    sb.AppendLine("{");
    sb.AppendLine("//do stuff");
    sb.AppendLine("}");

    return sb.ToString();
}

public static string GetLogicOperationSymbol(LogicalOperator op)
{
    var retval= "";
    switch (op)
    {
        case LogicalOperator.And:
            retval= "&&";
            break;
        case LogicalOperator.Or:
            retval= "||";
            break;
        case LogicalOperator.Not:
            retval= "!";
            break;
        default:
            break;
    }
    return retval;
}

女士们先生们,这只是构建您自己的微型DSL的一种方式。

我想指出其他几点。

  • 代替条件类,整个类可以被分割成包含更简单部分的节点标记。这种实现对我来说很有效,但你完全可以自由尝试更多树节点的好处。
  • 市面上有这样的工具:这些工具能够轻松地执行本文中提到的许多解析和标记化相关任务,甚至更多,现在你已经掌握了手动实现的技巧。

我第一次了解迷你语言、DSL以及大量其他很棒的东西,是在阅读《Unix编程艺术》这本书时。这是一本由埃里克·史蒂文·雷蒙德(开源运动的教父之一)免费提供的书籍。

© . All rights reserved.