响应式编程 函数式编程

tech2022-08-07  133

响应式编程 函数式编程

Phunkie is a library with functional structures for PHP. In this tutorial, Phunkie creator Marcello Duarte, head of training at Inviqa, explains how to create Parser combinators using the functional library. This post first appeared on the Inviqa blog, and was republished here with their permission.

Phunkie是一个具有PHP功能结构的库。 在本教程中,Punkie的创建者Marcello Duarte(Inviqa的培训负责人)介绍了如何使用功能库创建Parser组合器。 这篇文章首先出现在Inviqa博客上 ,并在他们的允许下在此处重新发布。

学习函数式编程 (Learning functional programming)

Functional programming really matters. Pure functions (or functions with no side effect) are the perfect units of behaviour because we can rely on them to build larger and more complex systems. The greatness of functional programming relies on this power of composability.

函数式编程确实很重要。 纯函数(或无副作用的函数)是行为的理想单位,因为我们可以依靠它们来构建更大,更复杂的系统。 函数式编程的伟大之处在于这种可组合性。

That’s something I first came to believe back in the days of my licentiate degree on Computing for Management, during a semester in AI with Lisp. My course curriculum was mostly focused on C/C++, therefore I had to stay focused on where the skills demand was.

这是我在Lisp的AI的一个学期中,获得管理计算学位的最初日子,我第一次相信这一点。 我的课程课程主要集中在C / C ++上,因此我必须继续专注于技能需求所在。

Thankfully, I’ve been able to reignite my love of studying functional programming here at Inviqa where we’re delivering more and more projects based on Scala, the general purpose programming language.

值得庆幸的是,我能够在Inviqa再次激发我学习函数式编程的热爱,在那里我们将基于通用编程语言Scala提供越来越多的项目 。

I’ve read “the red book” about three times (the Chiusano & Bjarnason one, not the Vaughn Vernon’s one). I took all the Martin Odersky’s Coursera courses, and I spent hours of my weekends watching videos and reading papers.

我读过《红皮书》大约三遍( Chiusano&Bjarnason读过一本 ,而不是Vaughn Vernon 读过一本)。 我参加了马丁·奥德斯基(Martin Odersky)的所有Coursera课程,并且在周末花费了数小时来观看视频和阅读论文。

And I set myself a challenge: to do with functional programming what I did when I was learning TDD and created PHPSpec (a BDD testing and design tool for PHP developers) to help me learn; I decided to write my own functional programming dialect in PHP. And so Phunkie was born!

而且,我给自己带来了一个挑战:使用函数式编程来学习我学习TDD并创建PHPSpec (PHP开发人员的BDD测试和设计工具)来帮助我学习; 我决定用PHP编写自己的函数式编程方言。 朋克就这样诞生了!

Now over to you. Learning functional programming means immersing yourself in a new paradigm, which can be challenging. It requires a totally different mindset for approaching problems.

现在交给您。 学习函数式编程意味着将自己沉浸在新的范式中,这可能是一个挑战。 解决问题需要完全不同的心态。

So, by the time you can use functional programming to solve real-world problems, you’ll have spent hours grasping the new thinking or getting clued-up on the theory.

因此,等到您可以使用函数式编程来解决实际问题时,您就已经花了数小时来掌握新思维或掌握理论。

In this tutorial, my aim is to help fast-track your journey by going over my implementation of Hutton & Meijer’s Monadic Parser Combinators. Hopefully you will be able to see both the thinking and the usefulness of functional programming.

在本教程中,我的目的是通过遍历我对Hutton&Meijer的Monadic Parser Combinators的实现来帮助您快速完成旅程。 希望您能够看到函数式编程的思想和实用性。

And, by using Phunkie, you can eliminate the need to learn Haskell to understand the topic. All you should need is some familiarity with PHP.

而且,通过使用Phunkie,您可以无需学习Haskell即可理解该主题。 您只需要对PHP有所了解。

So let’s get started!

因此,让我们开始吧!

解析器 (Parsers)

According to Terence Parr, parsing is the computer act of recognising a phrase. There are many strategies for parsing. We will be using recursive-descent parsing, one of the most basic ones, but one that’s powerful enough to implement some useful grammar constructs, such as sequencing, choice, and repetition.

根据Terence Parr的说法,解析是识别短语的计算机行为。 有很多解析策略。 我们将使用递归下降解析,这是最基本的解析之一,但是它足够强大,可以实现一些有用的语法构造,例如排序,选择和重复。

组合器 (Combinators)

If asked what functional programming is all about, I’d answer: composition. Functions are perfect units of behaviour that you can compose to create larger systems. Combinators are patterns of composition that have been around in functional programming long before any class or object were written. They are very elegant, too.

