├── README.md ├── json.php ├── parser.php └── test.php /README.md: -------------------------------------------------------------------------------- 1 | # Функции высших порядков и монады для PHP`шников 2 | 3 | Среди PHP программ преобладает процедурный или в последних 4 | версиях частично объектно-ориентированный стиль программирования. 5 | Но можно писать и иначе, в связи с чем хочется рассказать о 6 | функциональном стиле, благо кое-какие инструменты для этого 7 | имеются и в PHP. 8 | 9 | Поэтому мы рассмотрим реализацию парсера JSON в виде простейших 10 | функций и функций их комбинирующих в более сложные, постепенно 11 | дойдя до полноценного парсера JSON формата. 12 | 13 | Кроме собственно функционального подхода можно обратить внимание 14 | на использование классов для создания DSL-подобного синтаксиса 15 | и на использование генераторов для упрощения синтаксиса комбинаторов. 16 | 17 | ## Функциональный стиль 18 | 19 | Как программист справляется с огромной сложностью программ? 20 | Он берет простые блоки и строит из них более сложные, из которых 21 | строятся еще более сложные блоки и в конце-концов программа. 22 | По крайней мере так было после появляния первых языков с подпрограммами. 23 | 24 | В основе процедурного стиля лежит описание процедур, которые 25 | вызывают другие процедуры вместе меняющие какие-то общие данные. 26 | Объекто-ориентированный стиль добавляет возможность описывать структуры 27 | данных, составленные из других структур данных. Функциональный же 28 | стиль использует композицию (соединение) функций. 29 | 30 | Чем же отличается композиция функций от композиции процедур и объектов? 31 | Основа функционального подхода -- чистота функций, это значит, что 32 | результат работы функций зависит только от входных параметров. 33 | Если функции чисты, то гораздо проще предсказать результат их композиции 34 | и даже создать готовые функции для преобразования других функций. 35 | 36 | Именно функции принимающие и/или возвращающие в качестве результата 37 | другие функции называются функциями высшего порядка и представляют 38 | тему этой статьи. 39 | 40 | ## Какую задачу будем решать? 41 | 42 | Для примера возьмем задачу, которую не очень просто решить целиком 43 | и посмотрим как функциональный подход поможет нам эту задачу упростить. 44 | 45 | Для примера попробуем сделать парсер формата JSON, который из строки 46 | JSON получит соответствующий PHP объект: число, строку, список или 47 | ассоциированный список (со всеми возможными вложенными значениями конечно). 48 | 49 | ### Что такое парсер? 50 | 51 | Начнем с самых простых элементов: мы пишем парсер, что же это такое? 52 | Парсер это функция, которая берет строку и в случае успеха возвращает 53 | пару значений: результат разбора и остаток строки (если разбираемое значение 54 | занимало не всю строку) или пустой набор, если разобрать строку не удалось: 55 | 56 | ``` 57 | Parser: string => [x,string] | [] 58 | ``` 59 | 60 | Например если у нас есть функция-парсер `number` то мы могли бы написать такие тесты: 61 | 62 | ```php 63 | assert(number('123;') === [123,';']); 64 | assert(number('none') === []); 65 | ``` 66 | 67 | **DISCLAMER**: PHP имеет не слишком удобный синтаксис для работы с функциями, 68 | поэтому для более простого и понятного кода мы будем использовать класс, 69 | который является ни чем иным, как просто оберткой вокруг функции парсера и 70 | нужен для указания типов и для использования удобного синтаксиса цепочных 71 | вызовов, о чем подробнее поговорим дальше. 72 | 73 | ```php 74 | class Parser { 75 | const FAILED = []; 76 | private $parse; 77 | function __construct(callable $parse) { 78 | $this->parse = $parse; 79 | } 80 | function __invoke(string $s): array { 81 | return ($this->parse)($s); 82 | } 83 | } 84 | ``` 85 | 86 | Но будем помнить, что `Parser` это не более чем функция `string => array`. 87 | 88 | Для удобства так же введем функцию `parser`, которую будем использовать 89 | вместо вызова конструктора `new Parser` для краткости: 90 | 91 | ```php 92 | function parser($f, $scope = null) { 93 | return new Parser($f->bindTo($scope)); 94 | } 95 | ``` 96 | ## Простейшие парсеры 97 | 98 | Итак, мы разобрались что такое парсеры, но ни одного так и не написали, 99 | давайте это исправим. Вот пример парсера, который всегда возвращает `1`, 100 | независимо от исходной строки: 101 | 102 | ```php 103 | $alwaysOne = parser(function($s) { 104 | return [1, $s]; 105 | }); 106 | assert($alwaysOne('123') === [1, '123']); 107 | ``` 108 | 109 | Полезность этой функции не очевидна, давайте сделаем ее более общей и 110 | объявим функцию, которая позволит создавать подобный парсер для любого 111 | значения: 112 | 113 | ```php 114 | function just($x): Parser { 115 | return parser(function($s) use ($x) { 116 | return [ $x, $s ]; 117 | }); 118 | } 119 | ``` 120 | 121 | Пока все просто, но все еще не очень полезно, ведь мы хотим парсить строку, 122 | а не возвращать всегда одно и то же. Давайте сделаем парсер, который 123 | возвращает первые несколько символов входной строки: 124 | 125 | ```php 126 | function take(int $n): Parser { 127 | return parser(function($s) use ($n) { 128 | return strlen($s) < $n ? Parser::FAILED : [ substr($s, 0, $n), substr($s, $n) ]; 129 | }); 130 | } 131 | 132 | test(take(2), 'abc', ['ab','c']); 133 | test(take(4), 'abc', Parser::FAILED); 134 | ``` 135 | 136 | Уже лучше, первый наш парсер, который действительно разбирает строку! Напомню: 137 | мы описываем простейшие кирпичики из который сможем собрать более сложный парсер. 138 | Поэтому для полноты картины нам нехватает только парсера, который ничего не парсит 139 | вообще :) 140 | 141 | ```php 142 | function none(): Parser { 143 | return parser(function($s) { 144 | return Parser::FAILED; 145 | }); 146 | } 147 | ``` 148 | Он нам еще пригодится. 149 | 150 | Вот и все парсеры, которые нам нужны. Этого достаточно, чтобы разобрать JSON. 151 | Не верите? Осталось придумать способ собирать эти кирпичики в более сложные блоки. 152 | 153 | ## Собираем кирпичики вместе 154 | 155 | Поскольку мы решили заняться функциональным программированием, то для комбинирования 156 | функций парсеров в более сложные парсеры логично использовать функции! 157 | 158 | Например если у нас есть парсеры `first` и `second` и мы хотим применить к строке любой из них 159 | мы можем определить комбинатор парсеров -- функцию, создающую новый парсер на основе 160 | существующих: 161 | 162 | ```php 163 | function oneOf(Parser $first, Parser $second): Parser { 164 | return parser(function($s) use ($first,$second) { 165 | $result = $first($s); 166 | if ($result === Parser::FAILED) { 167 | $result = $second($s); 168 | } 169 | return $result; 170 | }); 171 | } 172 | test(oneOf(none(),just(1)), '123', [1,'123']); 173 | ``` 174 | 175 | Но, как уже упоминалось выше, такой синтаксис может быстро стать нечитаемым 176 | (например `oneOf($a,oneOf($b,oneOf($c,$d)))`), поэтому мы перепишем эту 177 | (и все следующие) функции как методы класса `Parser`: 178 | 179 | ```php 180 | function orElse(Parser $alternative): Parser { 181 | return parser(function($s) use ($alternative) { 182 | $result = $this($s); 183 | if ($result === Parser::FAILED) { 184 | $result = $alternative($s); 185 | } 186 | return $result; 187 | }, $this); // <- вот зачем в функции parser был bindTo: чтобы использовать $this в функции 188 | } 189 | test(none()->orElse(just(1)), '123', [1,'123']); 190 | ``` 191 | 192 | Так уже лучше, можно писать `$a->orElse($b)->orElse($c)->orElse($d)` вместо того, что было выше. 193 | 194 | И еще одна, не такая простая, но зато гораздо более мощная функция: 195 | 196 | ```php 197 | function flatMap(callable $f): Parser { 198 | return parser(function($s) use ($f) { 199 | $result = $this($s); 200 | if ($result != Parser::FAILED) { 201 | list ($x, $rest) = $result; 202 | $next = $f($x); 203 | $result = $next($rest); 204 | } 205 | return $result; 206 | }, $this); 207 | } 208 | ``` 209 | 210 | Давайте разберемся с ней подробнее. Она принимает функцию `f: x => Parser`, которая 211 | принимает результат парсинга нашего существующего парсера и возвращает на его 212 | основе новый парсер, который продолжает разбор строки с того места, где остановился 213 | наш предыдущий парсер. 214 | 215 | Например: 216 | 217 | ```php 218 | test(take(1), '1234', ['1','234']); 219 | test(take(2), '234', ['23', '4']); 220 | test( 221 | take(1)->flatMap(function($x) { # x -- результат парсинга take(1) 222 | return take(2)->flatMap(function($y) use ($x) { # y -- результат парсинга take(2) 223 | return just("$x~$y"); # -- финальный результат 224 | }); 225 | }), 226 | '1234', 227 | ['1~23','4'] 228 | ); 229 | ``` 230 | 231 | Таким образом мы скомбинировали `take(1)`, `take(2)` и `just("$x~$y")` и получили довольно сложный 232 | парсер, который сначала парсит один символ, за ним еще два и возвращает их в виде `$x~$y`. 233 | 234 | Основная особенность проделанной работы состоит в том, что мы описываем, что делать с результатами 235 | парсинга, но сама разбираемая строка тут не участвует, у нас нет возможности ошибиться и тем, 236 | какую часть строки куда передавать. А дальше мы увидим как сделать синтаксис таких комбинаций 237 | более простым и читабельным. 238 | 239 | Эта функция позволит нам описать несколько других полезных 240 | комбинаторов: 241 | 242 | ```php 243 | function onlyIf(callable $predicate): Parser { 244 | return $this->flatMap(function($x) use ($predicate) { 245 | return $predicate($x) ? just($x) : none(); 246 | }); 247 | } 248 | ``` 249 | 250 | Этот комбинатор позволяет уточнить действие парсера и проверить его результат 251 | на соответствие какому-то критерию. Например с его помощью мы постром очень полезный парсер: 252 | 253 | ```php 254 | function literal(string $value): Parser { 255 | return take(strlen($value))->onlyIf(function($actual) use ($value) { 256 | return $actual === $value; 257 | }); 258 | } 259 | test(literal('test'), 'test1', ['test','1']); 260 | test(literal('test'), 'some1', []); 261 | ``` 262 | 263 | ## DO нотация 264 | 265 | Мы уже описали простейшие парсеры `take`, `just` и `none`, способы их комбинации (`orElse`,`flatMap`,`onlyIf`) 266 | и даже описали с их помощью парсер литералов `literal`. 267 | 268 | Теперь мы приступим к построению более сложных парсеров, но перед этим хотелось бы 269 | сделать способ их описания более простым: комбинирующая функция `flatMap` позволяет нам 270 | многое, но выглядит это не очень. 271 | 272 | В связи с этим подсмотрим как решают эту проблему другие языки. Так в языках Haskell и Scala 273 | есть весьма удобный синтаксис для работы с подобными вещами (они даже имеют свое название -- 274 | монады), называется он (в Haskell) DO-нотация. 275 | 276 | Что по-сути делает `flatMap`? Он позволяет описать что делать с результатом парсинга не совершая 277 | собственно парсинга. Т.е. процедура как-бы приостанавливается до момента получения 278 | промежуточного результата. Для реализации подобного эффекта можно использовать новый для PHP 279 | синтаксис -- генераторы. 280 | 281 | ### Генераторы 282 | 283 | Давайте сделаем небольшое отступление и рассмотрим что такое генераторы. 284 | 285 | В PHP 5.5.0 и выше появилась возможность описать функцию: 286 | 287 | ```php 288 | function generator() { 289 | yield 1; 290 | yield 2; 291 | yield 3; 292 | } 293 | foreach (generator() as $i) print $i; # -> 123 294 | ``` 295 | 296 | Что более интересно для нас, данные можно не только получать из генератора, но и передавать 297 | в него через `yield`, и даже с 7 версии получить результат генератора через `getReturn`: 298 | 299 | ```php 300 | function suspendable() { 301 | $first = yield "first"; 302 | $second = yield "second"; 303 | return $first.$second; 304 | } 305 | $gen = suspendable(); 306 | while ($gen->valid()) { 307 | $current = $gen->current(); 308 | print $current.','; 309 | $gen->send($current.'!'); 310 | } 311 | print $gen->getReturn(); 312 | # -> first,second,first!second! 313 | ``` 314 | 315 | Это можно использовать, чтобы спрятать вызовы `flatMap` от программиста. 316 | 317 | ### `flatMap` используя комбинаторы 318 | 319 | ```php 320 | function _do(Closure $gen, $scope = null) { 321 | $step = function ($body) use (&$step) { 322 | if (! $body->valid()) { 323 | $result = $body->getReturn(); 324 | return is_null($result) ? none() : just($result); 325 | } else { 326 | return $body->current()->flatMap( 327 | function($x) use (&$step, $body) { 328 | $body->send($x); 329 | return $step($body); 330 | }); 331 | } 332 | }; 333 | $gen = $gen->bindTo($scope); 334 | return parser(function($text) use ($step,$gen) { 335 | return $step($gen())($text); 336 | }); 337 | } 338 | ``` 339 | 340 | Эта функция берет каждый `yield` в генераторе (который содержит парсер, результат которого мы хотим 341 | получить) и комбинирует его с оставшимся фрагментом кода (в виде рекурсивной функции `step`) через 342 | `flatMap`. 343 | 344 | То же самое без рекурсии и функции `flatMap` можно было бы записать так: 345 | 346 | ```php 347 | function _do(Closure $gen, $scope = null) { 348 | $gen = $gen->bindTo($scope); # ради использования $this в функции 349 | return parser(function($text) use ($gen) { 350 | $body = gen(); 351 | while ($body->valid()) { 352 | $next = $body->current(); 353 | $result = $next($text); 354 | if ($result === Parser::FAILED) { 355 | return Parser::FAILED; 356 | } 357 | list($x,$text) = $result; 358 | $body->send($x); 359 | } 360 | return $body->getReturn(); 361 | }); 362 | } 363 | ``` 364 | 365 | Но первая запись более интересна тем, что она не привязана особо к парсерам, от них там только 366 | функции `flatMap`, `just` и `none` (да и то, можно было бы переписать `just` чтобы обрабатывать 367 | null особым образом и обойтись без `none`). 368 | 369 | Объекты, которые можно комбинировать с помощью двух методов `flatMap` и `just` называют монады 370 | (это немного упрощенное определение) и этот же код можно использовать, чтобы писать комбинаторы 371 | для промисов (Promise), опциональных значений (Maybe,Option) и многих других. 372 | 373 | Но ради чего же мы писали эту не самую простую функцию? Для того, чтобы дальнейшее использование 374 | `flatMap` было гораздо легче. Сравним один и тот же код с чистым `flatMap`: 375 | 376 | ```php 377 | test( 378 | take(1)->flatMap(function($x) { 379 | return take(2)->flatMap(function($y) use ($x) { 380 | return just("$x~$y"); 381 | }); 382 | }), 383 | '1234', 384 | ['1~23','4'] 385 | ); 386 | ``` 387 | и тот же самый код, но написанный через `_do`: 388 | 389 | ```php 390 | test( 391 | _do(function() { 392 | $x = yield take(1); 393 | $y = yield take(2); 394 | return "$x~$y"; 395 | }), 396 | '1234', 397 | ['1~23','4'] 398 | ); 399 | ``` 400 | Результирующий парсер делает то же самое тем же способом, но читать и писать 401 | такой код гораздо проще! 402 | 403 | ## Строим более сложные парсеры и комбинаторы 404 | 405 | Теперь, используя эту нотацию мы можем написать еще несколько полезных парсеров: 406 | 407 | ```php 408 | function takeWhile(callable $predicate): Parser { 409 | return _do(function() use ($predicate) { 410 | $c = yield take(1)->onlyIf($predicate)->orElse(just('')); 411 | if ($c !== '') { 412 | $rest = yield takeWhile($predicate); 413 | return $c.$rest; 414 | } else { 415 | return ''; 416 | } 417 | }); 418 | } 419 | function takeOf(string $pattern): Parser { 420 | return takeWhile(function($c) use ($pattern) { 421 | return preg_match("/^$pattern$/", $c); 422 | }); 423 | } 424 | test(takeOf('[0-9]'), '123abc', ['123','abc' ]); 425 | test(takeOf('[a-z]'), '123abc', [ '','123abc']); 426 | ``` 427 | 428 | И полезные методы класса `Parser` для повторяющихся элементов: 429 | 430 | ```php 431 | function repeated(): Parser { 432 | $atLeastOne = _do(function() { 433 | $first = yield $this; 434 | $rest = yield $this->repeated(); 435 | return array_merge([$first],$rest); 436 | },$this); 437 | return $atLeastOne->orElse(just([])); 438 | } 439 | function separatedBy(Parser $separator): Parser { 440 | $self = $this; 441 | $atLeastOne = _do(function() use ($separator) { 442 | $first = yield $this; 443 | $rest = yield $this->prefixedWith($separator)->repeated(); 444 | return array_merge([$first], $rest); 445 | },$this); 446 | return $atLeastOne->orElse(just([])); 447 | } 448 | ``` 449 | 450 | ## JSON 451 | 452 | Каждый из написанных нами парсеров и комбинаторов в отдельности просты 453 | (ну может кроме `flatMap` и `_do`, но их всего два и они очень универсальны), 454 | но используя их теперь нам не составит труда написать парсер JSON. 455 | 456 | ### `jNumber = ('-'|'+'|'') [0-9]+ (.[0-9]+)?` 457 | ```php 458 | $jNumber = _do(function() { 459 | $number = yield literal('-')->orElse(literal('+'))->orElse(just('')); 460 | $number .= yield takeOf('[0-9]'); 461 | if (yield literal('.')->orElse(just(false))) { 462 | $number .= '.'. yield takeOf('[0-9]'); 463 | } 464 | if ($number !== '') 465 | return +$number; 466 | }); 467 | ``` 468 | Код вполне самодокументирующийся, читать и искать в нем ошибки довольно просто. 469 | 470 | ### `jBool = true | false` 471 | ```php 472 | $jBool = literal('true')->orElse(literal('false'))->flatMap(function($value) { 473 | return just($value === 'true'); 474 | }); 475 | ``` 476 | ### `jString = '"' [^"]* '"'` 477 | ```php 478 | $jString = _do(function() { 479 | yield literal('"'); 480 | $value = yield takeOf('[^"]'); 481 | yield literal('"'); 482 | return $value; 483 | }); 484 | ``` 485 | ### `jList = '[' (jValue (, jValue)*)? ']'` 486 | ```php 487 | $jList = _do(function() use (&$jValue) { 488 | yield literal('['); 489 | $items = yield $jValue->separatedBy(literal(',')); 490 | yield literal(']'); 491 | return $items; 492 | }); 493 | ``` 494 | ### `jObject = '{' (pair (, pair)*)? '}'` 495 | ```php 496 | $jObject = _do(function() use (&$jValue) { 497 | yield literal('{'); 498 | 499 | $result = []; 500 | $pair = _do(function() use (&$jValue,&$result) { 501 | $key = yield takeOf('\\w'); 502 | yield literal(':'); 503 | $value = yield $jValue; 504 | $result[$key] = $value; 505 | return true; 506 | }); 507 | yield $pair->separatedBy(literal(',')); 508 | yield literal('}'); 509 | 510 | return $result; 511 | }); 512 | ``` 513 | 514 | ### `jValue = jNull | jBool | jNumber | jString | jList | jObject` 515 | ```php 516 | $jValue = $jNull->orElse($jBool)->orElse($jNumber)->orElse($jString)->orElse($jList)->orElse($jObject); 517 | ``` 518 | Вот и готов парсер JSON `jValue`! Причем выглядит не так уж непонятно, как казалось вначале. 519 | Есть некоторые замечания к производительности, но они решаются заменой способа разделения строки 520 | (например вместо `string => [x, string]` можно использовать `[string,index] => [x,string,index]` и избежать 521 | многократного разбиения строки). 522 | Причем для изменения такого рода достаточно переписать `just`, `take` и `flatMap`, весь 523 | остальной код, построенный на их основе останется без изменений! 524 | 525 | ## Заключение 526 | 527 | Моей целью было конечно не написание очередного парсера JSON, а демонстрация того, как написание 528 | маленьких простых (и функционально чистых) функций, а так же простых способов их комбинирования 529 | позволяет строить сложные функции простым способом. 530 | 531 | А в простом и понятном коде и ошибок меньше. 532 | 533 | Не бойтесь функционального подода :) 534 | 535 | -------------------------------------------------------------------------------- /json.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | orElse(literal('+'))->orElse(just('')); 11 | $number .= yield takeOf('[0-9]'); 12 | if (yield literal('.')->orElse(just(false))) { 13 | $number .= '.'. yield takeOf('[0-9]'); 14 | } 15 | if ($number !== '') 16 | return +$number; 17 | }); 18 | 19 | test($jNumber, '42', [ 42,'']); 20 | test($jNumber, '-42', [ -42,'']); 21 | test($jNumber, '3.14', [ 3.14,'']); 22 | test($jNumber,'-3.14', [-3.14,'']); 23 | 24 | $NULL = new stdClass; 25 | 26 | $jNull = literal('null')->flatMap(function() use ($NULL) { 27 | return just($NULL); 28 | }); 29 | 30 | test($jNull, 'null', [$NULL,'']); 31 | 32 | $jBool = literal('true')->orElse(literal('false'))->flatMap(function($value) { 33 | return just($value === 'true'); 34 | }); 35 | 36 | test($jBool, 'true', [true,'']); 37 | test($jBool, 'false', [false,'']); 38 | 39 | $jString = _do(function() { 40 | yield literal('"'); 41 | $value = yield takeOf('[^"]'); 42 | yield literal('"'); 43 | return $value; 44 | }); 45 | 46 | test($jString, '""', ["",'']); 47 | test($jString, '"abc"', ["abc",'']); 48 | test($jString, '"abc', []); 49 | test($jString, 'abc"', []); 50 | 51 | $jList = _do(function() use (&$jValue) { 52 | yield literal('['); 53 | $items = yield $jValue->separatedBy(literal(',')); 54 | yield literal(']'); 55 | return $items; 56 | }); 57 | 58 | $jObject = _do(function() use (&$jValue) { 59 | yield literal('{'); 60 | 61 | $result = []; 62 | $pair = _do(function() use (&$jValue,&$result) { 63 | $key = yield takeOf('\\w'); 64 | yield literal(':'); 65 | $value = yield $jValue; 66 | $result[$key] = $value; 67 | return true; 68 | }); 69 | yield $pair->separatedBy(literal(',')); 70 | yield literal('}'); 71 | 72 | return $result; 73 | }); 74 | 75 | $jValue = $jNull->orElse($jBool)->orElse($jNumber)->orElse($jString)->orElse($jList)->orElse($jObject); 76 | 77 | test($jValue, '', []); 78 | 79 | test($jList, '[]', [ [],'']); 80 | test($jList, '[1]', [ [1],'']); 81 | test($jList, '[1,"test",true,false,null]', [ [1,"test",true,false,$NULL],'']); 82 | test($jList, '[[[1]]]', [ [[[1]]],'']); 83 | 84 | test($jObject, '{key:42}', [ ['key' => 42], '']); 85 | test($jObject, '{key:42,test:{test:42}}', [ ['key' => 42, 'test' => ['test' => 42]], '']); 86 | 87 | test($jValue, 88 | '{num:-3.14,str:"test",list:[1,2,3]}', 89 | [[ 90 | 'num' => -3.14, 91 | 'str' => "test", 92 | 'list' => [1,2,3] 93 | ], 94 | ''] 95 | ); 96 | 97 | -------------------------------------------------------------------------------- /parser.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/php 2 | parse = $parse; 12 | } 13 | function __invoke(string $s): array { 14 | return ($this->parse)($s); 15 | } 16 | use ParserM; 17 | use ParserRep; 18 | use ParserOr; 19 | } 20 | 21 | # Simple parsers 22 | 23 | function parser($f, $scope = null) { 24 | return new Parser($f->bindTo($scope)); 25 | } 26 | 27 | function just($x): Parser { 28 | return parser(function($s) use ($x) { 29 | return [ $x, $s ]; 30 | }); 31 | } 32 | 33 | test(just(42), '123', [42,'123']); 34 | 35 | function none(): Parser { 36 | return parser(function($s) { 37 | return Parser::FAILED; 38 | }); 39 | } 40 | 41 | function take(int $n): Parser { 42 | return parser(function($s) use ($n) { 43 | return strlen($s) < $n ? Parser::FAILED : [ substr($s, 0, $n), substr($s, $n) ]; 44 | }); 45 | } 46 | 47 | test(take(2), 'abc', ['ab','c']); 48 | test(take(4), 'abc', Parser::FAILED); 49 | 50 | # Combinator`s 51 | 52 | trait ParserOr { 53 | function orElse(Parser $alternative): Parser { 54 | return parser(function($s) use ($alternative) { 55 | $result = $this($s); 56 | if ($result === Parser::FAILED) { 57 | $result = $alternative($s); 58 | } 59 | return $result; 60 | }, $this); 61 | } 62 | } 63 | 64 | test(take(10)->orElse(take(2)), '123', ['12','3']); 65 | 66 | 67 | trait ParserM { 68 | 69 | function flatMap(callable $f): Parser { 70 | return parser(function($s) use ($f) { 71 | $result = $this($s); 72 | if ($result != Parser::FAILED) { 73 | list ($x, $rest) = $result; 74 | $next = $f($x); 75 | $result = $next($rest); 76 | } 77 | return $result; 78 | }, $this); 79 | } 80 | 81 | function onlyIf(callable $predicate): Parser { 82 | return $this->flatMap(function($x) use ($predicate) { 83 | return $predicate($x) ? just($x) : none(); 84 | }); 85 | } 86 | 87 | function prefixedWith(Parser $prefix): Parser { 88 | $self = $this; 89 | return $prefix->flatMap(function() use ($self) { 90 | return $self; 91 | }); 92 | } 93 | 94 | function followedBy(Parser $after): Parser { 95 | return $this->flatMap(function($x) use ($after) { 96 | return $after->flatMap(function() use ($x) { 97 | return just($x); 98 | }); 99 | }); 100 | } 101 | } 102 | 103 | test( 104 | take(1)->flatMap(function($x) { 105 | return take(2)->flatMap(function($y) use ($x) { 106 | return just("$x~$y"); 107 | }); 108 | }), 109 | '1234', 110 | ['1~23','4'] 111 | ); 112 | 113 | test(take(1)->onlyIf(function($x) { return false; }), '123', []); 114 | test(take(1)->prefixedWith(take(1)), '123', ['2','3']); 115 | test(take(1)->followedBy(take(1)), '123', ['1','3']); 116 | 117 | 118 | function literal(string $value): Parser { 119 | return take(strlen($value))->onlyIf(function($actual) use ($value) { 120 | return $actual === $value; 121 | }); 122 | } 123 | 124 | test(literal('test'), 'test1', ['test','1']); 125 | test(literal('test'), 'some1', []); 126 | 127 | # DO notation 128 | 129 | function _do(Closure $gen, $scope = null) { 130 | $step = function ($body) use (&$step) { 131 | if (! $body->valid()) { 132 | $result = $body->getReturn(); 133 | return is_null($result) ? none() : just($result); 134 | } else { 135 | return $body->current()->flatMap( 136 | function($x) use (&$step, $body) { 137 | $body->send($x); 138 | return $step($body); 139 | }); 140 | } 141 | }; 142 | $gen = $gen->bindTo($scope); 143 | return parser(function($text) use ($step,$gen) { 144 | return $step($gen())($text); 145 | }); 146 | } 147 | 148 | test( 149 | _do(function() { 150 | $x = yield take(1); 151 | $y = yield take(2); 152 | return "$x~$y"; 153 | }), 154 | '1234', 155 | ['1~23','4'] 156 | ); 157 | 158 | # Combined parsers 159 | 160 | function takeWhile(callable $predicate): Parser { 161 | return _do(function() use ($predicate) { 162 | $c = yield take(1)->onlyIf($predicate)->orElse(just('')); 163 | if ($c !== '') { 164 | $rest = yield takeWhile($predicate); 165 | return $c.$rest; 166 | } else { 167 | return ''; 168 | } 169 | }); 170 | } 171 | 172 | function takeOf(string $pattern): Parser { 173 | return takeWhile(function($c) use ($pattern) { 174 | return preg_match("/^$pattern$/", $c); 175 | }); 176 | } 177 | 178 | test(takeOf('[0-9]'), '123abc', ['123','abc' ]); 179 | test(takeOf('[a-z]'), '123abc', [ '','123abc']); 180 | 181 | trait ParserRep { 182 | function repeated(): Parser { 183 | $atLeastOne = _do(function() { 184 | $first = yield $this; 185 | $rest = yield $this->repeated(); 186 | return array_merge([$first],$rest); 187 | },$this); 188 | return $atLeastOne->orElse(just([])); 189 | } 190 | function separatedBy(Parser $separator): Parser { 191 | $self = $this; 192 | $atLeastOne = _do(function() use ($separator) { 193 | $first = yield $this; 194 | $rest = yield $this->prefixedWith($separator)->repeated(); 195 | return array_merge([$first], $rest); 196 | },$this); 197 | return $atLeastOne->orElse(just([])); 198 | } 199 | } 200 | 201 | test(take(2)->repeated(), '123456', [ ['12','34','56'], '' ]); 202 | test(take(2)->separatedBy(literal(',')), '12,34,56', [ ['12','34','56'], '' ]); 203 | 204 | 205 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 |