Ogma 0.1.1

Ogma 0.1.1

Mathias Quintero维护。



Ogma 0.1.1

Ogma

Ogma 是一个用 Swift 编写的轻量级解析框架。再见了复杂的有限状态机!现在您可以使用纯函数轻松编写解析器。

如何编写解析器?

一旦您了解想要解析的内容(有一个定义好的上下文无关文法会是很有帮助的),您就需要实现以下内容:

  • 您的模型:解析器结果的样子
  • 您的词法分析器:将输入字符串转换为标记的组件
  • 您的解析器:从词法分析器消费标记。解析器将接受标记作为输入并输出您的模型

安装

Ogma 通过 Cocoapods 提供。为了将 Ogma 添加到您的项目中,请在您的 Podfile 中添加它

pod 'Ogma', '~> 0.1'

然后运行

$ pod install

示例

目前,为了简化,我们将使用 Ogma 编写一个简单的计算器。我们的计算器将仅支持加法和乘法。我们现在不会在加法之前进行乘法。那是另一篇文章的内容。

让我们回到最初的想法:模型、词法分析器、解析器!

模型

我们可以将计算器的输入建模为一个表达式

public indirect enum Expression {

    public struct Addition {
        let lhs: Expression
        let rhs: Expression
    }

    public struct Multiplication {
        let lhs: Expression
        let rhs: Expression
    }

    case int(Int)
    case addition(Addition)
    case multiplication(Multiplication)
}

既然我们正在做这件事,我们不妨直接写一个经典的eval函数

extension Expression {

    func eval() -> Int {
        switch self {
        case .int(let int):
            return int
        case .addition(let lhs, let rhs):
            return lhs.eval() + rhs.eval()
        case .multiplication(let lhs, let rhs):
            return lhs.eval() * rhs.eval()
        }
    }

}

分词器

首先,让我们定义将要使用的标记。所以我们将允许整数、+*,以及开闭括号。

因此,我们的标记现在可以是一个枚举。为了清晰起见,我们建议使用名称空间将标记与您的模型一起使用。为了清楚地表示它们属于一起

extension Expression {

    public enum Token: TokenProtocol {
        case int(Int)
        case plus
        case times
    }

}

在标记清晰后,我们现在可以只编写一个分词器。分词器是一个组件,它将一个字符串转换为标记。构建它的最简单方法是我们所说的TokenGenerator。TokenGenerator尝试尽可能消耗字符串的开始部分以创建单一路标记。

要使用TokenGenerator,我们的分词器只需遵守GeneratorLexer并返回它将使用的生成器

extension Expression {

    enum Lexer: GeneratorLexer {
        typealias Token = Expression.Token

        static let generators: Generators = [
                IntLiteralTokenGenerator().map(Token.int),
                RegexTokenGenerator(pattern: "\\+").map(to: .plus),
                RegexTokenGenerator(pattern: "\\*").map(to: .times),
            ]
    }


}

现在这里有很多事情在发生。我们主要使用两种东西

  • IntLiteralTokenGenerator是由Ogma实现的已实现的TokenGenerator。它将一个字符串尝试解析一个整数。我们只是将这个生成器映射返回我们之前建模的标记。
  • RegexTokenGenerator是一个将返回与模式匹配的字符串的TokenGenerator。然后我们将每个实例映射到.plus。所以如果我们找到+,我们返回Token.plus

解析器

现在我们可以通过让我们的模型实现Parsable来构建我们的解析器。对一个对象来说,如果要成为Parsable,这只意味着有一个静态解析器可以返回类型。从开始,我们先为整数实现它。最简单的方法是为token写一个计算属性,当它有整数时

extension Expression.Token {
    var int: Int? {
        guard case .int(let int) = self else { return nil }
        return int
    }
}

并给出解析器的键路径

extension Int: Parsable {
    public typealias Token = Expression.Token

    public static let parser: AnyParser<Expression.Token, Int> = .consuming(keyPath: \.int)
}

现在,操作。通过使用&&运算符,我们可以链接多个解析器并顺序地期望结果

extension Expression.Addition: Parsable {
    public typealias Token = Expression.Token

    public static let parser: AnyParser<Expression.Token, Expression.Addition> = {
        let parser = Expression.self && .plus && Expression.self
        return parser.map { Expression.Addition(lhs: .int($0), rhs: $1) }
    }()
}

extension Expression.Multiplication: Parsable {
    public typealias Token = Expression.Token

    public static let parser: AnyParser<Expression.Token, Expression.Multiplication> = {
        let parser = Expression.self && .times && Expression.self
        return parser.map { Expression.Multiplication(lhs: .int($0), rhs: $1) }
    }()
}

您可以看到,我们声明乘法表达式由一个表达式、一个操作符和一个另一个表达式组成。Dogma将自动正确匹配它;)。