如果被问到什么是函数编程的全部,我会回答:组合。 函数是行为的完美单位,您可以将它们组合起来以创建更大的系统。 组合器是在编写任何类或对象之前就已经存在于函数式编程中的组合模式。 他们也很优雅。

We will implement parsers as functions. Only three very basic parsers are enough to help us create all the other parsers in this article.

我们将解析器实现为函数。 只有三个非常基本的解析器足以帮助我们创建本文中的所有其他解析器。

使用类型表示功能 (Using types to represent functions)

Let’s think of what a parser does. We give it a string. It then tries to figure out if that string matches any grammatical definition. If we have a match we keep the result as well as the remaining fragment of the string. If we fail, we stop.

让我们考虑一下解析器的功能。 我们给它一个字符串。 然后,它尝试找出该字符串是否与任何语法定义匹配。 如果匹配,则保留结果以及字符串的其余片段。 如果失败,我们就停止。

So what would that function declaration look like?

那么该函数声明是什么样的呢?

<?php function parse(string $toParse): mixed { // if it matches a grammar, it returns the match and the remaining string // if it doesn’t, it returns an empty set }

But a function can only return one result. So how are we returning a match plus the remaining string? We will use a Phunkie type: Pair. A pair allows you to group values of different types together, like so:

但是一个函数只能返回一个结果。 那么我们如何返回匹配项加上剩余的字符串呢? 我们将使用Phunkie类型: Pair 。 一对允许您将不同类型的值组合在一起,如下所示:

<?php // We will use the Phunkie immutable pair for our pair use Phunkie\Types\Pair; function parse(string $toParse): Pair { // if matches grammar return Pair($match, $remaining); }

So a parser type can be described like this: Parser = (String) -> Pair<Result, String>, Which reads: a parser is a function that takes a string and returns a pair composed of the parsing result and the remaining string to be parsed.

因此,解析器类型可以这样描述: Parser = (String) -> Pair<Result, String> ,其内容为:解析器是一个函数,它接受一个字符串并返回一个由解析结果和剩余字符串组成的对。被解析。

This is enough if we’re only interested in one parser. But what if we’re talking about combining parsers? The result Pair<Result, String> is not enough, so we will have a list of those pairs, where a pair will be the result of each parser. Taking that into account, a parser type would look like: Parser = (String) -> List<Pair<Result, String>>.

如果我们只对一个解析器感兴趣,这就足够了。 但是,如果我们要讨论合并解析器怎么办? 结果Pair<Result, String>是不够的,因此我们将获得这些对的列表,其中一对将是每个解析器的结果。 考虑到这一点,解析器类型将如下所示: Parser = (String) -> List<Pair<Result, String>> 。

This will be written in PHP using the class syntax in this way:

这将使用类语法以这种方式用PHP编写:

<?php // We will use the Phunkie immutable list for our list use Phunkie\Types\ImmList; /** * Represents a function (String) -> List<Pair<Result, String>> */ class Parser { /** * A function that takes a String and returns List of Pairs (Result, String) * * @var callable */ private $run; public function __construct(callable $run) { $this->run = $run; } /** * @param string $toParse * @return ImmList of pairs of (results, remaining) */ public function run(string $toParse): ImmList { return ($this->run)($toParse); } }

原始解析器 (Primitive parsers)

Now that we have our type definition we can start creating very basic parsers. The first one is a parser that always succeeds, without consuming any of the string. Rather useless, you may say. Well, we’re just getting started, and soon enough you’ll see what we’ll be able to do with these primitive parsers.

现在我们有了类型定义,我们可以开始创建非常基本的解析器了。 第一个是始终成功的解析器,而不消耗任何字符串。 您可能会说,它没用。 好吧,我们才刚刚开始,很快您就会看到我们将能够使用这些原始解析器进行处理。

Let’s start by creating an example of how we’d use it. We will call our parser result. The test would look like this:

让我们从创建一个如何使用它的示例开始。 我们将调用解析器result 。 测试看起来像这样:

<?php describe("Parser", function() { context("Basic parsers", function() { describe("result", function() { it("succeeds without consuming any of the string", function() { expect(result("hello")->run("world")) ->toEqual(ImmList(Pair("hello", "world"))); }); }); } }

And the implementation is quite simple:

实现非常简单:

<?php function result(string $a): Parser { return new Parser(function(string $s) use ($a): ImmList { return ImmList(Pair($a, $s)); }); }

That was easy. Now let’s build another primitive browser that always fails, regardless of what is given to it. A failure is represented by an empty list. An empty list in Phunkie is constructed with Nil(). We can call this parser: zero. The test would look like this:

那很简单。 现在,让我们构建另一个始终失败的原始浏览器,无论给出什么。 一个失败由一个空列表表示。 使用Nil()构造了Phunkie中的一个空列表。 我们可以将此解析器称为: zero 。 测试看起来像这样:

describe("zero", function(){ it("always return an empty list, which means failure", function(){ expect(zero()->run("world"))->toEqual(Nil()); }); });

And you can probably guess the implementation:

您可能会猜测实现:

function zero(): Parser { return new Parser(function($s): ImmList { return Nil(); }); }

Let’s cover one more basic parser. This time, a little bit more useful. This parser will consume the first character of a string. It will fail if the string is empty. Let’s see what it looks like. Here’s the example:

让我们再介绍一个基本的解析器。 这次更有用了。 该解析器将使用字符串的第一个字符。 如果字符串为空,它将失败。 让我们看看它是什么样的。 例子如下:

describe("item", function() { it("returns an empty list for an empty string", function() { expect(item()->run(""))->toEqual(Nil()); }); it("parses a string and returns the first character", function() { expect(item()->run("hello"))->toEqual(ImmList(Pair('h', "ello"))); }); });

And the implementation should be trivial:

并且实现应该很简单:

function item(): Parser { return new Parser(function(string $s): ImmList { return strlen($s) == 0 ? Nil() : ImmList(Pair($s[0], substr($s, 1))); }); }

This is it for primitive parsers. We can now capitalise on them and build combinators that combine the power of multiple parsers to build greater parsers.

对于原始解析器来说就是这样。 现在,我们可以利用它们并构建组合器,以组合多个解析器的功能来构建更大的解析器。

解析器组合器 (Parser combinators)

Let’s imagine we want to get the string "hello", apply the item parser, and then apply the item parser again. It would be nice to have a parser that lets you apply a sequence of two parsers and returns a pair of results. Let’s call that parser seq. Here is the example:

假设我们要获取字符串"hello" ,应用item解析器,然后再次应用item解析器。 拥有一个可以让您应用两个解析器的序列并返回一对结果的解析器会很好。 我们称该解析器为seq 。 这是示例:

describe("seq", function() { it("applies parsers in sequence", function() { expect(seq(item(), item())->run("hello")) ->toEqual(ImmList(Pair(Pair('h', 'e'), "llo"))); }); });

If we try and implement this from intuition, we will probably come up with some imperative style code we are used to see outside functional programming. A first attempt would look something like this:

如果我们试图凭直觉来实现这一点,我们可能会想出一些命令式风格的代码,这些代码通常用于外部函数式编程。 第一次尝试看起来像这样:

function seq(Parser $p, Parser $q): Parser { return new Parser(function($s) use ($p, $q) { // run the first parser, store the result $resP = $p->run($s); // run the second parser, using the remaining string from the first parser $resQ = $q->run($resP->head->_2); // return the pair with both results return ImmList(Pair(Pair($resP->head->_1, $resQ->head->_1), $resQ->head->_2)); }); }

Looks confusing, right? That’s because it is!

看起来很混乱,对吧? 那是因为!

This imperative style example is oversimplified. We’re not even checking if the first parser fails. What’s more, if we applied another seq to this, we would end up with nested tuples which is probably not a useful result for this parser.

此命令式示例过分简化。 我们甚至没有检查第一个解析器是否失败。 而且,如果对此应用另一个seq ,最终将得到嵌套的元组,这对于此解析器可能不是有用的结果。

You will remember that we defined the Parser type as: something which has a function that parses a string and returns a result and the remaining string. In more general terms, our Parser type represents a computation.

您会记得我们将Parser类型定义为:具有解析字符串并返回结果和剩余字符串的函数。 更笼统地说,我们的Parser类型表示计算。

The computation can be triggered when we call the method run. We do this a lot in modern functional programming, using types not just to represent a family of values, but to represent other computation elements.

当我们调用方法run时,可以触发计算。 在现代函数式编程中,我们经常这样做,不仅使用类型来表示一组值,还使用其他类型来表示其他计算元素。

In fact, the operation we are after here – getting the value from within the first (parsing) context and passing that value to another (parsing) context – is a well-known pattern within functional programming.

实际上,我们所要进行的操作-从第一个(解析)上下文中获取值并将该值传递给另一个(解析)上下文-是函数式编程中的一种众所周知的模式。

This abstraction over a value within a context is called a monad. By implementing the monad combinator method flatMap on our Parser class we can make parsers very composable – which is the whole point of using monads.

对上下文中的值的这种抽象称为monad。 通过在Parser类上实现monad组合器方法flatMap ,我们可以使解析器非常可组合-这就是使用monad的全部目的。

Let me show you one possible implementation of flatMap for Parser, then we can go over it, step by step.

让我向您展示flatMap for Parser一种可能的实现,然后我们将逐步介绍它。

class Parser { private $run; public function __construct(callable $run) { $this->run = $run; } public function run(string $toParse): ImmList { return ($this->run)($toParse); } public function flatMap(callable $f): Parser { return new Parser(function(string $s) use ($f) { return $this->run($s)->flatMap(function(Pair $result) use ($f) { return $f($result->_1)->run($result->_2); }); }); } }

What this method is doing is creating a fresh Parser using both its own parser function and a new parser function passed to it. First we call run: $this->run("hello"). If $this was an item parser, this will return something like ImmList(Pair("h", "ello")).

这是什么方法做的是创造一个新的Parser使用它自己的解析器函数,并传递给它一个新的解析器功能。 首先我们调用run : $this->run("hello") 。 如果$this是一个item解析器,它将返回类似ImmList(Pair("h", "ello")) 。

Once we have the result of applying the Parser to the input string we can then apply the other parser function to this result. Well yes, the result is inside a list! So how do you get something from inside a context? Oh yeah, monads! Yes, lists are monads too. So we flatMap on the list to get access to the result: $this->run($s)->flatMap(function(Pair $result). Now we can apply the given function to the result pair. We can access the first and the second element of the pair using the _1 and _2 read-only accessors: return $f($result->_1)->run($result->_2);.

一旦获得将解析器应用于输入字符串的结果,便可以将另一个解析器函数应用于此结果。 好吧,结果在列表中! 那么,如何从上下文中得到一些东西呢? 哦,是的,单子! 是的,列表也是单子。 因此,我们在列表上的flatMap可以访问结果: $this->run($s)->flatMap(function(Pair $result) 。现在,我们可以将给定的函数应用于结果对,我们可以访问第一个并使用_1和_2只读访问器访问该对中的第二个元素:return $f($result->_1)->run($result->_2) ;。

Another important combinator is map. Whilst the function given to flatMap has to return a Parser, a function given to map can return any value, and the map method will make sure it is returned into the context in which the function was mapped.

另一个重要的组合器是map 。 提供给flatMap的函数必须返回Parser ,提供给map的函数可以返回任何值,并且map方法将确保将其返回到映射该函数的上下文中。

public function map(callable $f) { return new Parser(function(string $s) use ($f) { return $this->run($s)->map(function(Pair $result) use ($f) { return Pair($f($result->_1), $result->_2); }); }); }

Note in the code example how the returned value of $f is used to create the result of composing parser. map is usually used at the end of a sequence of flatMaps, simply to organise the values you want returned, ensuring they’re in the context you need.

注意在代码示例中,如何使用$f的返回值创建组合解析器的结果。 map通常用在flatMaps序列的flatMaps ,只是为了组织您要返回的值,以确保它们位于所需的上下文中。

Let’s consider an implementation of seq using monads. Note how we use map to expose just the result we expect to see:

让我们考虑使用monads实现seq 。 请注意,我们如何使用map来公开我们期望看到的结果:

function seq(Parser $p, Parser $q): Parser { return $p->flatMap(function($x) use ($q) { return $q->map(function($y) use ($x) { return Pair($x, $y); }); }); }

In fact this pattern is so common in functional programming that FP languages usually have a special syntax for it. Here is how it looks in Phunkie (0.6.0):

实际上,这种模式在函数式编程中非常普遍,以至于FP语言通常对此具有特殊的语法。 这是Phunkie(0.6.0)中的外观:

function seq(Parser $p, Parser $q): Parser { return for_( __($x)->_($p), __($y)->_($q) )->yields($x, $y); }

I know what you’re thinking: this seq parser is very neat, but what’s the use of combining parsers if our parsers can only parse the first character of a string? Well, now that we’ve covered the basics using examples, we can move onto questions like this.

我知道您在想什么:此seq解析器非常简洁,但是如果我们的解析器只能解析字符串的第一个字符,则组合解析器有什么用? 好了,既然我们已经使用示例介绍了基础知识,那么我们可以继续探讨诸如此类的问题。

Stay tuned for part II in this series, which will cover more sequencing and other strategies.

请继续关注本系列的第二部分,它将涵盖更多的排序和其他策略。

翻译自: https://www.sitepoint.com/functional-programming-with-phunkie-parser-combinators-in-php/

响应式编程 函数式编程

相关资源:jdk-8u281-windows-x64.exe
最新回复(0)