最后,我们使表达式Parsable。这次使用||运算符来表示它应取第一个解析器成功的返回值

extension Expression: Parsable {

    public static let parser: AnyParser<Expression.Token, Expression> = Addition.map(Expression.addition) ||
        Multiplication.map(Expression.multiplication) ||
        Int.map(Expression.int)

}

利润!💸

我们现在可以使用我们的解析器

extension String {

    func calculate() throws -> Int {
        return try Expression.parse(self, using: Expression.Lexer.self).eval()
    }

}

太棒了!我们刚刚编写了一个计算器,我们没有在任何地方使用状态机。事实上,我们所有的函数都是完全纯函数,可以很容易地扩展。

其他酷功能

忽略标记

一些语言选择完全忽略某些字符和表达式。这些是应该根本不会到达解析器的标记。例如:注释和空白字符。

如果你已经有了消耗这些的标记生成器,你只需在你的词法分析器中调用 忽略。例如,如果我们想在计算器中忽略空白字符

enum Lexer: GeneratorLexer {
	...

    static let generators: Generators = [
			WhiteSpaceTokenGenerator().ignore(),
			...
		]
}

二元运算符和运算符优先级

编写解析器时的另一个常见场景是处理运算符优先级。甚至在上面提到的例子中,关于 "+" 和 "-" 之间的优先级。Ogma 已经提供了一种编写二元运算和处理运算符优先级的方法。

注意:目前这仅在你事先知道所有可能的运算符时才有效。如果你打算编写一个支持自定义运算符的语言,这个 API 不适合你。

要使用二元运算符 API,你必须实现两个协议

  • BinaryOperator:基本上是运行运算符的标识符。这必须是可以比较的,以便我们知道优先级。值越小,优先级越高。
  • MemberOfBinaryOperation:这是你的运算符的左右两边。成员还知道它支持哪些运算符,并可以从运算符实例化。

示例

在我们的计算器案例中,我们现在可以废除加法乘法结构,转而使用二元运算。我们首先实现操作符

extension Expression {

	public enum Operator: Int, BinaryOperator {
		case multiplication
		case addition
		
		var token: Expression.Token {
			switch self {
			case .multiplication:
				return .times
			case .addition:
				return .plus
			}
		}
	} 

}

请注意,我们首先实现了乘法,然后是加法。这意味着既然操作符通过Int实现RawRepresentable,那么实际的优先级是先乘法后加法。我们对表达式进行了几个调整

public indirect enum Expression {
   	case int(Int)
	case operation(BinaryOperation<Expression>) 
}

并实现了二元运算的成员

extension Expression: MemberOfBinaryOperation {

	public init(from operation: BinaryOperation<Expression>) {
		self = .operation(operation)
	}

}

我们几乎完成了,现在我们只需要更新新的模型解析器

extension Expression: Parsable {

    public static let parser: AnyParser<Expression.Token, Expression> = BinaryOperation<Expression>.map(Expression.operation) ||
        Int.map(Expression.int)

}

字符串注解

有时你并不期望所有用户的输入都是有效的。也许你希望他们能够将你的语言内联在其他文本中。为此有AnnotatedString。注解字符串本质上只是一个枚举数组

public enum AnnotationElement<Annotation> {
    case text(String)
    case annotated(String, Annotation)
}

其中可以是未匹配的普通文本或带有解析值的注解部分。

现在你可以支持一些酷炫的功能,比如解析内联JSON

Hello, here's some JSON: { "Hello": "World" }

这将输出

[
	.text("Hello, here's some JSON:"),
	.annotated("{ \"Hello\": \"World\" }",
		 	   .dictionary(["Hello": .string("World"])),
]

事实上,你甚至不需要完整的解析器架构就能享受这个功能。如果你只想用正则表达式来注解可以表达的内容,那么一个词法分析器就足够了。

例如,如果你正在解析推文中的提及和标签

首先:构建一个词法分析器。

enum Twitter {

    enum Token: TokenProtocol {
        case mention(String)
        case hashtag(String)
    }

    enum Lexer: GeneratorLexer {
        typealias Token = Twitter.Token

        static let generators: Generators = [
            RegexTokenGenerator(pattern: "@(\\w+)", group: 1).map { .mention($0) },
            RegexTokenGenerator(pattern: "#(\\w+)", group: 1).map { .hashtag($0) },
        ]
    }

}

其次:调用注解

let tweet = try Twitter.Lexer.annotate(input) as AnnotatedString<Twitter.Token>

未来主题和已知问题

  • 一个类型只能实现一次可解析的。并且为一种Token类型。因此,如果你计划编写多个解析器,你不应该让标准库的类型实现Parsable

作者

致谢

本项目的实现得益于