├── .github └── workflows │ └── php.yml ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── bench ├── auto-curry.php ├── named-closures.php └── sort-from-array.php ├── composer.json ├── docker-compose.yml ├── make-code.php ├── peridot.php ├── src ├── c.generated.php ├── consts.generated.php ├── consts.ns.generated.php ├── curried.generated.php ├── f.generated.php ├── fn.php └── generate.php └── test ├── api.spec.php └── fn.spec.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | php-versions: ['7.4'] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Setup PHP 14 | uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: ${{ matrix.php-versions }} 17 | - name: Install dependencies 18 | run: composer install 19 | - name: Run Tests 20 | run: composer test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.4-cli 2 | 3 | RUN apt-get update && apt-get install -y git zip 4 | 5 | COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/bin/ 6 | COPY --from=composer:2.4.4 /usr/bin/composer /usr/bin/composer -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test code 2 | 3 | test: 4 | ./vendor/bin/peridot test 5 | code: 6 | php make-code.php 7 | docs: 8 | head -n $$(grep -n '## API' README.md | cut -f1 -d:) README.md > _tmp_readme.bak 9 | ./vendor/bin/peridot -r peridocs test >> _tmp_readme.bak 10 | mv _tmp_readme.bak README.md 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fun 2 | 3 | Yet another functional library for PHP. What makes this library special is that it uses PHP Parser to generate curried versions of the non-curried implementations for best performance. 4 | 5 | ## Installation 6 | 7 | Install with composer at `krak/fn` 8 | 9 | ## Usage 10 | 11 | All functions are defined in `Krak\Fun`, are not curried, and are data last. Curried versions of functions are defined `Kran\Fun\Curried`. Constants are also generated per function in `Krak\Fun\Consts`. 12 | 13 | ```php 14 | ')(2)) 24 | )([1,2,3,4]); 25 | assert($res == [9, 12]); 26 | ``` 27 | 28 | Check the `src/fn.php` for examples of all the functions. 29 | 30 | ### Fun API 31 | 32 | In addition to importing the functions/consts individually, you can also utilize the `f` and `c` namespaces as a shorthand which make using the library a lot easier. 33 | 34 | ```php 35 | ')(2)) 43 | )([1,2,3,4]); 44 | assert($res == [9, 12]); 45 | ``` 46 | 47 | The `f` namespace holds the standard functions basically copied over verbatim from the `Krak\Fun` namespace. 48 | 49 | The `c` namespace contains all of the curried functions and constant definitions. 50 | 51 | One great way to use the consts is with compose or pipe chains: 52 | 53 | ```php 54 | use Krak\Fun\{f, c}; 55 | 56 | $res = f\compose( 57 | c\toArray, 58 | c\map(function($tup) { 59 | return $tup[0] + $tup[1]; 60 | }), 61 | c\toPairs 62 | )([1,2,3]); 63 | // $res == [1, 3, 5] 64 | ``` 65 | 66 | ### Constants 67 | 68 | This library generates constants with same name as the function they were generated from where their value is the fully qualified name of the function. 69 | 70 | PHP (unfortunately) will treat strings as callables if they resolve to a function name. So generating constants with the same name as functions allows us to support a neat first class function type syntax. 71 | 72 | ```php 73 | Void 123 | ``` 124 | 125 | the curried verison would look like: 126 | 127 | ``` 128 | (a, c = null) -> (b) -> Void 129 | ``` 130 | 131 | 132 | ### Debugging 133 | 134 | If you have a function compose chain and want to debug/test the result of any of the functions, you can do something like the following examples: 135 | 136 | 1. Debug a single value: 137 | 138 | ```php 139 | f\compose( 140 | function() {}, // do something else 141 | c\dd(), // debug result here 142 | function() {}, // do another thing that returns a single value 143 | function() {} // do something 144 | ); 145 | ``` 146 | 2. Debug an iterable: 147 | 148 | ```php 149 | f\compose( 150 | function() {}, // do something else 151 | c\dd(), c\toArray, // debug result here 152 | c\map(function() {}) // do something 153 | ); 154 | ``` 155 | 156 | ### Using Compose Chains for Readable Code 157 | 158 | One of my favorite features of using this library is building compose chains in a way that make your application services a lot easier to read and follow along with. 159 | 160 | ```php 161 | 162 | use Krak\Fun\{f, c}; 163 | 164 | /** 165 | * Fetches orders based off of the arguments, filters data, and imports to database 166 | */ 167 | final class ImportOrdersFromApi 168 | { 169 | public function __construct(ApiClient $client, OrderRepository $orderRepository, SaveOrders $saveOrders) { 170 | // ... 171 | } 172 | 173 | public function __invoke(ImportOrderRequest $req): void { 174 | f\compose( 175 | $this->persistApiOrders(), 176 | $this->removeAlreadyImportedOrders(), 177 | $this->fetchOrdersFromApi() 178 | )($req); 179 | } 180 | 181 | private function persistApiOrders(): callable { 182 | // important that this is an c\each so that it will consume the iterable chain 183 | return f\compose( 184 | c\each($this->saveOrders), // saveOrders has a signature of `__invoke(iterable Order[]) => void` 185 | c\chunk(50), // chunk to persist many at once 186 | c\map(function(array $apiOrder) { 187 | return Order::createFromApiData($apiOrder); 188 | }) 189 | ); 190 | } 191 | 192 | private function removeAlreadyImportedOrders(): callable { 193 | return f\compose( 194 | c\flatMap(function(array $apiOrders) { 195 | $apiOrderIds = array_column($apiOrders, 'order_id'); 196 | /** array of order id => order entity */ 197 | $orders = $this->orderRepository->findByApiOrderIds($ids); 198 | return f\filter(function(array $apiOrder) use ($orders) { 199 | return !array_key_exists($apiOrder['order_id'], $orders); 200 | }, $apiOrders); 201 | }), 202 | // chunk by 50 to save on database requests 203 | c\chunk(50) 204 | ); 205 | } 206 | 207 | /** Returns an iterable of api orders */ 208 | private function fetchOrdersFromApi(): callable { 209 | return function(ImportOrderRequest $req) { 210 | yield from $this->apiClient->fetch(/* pass in req args */); 211 | }; 212 | } 213 | } 214 | ``` 215 | 216 | ## Docs 217 | 218 | Docs are generated with `make docs`. This uses Krak Peridocs to actually generate the documentation from the peridot tests. 219 | 220 | ## Code Generation 221 | 222 | The constants and curried functions are generated with `make code`. 223 | 224 | ## Tests 225 | 226 | Tests are run via `make test` and are stored in the `test` directory. We use peridot for testing. 227 | 228 | ## API 229 |
allanyarrayCompactarrayFilterarrayMaparrayReindex
arrayWrapassignchainchunkchunkBycompact
composeconstructcurrydifferenceWithdddrop
dropWhileeachfilterfilterKeysflatMapflatten
flipfromPairsgroupByhasIndexInheadinArray
indexindexInindexOfisNulliterjoin
keysmapmapAccummapKeysmapKeyValuemapOn
nullableonEachoppadpartialpartition
pickpickBypipeproductproppropIn
rangereducereduceKeyValuereindexretrysearch
setIndexsetIndexInsetPropslicesortFromArrayspread
taketakeWhiletapthrowIftoArraytoArrayWithKeys
toPairsupdateIndexInvalueswhenwithStatewithin
withoutzip
230 | 231 |

all(callable $predicate, iterable $iter): bool

232 | 233 | **Name:** `Krak\Fun\all` 234 | 235 | Returns true if the predicate returns true on all of the items: 236 | 237 | ```php 238 | $res = all(function ($v) { 239 | return $v % 2 == 0; 240 | }, [2, 4, 6]); 241 | expect($res)->equal(true); 242 | ``` 243 | 244 | Returns false if the predicate returns false on any of the items: 245 | 246 | ```php 247 | $res = all(function ($v) { 248 | return $v % 2 == 0; 249 | }, [1, 2, 4, 6]); 250 | expect($res)->equal(false); 251 | ``` 252 | 253 | 254 | 255 |

any(callable $predicate, iterable $iter): bool

256 | 257 | **Name:** `Krak\Fun\any` 258 | 259 | Returns true if the predicate returns true on any of the items: 260 | 261 | ```php 262 | $res = any(function ($v) { 263 | return $v % 2 == 0; 264 | }, [1, 3, 4, 5]); 265 | expect($res)->equal(true); 266 | ``` 267 | 268 | Returns false if the predicate returns false on all of the items: 269 | 270 | ```php 271 | $res = any(function ($v) { 272 | return $v % 2 == 0; 273 | }, [1, 3, 5]); 274 | expect($res)->equal(false); 275 | ``` 276 | 277 | 278 | 279 |

arrayCompact(iterable $iter): array

280 | 281 | **Name:** `Krak\Fun\arrayCompact` 282 | 283 | It will remove all nulls from an iterable and return an array: 284 | 285 | ```php 286 | $res = arrayCompact([1, 2, null, null, 3]); 287 | expect(\array_values($res))->equal([1, 2, 3]); 288 | ``` 289 | 290 | Keep in mind that the keys will be preserved when using arrayCompact, so make sure to use array_values if you want to ignore keys. 291 | 292 |

arrayFilter(callable $fn, iterable $data): array

293 | 294 | **Name:** `Krak\Fun\arrayFilter` 295 | 296 | Alias of array_filter: 297 | 298 | ```php 299 | $res = arrayFilter(partial(op, '<', 2), [1, 2, 3]); 300 | expect($res)->equal([1]); 301 | ``` 302 | 303 | Filters iterables as well as arrays: 304 | 305 | ```php 306 | $res = arrayFilter(partial(op, '<', 2), range(1, 3)); 307 | expect($res)->equal([1]); 308 | ``` 309 | 310 | 311 | 312 |

arrayMap(callable $fn, iterable $data): array

313 | 314 | **Name:** `Krak\Fun\arrayMap` 315 | 316 | Alias of array_map: 317 | 318 | ```php 319 | $res = arrayMap(partial(op, '*', 2), [1, 2, 3]); 320 | expect($res)->equal([2, 4, 6]); 321 | ``` 322 | 323 | Maps iterables as well as arrays: 324 | 325 | ```php 326 | $res = arrayMap(partial(op, '*', 2), range(1, 3)); 327 | expect($res)->equal([2, 4, 6]); 328 | ``` 329 | 330 | 331 | 332 |

arrayReindex(callable $fn, iterable $iter): array

333 | 334 | **Name:** `Krak\Fun\arrayReindex` 335 | 336 | Re-indexes a collection via a callable into an associative array: 337 | 338 | ```php 339 | $res = arrayReindex(function ($v) { 340 | return $v['id']; 341 | }, [['id' => 2], ['id' => 3], ['id' => 1]]); 342 | expect($res)->equal([2 => ['id' => 2], 3 => ['id' => 3], 1 => ['id' => 1]]); 343 | ``` 344 | 345 | 346 | 347 |

arrayWrap($value)

348 | 349 | **Name:** `Krak\Fun\arrayWrap` 350 | 351 | Wraps any non list array into an array: 352 | 353 | ```php 354 | $results = arrayMap(arrayWrap, [1, 'abc', ['a' => 1]]); 355 | expect($results)->equal([[1], ['abc'], [['a' => 1]]]); 356 | ``` 357 | 358 | List based arrays are left as is: 359 | 360 | ```php 361 | $results = arrayMap(arrayWrap, [[], [1, 2, 3]]); 362 | expect($results)->equal([[], [1, 2, 3]]); 363 | ``` 364 | 365 | Note: `array_is_list` which requires php 8.1 or symfony/polyfill-php81 366 | 367 |

assign($obj, iterable $iter)

368 | 369 | **Name:** `Krak\Fun\assign` 370 | 371 | Assigns iterable keys and values to an object: 372 | 373 | ```php 374 | $obj = new \StdClass(); 375 | $obj = assign($obj, ['a' => 1, 'b' => 2]); 376 | expect($obj->a)->equal(1); 377 | expect($obj->b)->equal(2); 378 | ``` 379 | 380 | 381 | 382 |

chain(iterable ...$iters)

383 | 384 | **Name:** `Krak\Fun\chain` 385 | 386 | Chains iterables together into one iterable: 387 | 388 | ```php 389 | $res = chain([1], range(2, 3)); 390 | expect(toArray($res))->equal([1, 2, 3]); 391 | ``` 392 | 393 | 394 | 395 |

chunk(int $size, iterable $iter): iterable

396 | 397 | **Name:** `Krak\Fun\chunk` 398 | 399 | Chunks an iterable into equal sized chunks.: 400 | 401 | ```php 402 | $res = chunk(2, [1, 2, 3, 4]); 403 | expect(toArray($res))->equal([[1, 2], [3, 4]]); 404 | ``` 405 | 406 | If there is any remainder, it is yielded as is: 407 | 408 | ```php 409 | $res = chunk(3, [1, 2, 3, 4]); 410 | expect(toArray($res))->equal([[1, 2, 3], [4]]); 411 | ``` 412 | 413 | 414 | 415 |

chunkBy(callable $fn, iterable $iter, ?int $maxSize = null): iterable

416 | 417 | **Name:** `Krak\Fun\chunkBy` 418 | 419 | Chunks items together off of the result from the callable: 420 | 421 | ```php 422 | $items = ['aa', 'ab', 'ac', 'ba', 'bb', 'bc', 'ca', 'cb', 'cc']; 423 | $chunks = chunkBy(function (string $item) { 424 | return $item[0]; 425 | // return first char 426 | }, $items); 427 | expect(toArray($chunks))->equal([['aa', 'ab', 'ac'], ['ba', 'bb', 'bc'], ['ca', 'cb', 'cc']]); 428 | ``` 429 | 430 | Allows a maxSize to prevent chunks from exceeding a limit: 431 | 432 | ```php 433 | $items = ['aa', 'ab', 'ac', 'ba', 'bb', 'bc', 'ca', 'cb', 'cc']; 434 | $chunks = chunkBy(function (string $item) { 435 | return $item[0]; 436 | // return first char 437 | }, $items, 2); 438 | expect(toArray($chunks))->equal([['aa', 'ab'], ['ac'], ['ba', 'bb'], ['bc'], ['ca', 'cb'], ['cc']]); 439 | ``` 440 | 441 | 442 | 443 |

compact(iterable $iter): iterable

444 | 445 | **Name:** `Krak\Fun\compact` 446 | 447 | Removes all null values from an iterable: 448 | 449 | ```php 450 | $res = compact([1, null, 2, 3, null, null, 4]); 451 | expect(toArray($res))->equal([1, 2, 3, 4]); 452 | ``` 453 | 454 | 455 | 456 |

compose(callable ...$fns)

457 | 458 | **Name:** `Krak\Fun\compose` 459 | 460 | Composes functions together. compose(f, g)(x) == f(g(x)): 461 | 462 | ```php 463 | $mul2 = Curried\op('*')(2); 464 | $add3 = Curried\op('+')(3); 465 | $add3ThenMul2 = compose($mul2, $add3); 466 | $res = $add3ThenMul2(5); 467 | expect($res)->equal(16); 468 | ``` 469 | 470 | Allows an empty initial argument: 471 | 472 | ```php 473 | $res = compose(Curried\reduce(function ($acc, $v) { 474 | return $acc + $v; 475 | }, 0), function () { 476 | yield from [1, 2, 3]; 477 | })(); 478 | expect($res)->equal(6); 479 | ``` 480 | 481 | 482 | 483 |

construct($className, ...$args)

484 | 485 | **Name:** `Krak\Fun\construct` 486 | 487 | Constructs (instantiates) a new class with the given arguments: 488 | 489 | ```php 490 | $res = construct(\ArrayObject::class, [1, 2, 3]); 491 | expect($res->count())->equal(3); 492 | ``` 493 | 494 | 495 | 496 |

curry(callable $fn, int $num = 1)

497 | 498 | **Name:** `Krak\Fun\curry` 499 | 500 | currys the given function $n times: 501 | 502 | ```php 503 | $res = curry(_idArgs::class, 2)(1)(2)(3); 504 | expect($res)->equal([1, 2, 3]); 505 | ``` 506 | 507 | Given a function definition: (a, b) -> c. A curried version will look like (a) -> (b) -> c 508 | 509 |

differenceWith(callable $cmp, iterable $a, iterable $b)

510 | 511 | **Name:** `Krak\Fun\differenceWith` 512 | 513 | Takes the difference between two iterables with a given comparator: 514 | 515 | ```php 516 | $res = differenceWith(partial(op, '==='), [1, 2, 3, 4, 5], [2, 3, 4]); 517 | expect(toArray($res))->equal([1, 5]); 518 | ``` 519 | 520 | 521 | 522 |

dd($value, callable $dump = null, callable $die = null)

523 | 524 | **Name:** `Krak\Fun\dd` 525 | 526 | dumps and dies: 527 | 528 | ```php 529 | $res = null; 530 | $died = false; 531 | $dump = function ($v) use(&$res) { 532 | $res = $v; 533 | }; 534 | $die = function () use(&$died) { 535 | $died = true; 536 | }; 537 | dd(1, $dump, $die); 538 | expect($res)->equal(1); 539 | expect($died)->equal(true); 540 | ``` 541 | 542 | 543 | 544 |

drop(int $num, iterable $iter): iterable

545 | 546 | **Name:** `Krak\Fun\drop` 547 | 548 | Drops the first num items from an iterable: 549 | 550 | ```php 551 | $res = drop(2, range(0, 3)); 552 | expect(toArray($res))->equal([2, 3]); 553 | ``` 554 | 555 | 556 | 557 |

dropWhile(callable $predicate, iterable $iter): iterable

558 | 559 | **Name:** `Krak\Fun\dropWhile` 560 | 561 | Drops elements from the iterable while the predicate returns true: 562 | 563 | ```php 564 | $res = dropWhile(Curried\op('>')(0), [2, 1, 0, 1, 2]); 565 | expect(toArray($res))->equal([0, 1, 2]); 566 | ``` 567 | 568 | 569 | 570 |

each(callable $handle, iterable $iter)

571 | 572 | **Name:** `Krak\Fun\each` 573 | 574 | Invokes a callable on each item in an iterable: 575 | 576 | ```php 577 | $state = [(object) ['id' => 1], (object) ['id' => 2]]; 578 | each(function ($item) { 579 | $item->id += 1; 580 | }, $state); 581 | expect([$state[0]->id, $state[1]->id])->equal([2, 3]); 582 | ``` 583 | 584 | Normally using php foreach should suffice for iterating over an iterable; however, php variables in foreach loops are not scoped whereas closures are. 585 | 586 |

filter(callable $predicate, iterable $iter): iterable

587 | 588 | **Name:** `Krak\Fun\filter` 589 | 590 | Lazily filters an iterable off of a predicate that should return true or false. If true, keep the data, else remove the data from the iterable: 591 | 592 | ```php 593 | $values = filter(partial(op, '>', 2), [1, 2, 3, 4]); 594 | // keep all items that are greater than 2 595 | expect(toArray($values))->equal([3, 4]); 596 | ``` 597 | 598 | 599 | 600 |

filterKeys(callable $predicate, iterable $iter): iterable

601 | 602 | **Name:** `Krak\Fun\filterKeys` 603 | 604 | Filters an iterable off of the keys: 605 | 606 | ```php 607 | $res = filterKeys(Curried\inArray(['a', 'b']), ['a' => 1, 'b' => 2, 'c' => 3]); 608 | expect(toArrayWithKeys($res))->equal(['a' => 1, 'b' => 2]); 609 | ``` 610 | 611 | 612 | 613 |

flatMap(callable $map, iterable $iter): iterable

614 | 615 | **Name:** `Krak\Fun\flatMap` 616 | 617 | Maps and then flattens an iterable: 618 | 619 | ```php 620 | $res = flatMap(function ($v) { 621 | return [-$v, $v]; 622 | }, range(1, 3)); 623 | expect(toArray($res))->equal([-1, 1, -2, 2, -3, 3]); 624 | ``` 625 | 626 | flatMap is perfect for when you want to map an iterable and also add elements to the resulting iterable. 627 | 628 |

flatten(iterable $iter, $levels = INF): iterable

629 | 630 | **Name:** `Krak\Fun\flatten` 631 | 632 | Flattens nested iterables into a flattened set of elements: 633 | 634 | ```php 635 | $res = flatten([1, [2, [3, [4]]]]); 636 | expect(toArray($res))->equal([1, 2, 3, 4]); 637 | ``` 638 | 639 | Can flatten a specific number of levels: 640 | 641 | ```php 642 | $res = flatten([1, [2, [3]]], 1); 643 | expect(toArray($res))->equal([1, 2, [3]]); 644 | ``` 645 | 646 | Flattening zero levels does nothing: 647 | 648 | ```php 649 | $res = flatten([1, [2]], 0); 650 | expect(toArray($res))->equal([1, [2]]); 651 | ``` 652 | 653 | 654 | 655 |

flip(iterable $iter): iterable

656 | 657 | **Name:** `Krak\Fun\flip` 658 | 659 | Flips the keys => values of an iterable to values => keys: 660 | 661 | ```php 662 | $res = flip(['a' => 0, 'b' => 1]); 663 | expect(toArray($res))->equal(['a', 'b']); 664 | ``` 665 | 666 | 667 | 668 |

fromPairs(iterable $iter): iterable

669 | 670 | **Name:** `Krak\Fun\fromPairs` 671 | 672 | Converts an iterable of tuples [$key, $value] into an associative iterable: 673 | 674 | ```php 675 | $res = fromPairs([['a', 1], ['b', 2]]); 676 | expect(toArrayWithKeys($res))->equal(['a' => 1, 'b' => 2]); 677 | ``` 678 | 679 | 680 | 681 |

groupBy(callable $fn, iterable $iter, ?int $maxSize = null): iterable

682 | 683 | **Name:** `Krak\Fun\groupBy` 684 | 685 | Alias of chunkBy 686 | 687 | Groups items together off of the result from the callable: 688 | 689 | ```php 690 | $items = ['aa', 'ab', 'ac', 'ba', 'bb', 'bc', 'ca', 'cb', 'cc']; 691 | $groupedItems = groupBy(function (string $item) { 692 | return $item[0]; 693 | // return first char 694 | }, $items); 695 | expect(toArray($groupedItems))->equal([['aa', 'ab', 'ac'], ['ba', 'bb', 'bc'], ['ca', 'cb', 'cc']]); 696 | ``` 697 | 698 | Allows a maxSize to prevent groups from exceeding a limit: 699 | 700 | ```php 701 | $items = ['aa', 'ab', 'ac', 'ba', 'bb', 'bc', 'ca', 'cb', 'cc']; 702 | $groupedItems = groupBy(function (string $item) { 703 | return $item[0]; 704 | // return first char 705 | }, $items, 2); 706 | expect(toArray($groupedItems))->equal([['aa', 'ab'], ['ac'], ['ba', 'bb'], ['bc'], ['ca', 'cb'], ['cc']]); 707 | ``` 708 | 709 | 710 | 711 |

hasIndexIn(array $keys, array $data): bool

712 | 713 | **Name:** `Krak\Fun\hasIndexIn` 714 | 715 | Checks if a nested index exists in the given data: 716 | 717 | ```php 718 | $res = hasIndexIn(['a', 'b', 'c'], ['a' => ['b' => ['c' => null]]]); 719 | expect($res)->equal(true); 720 | ``` 721 | 722 | Returns false if any of the indexes do not exist in the data: 723 | 724 | ```php 725 | $res = hasIndexIn(['a', 'b', 'c'], ['a' => ['b' => []]]); 726 | expect($res)->equal(false); 727 | ``` 728 | 729 | 730 | 731 |

head(iterable $iter)

732 | 733 | **Name:** `Krak\Fun\head` 734 | 735 | Returns the fist element in an iterable: 736 | 737 | ```php 738 | $res = head([1, 2, 3]); 739 | expect($res)->equal(1); 740 | ``` 741 | 742 | But returns null if the iterable is empty: 743 | 744 | ```php 745 | $res = head([]); 746 | expect($res)->equal(null); 747 | ``` 748 | 749 | 750 | 751 |

inArray(array $set, $item): bool

752 | 753 | **Name:** `Krak\Fun\inArray` 754 | 755 | Checks if an item is within an array of items: 756 | 757 | ```php 758 | $res = inArray([1, 2, 3], 2); 759 | expect($res)->equal(true); 760 | ``` 761 | 762 | 763 | 764 |

index($key, $data, $else = null)

765 | 766 | **Name:** `Krak\Fun\index` 767 | 768 | Accesses an index in an array: 769 | 770 | ```php 771 | $res = index('a', ['a' => 1]); 772 | expect($res)->equal(1); 773 | ``` 774 | 775 | If no value exists at the given index, $else will be returned: 776 | 777 | ```php 778 | $res = index('a', ['b' => 1], 2); 779 | expect($res)->equal(2); 780 | ``` 781 | 782 | Also works with objects that implement ArrayAccess: 783 | 784 | ```php 785 | class MyClass implements \ArrayAccess 786 | { 787 | private $container = []; 788 | public function __construct() 789 | { 790 | $this->container = ['one' => 1, 'two' => 2]; 791 | } 792 | public function offsetExists($offset) 793 | { 794 | return isset($this->container[$offset]); 795 | } 796 | public function offsetGet($offset) 797 | { 798 | return isset($this->container[$offset]) ? $this->container[$offset] : null; 799 | } 800 | public function offsetSet($offset, $value) 801 | { 802 | /* ... */ 803 | } 804 | public function offsetUnset($offset) 805 | { 806 | /* ... */ 807 | } 808 | } 809 | $object = new MyClass(); 810 | expect(index('two', $object))->equal(2); 811 | expect(index('three', $object, 'else'))->equal('else'); 812 | ``` 813 | 814 | 815 | 816 |

indexIn(array $keys, array $data, $else = null)

817 | 818 | **Name:** `Krak\Fun\indexIn` 819 | 820 | Accesses a nested index in a deep array structure: 821 | 822 | ```php 823 | $res = indexIn(['a', 'b'], ['a' => ['b' => 1]]); 824 | expect($res)->equal(1); 825 | ``` 826 | 827 | If any of the indexes do not exist, $else will be returned: 828 | 829 | ```php 830 | $res = indexIn(['a', 'b'], ['a' => ['c' => 1]], 2); 831 | expect($res)->equal(2); 832 | ``` 833 | 834 | 835 | 836 |

indexOf(callable $predicate, iterable $iter)

837 | 838 | **Name:** `Krak\Fun\indexOf` 839 | 840 | Searches for an element and returns the key if found: 841 | 842 | ```php 843 | $res = indexOf(partial(op, '==', 'b'), ['a', 'b', 'c']); 844 | expect($res)->equal(1); 845 | ``` 846 | 847 | 848 | 849 |

isNull($val)

850 | 851 | **Name:** `Krak\Fun\isNull` 852 | 853 | alias for is_null: 854 | 855 | ```php 856 | expect(isNull(null))->equal(true); 857 | expect(isNull(0))->equal(false); 858 | ``` 859 | 860 | 861 | 862 |

iter($iter): \Iterator

863 | 864 | **Name:** `Krak\Fun\iter` 865 | 866 | Converts any iterable into a proper instance of Iterator. 867 | 868 | Can convert arrays: 869 | 870 | ```php 871 | expect(iter([1, 2, 3]))->instanceof('Iterator'); 872 | ``` 873 | 874 | Can convert an Iterator: 875 | 876 | ```php 877 | expect(iter(new \ArrayIterator([1, 2, 3])))->instanceof('Iterator'); 878 | ``` 879 | 880 | Can convert objects: 881 | 882 | ```php 883 | $obj = (object) ['a' => 1, 'b' => 2]; 884 | expect(iter($obj))->instanceof('Iterator'); 885 | expect(toArrayWithKeys(iter($obj)))->equal(['a' => 1, 'b' => 2]); 886 | ``` 887 | 888 | Can convert any iterable: 889 | 890 | ```php 891 | $a = new class implements \IteratorAggregate 892 | { 893 | public function getIterator() 894 | { 895 | return new \ArrayIterator([1, 2, 3]); 896 | } 897 | }; 898 | expect(iter($a))->instanceof('Iterator'); 899 | expect(toArray(iter($a)))->equal([1, 2, 3]); 900 | ``` 901 | 902 | Can convert strings: 903 | 904 | ```php 905 | expect(iter('abc'))->instanceof('Iterator'); 906 | expect(toArray(iter('abc')))->equal(['a', 'b', 'c']); 907 | ``` 908 | 909 | Will throw an exception otherwise: 910 | 911 | ```php 912 | expect(function () { 913 | iter(1); 914 | })->throw('LogicException', 'Iter could not be converted into an iterable.'); 915 | ``` 916 | 917 | 918 | 919 |

join(string $sep, iterable $iter)

920 | 921 | **Name:** `Krak\Fun\join` 922 | 923 | Joins an iterable with a given separator: 924 | 925 | ```php 926 | $res = join(",", range(1, 3)); 927 | expect($res)->equal("1,2,3"); 928 | ``` 929 | 930 | 931 | 932 |

keys(iterable $iter): iterable

933 | 934 | **Name:** `Krak\Fun\keys` 935 | 936 | Yields only the keys of an in iterable: 937 | 938 | ```php 939 | $keys = keys(['a' => 1, 'b' => 2]); 940 | expect(toArray($keys))->equal(['a', 'b']); 941 | ``` 942 | 943 | 944 | 945 |

map(callable $predicate, iterable $iter): iterable

946 | 947 | **Name:** `Krak\Fun\map` 948 | 949 | Lazily maps an iterable's values to a different set: 950 | 951 | ```php 952 | $values = map(partial(op, '*', 2), [1, 2, 3, 4]); 953 | expect(toArray($values))->equal([2, 4, 6, 8]); 954 | ``` 955 | 956 | 957 | 958 |

mapAccum(callable $fn, iterable $iter, $acc = null)

959 | 960 | **Name:** `Krak\Fun\mapAccum` 961 | 962 | Maps a function to each element of a list while passing in an accumulator to accumulate over every iteration: 963 | 964 | ```php 965 | $data = iter('abcd'); 966 | [$totalSort, $values] = mapAccum(function ($acc, $value) { 967 | return [$acc + 1, ['name' => $value, 'sort' => $acc]]; 968 | }, iter('abcd'), 0); 969 | expect($totalSort)->equal(4); 970 | expect($values)->equal([['name' => 'a', 'sort' => 0], ['name' => 'b', 'sort' => 1], ['name' => 'c', 'sort' => 2], ['name' => 'd', 'sort' => 3]]); 971 | ``` 972 | 973 | Note: mapAccum converts the interable into an array and is not lazy like most of the other functions in this library 974 | 975 |

mapKeys(callable $predicate, iterable $iter): iterable

976 | 977 | **Name:** `Krak\Fun\mapKeys` 978 | 979 | Lazily maps an iterable's keys to a different set: 980 | 981 | ```php 982 | $keys = mapKeys(partial(op, '.', '_'), ['a' => 1, 'b' => 2]); 983 | expect(toArrayWithKeys($keys))->equal(['a_' => 1, 'b_' => 2]); 984 | ``` 985 | 986 | 987 | 988 |

mapKeyValue(callable $fn, iterable $iter): iterable

989 | 990 | **Name:** `Krak\Fun\mapKeyValue` 991 | 992 | Lazily maps an iterable's key/value tuples to a different set: 993 | 994 | ```php 995 | $keys = mapKeyValue(function ($kv) { 996 | [$key, $value] = $kv; 997 | return ["{$key}_", $value * $value]; 998 | }, ['a' => 1, 'b' => 2]); 999 | expect(toArrayWithKeys($keys))->equal(['a_' => 1, 'b_' => 4]); 1000 | ``` 1001 | 1002 | 1003 | 1004 |

mapOn(array $maps, iterable $iter): iterable

1005 | 1006 | **Name:** `Krak\Fun\mapOn` 1007 | 1008 | Maps values on specific keys: 1009 | 1010 | ```php 1011 | $values = mapOn(['a' => partial(op, '*', 3), 'b' => partial(op, '+', 1)], ['a' => 1, 'b' => 2, 'c' => 3]); 1012 | expect(toArray($values))->equal([3, 3, 3]); 1013 | ``` 1014 | 1015 | 1016 | 1017 |

nullable(callable $fn, $value)

1018 | 1019 | **Name:** `Krak\Fun\nullable` 1020 | 1021 | Performs the callable if the value is not null: 1022 | 1023 | ```php 1024 | expect(nullable('intval', '0'))->equal(0); 1025 | ``` 1026 | 1027 | Returns null if the value is null: 1028 | 1029 | ```php 1030 | expect(nullable('intval', null))->equal(null); 1031 | ``` 1032 | 1033 | 1034 | 1035 |

onEach(callable $handle, iterable $iter)

1036 | 1037 | **Name:** `Krak\Fun\onEach` 1038 | 1039 | Duplicate of each. 1040 | 1041 | Invokes a callable on each item in an iterable: 1042 | 1043 | ```php 1044 | $state = [(object) ['id' => 1], (object) ['id' => 2]]; 1045 | onEach(function ($item) { 1046 | $item->id += 1; 1047 | }, $state); 1048 | expect([$state[0]->id, $state[1]->id])->equal([2, 3]); 1049 | ``` 1050 | 1051 | Normally using php foreach should suffice for iterating over an iterable; however, php variables in foreach loops are not scoped whereas closures are. 1052 | 1053 |

op(string $op, $b, $a)

1054 | 1055 | **Name:** `Krak\Fun\op` 1056 | 1057 | op evaluates binary operations. It expects the right hand operator first which makes most sense when currying or partially applying the op function. 1058 | When reading the op func, it should be read: `evaluate $op with $b with $a` e.g.: 1059 | 1060 | ``` 1061 | op('+', 2, 3) -> add 2 with 3 1062 | op('-', 2, 3) -> subtract 2 from 3 1063 | op('>', 2, 3) => compare greater than 2 with 3 1064 | ``` 1065 | 1066 | Evaluates two values with a given operator: 1067 | 1068 | ```php 1069 | $res = op('<', 2, 1); 1070 | expect($res)->equal(true); 1071 | ``` 1072 | 1073 | Supports equality operators: 1074 | 1075 | ```php 1076 | $obj = new stdClass(); 1077 | $ops = [['==', [1, 1]], ['eq', [2, 2]], ['!=', [1, 2]], ['neq', [2, 3]], ['===', [$obj, $obj]], ['!==', [new stdClass(), new stdClass()]], ['>', [1, 2]], ['gt', [1, 3]], ['>=', [1, 2]], ['gte', [1, 1]], ['<', [2, 1]], ['lt', [3, 1]], ['<=', [2, 1]], ['lte', [1, 1]]]; 1078 | foreach ($ops as list($op, list($b, $a))) { 1079 | $res = op($op, $b, $a); 1080 | expect($res)->equal(true); 1081 | } 1082 | ``` 1083 | 1084 | Supports other operators: 1085 | 1086 | ```php 1087 | $ops = [['+', [2, 3], 5], ['-', [2, 3], 1], ['*', [2, 3], 6], ['**', [2, 3], 9], ['/', [2, 3], 1.5], ['%', [2, 3], 1], ['.', ['b', 'a'], 'ab']]; 1088 | foreach ($ops as list($op, list($b, $a), $expected)) { 1089 | $res = op($op, $b, $a); 1090 | expect($res)->equal($expected); 1091 | } 1092 | ``` 1093 | 1094 | Is more useful partially applied or curried: 1095 | 1096 | ```php 1097 | $add2 = Curried\op('+')(2); 1098 | $mul3 = partial(op, '*', 3); 1099 | $sub4 = Curried\op('-')(4); 1100 | // ((2 + 2) * 3) - 4 1101 | $res = compose($sub4, $mul3, $add2)(2); 1102 | expect($res)->equal(8); 1103 | ``` 1104 | 1105 | 1106 | 1107 |

pad(int $size, iterable $iter, $padValue = null): iterable

1108 | 1109 | **Name:** `Krak\Fun\pad` 1110 | 1111 | Pads an iterable to a specific size: 1112 | 1113 | ```php 1114 | $res = pad(5, [1, 2, 3]); 1115 | expect(toArray($res))->equal([1, 2, 3, null, null]); 1116 | ``` 1117 | 1118 | Allows custom pad values: 1119 | 1120 | ```php 1121 | $res = pad(5, [1, 2, 3], 0); 1122 | expect(toArray($res))->equal([1, 2, 3, 0, 0]); 1123 | ``` 1124 | 1125 | Pads nothing if iterable is the same size as pad size: 1126 | 1127 | ```php 1128 | $res = pad(5, [1, 2, 3, 4, 5]); 1129 | expect(toArray($res))->equal([1, 2, 3, 4, 5]); 1130 | ``` 1131 | 1132 | Pads nothing if iterable is greater than pad size: 1133 | 1134 | ```php 1135 | $res = pad(5, [1, 2, 3, 4, 5, 6]); 1136 | expect(toArray($res))->equal([1, 2, 3, 4, 5, 6]); 1137 | ``` 1138 | 1139 | Ignores keys of original iterable: 1140 | 1141 | ```php 1142 | $res = pad(3, ['a' => 1, 'b' => 2]); 1143 | expect(toArrayWithKeys($res))->equal([1, 2, null]); 1144 | ``` 1145 | 1146 | 1147 | 1148 |

partial(callable $fn, ...$appliedArgs)

1149 | 1150 | **Name:** `Krak\Fun\partial` 1151 | 1152 | Partially applies arguments to a function. Given a function signature like f = (a, b, c) -> d, partial(f, a, b) -> (c) -> d: 1153 | 1154 | ```php 1155 | $fn = function ($a, $b, $c) { 1156 | return ($a + $b) * $c; 1157 | }; 1158 | $fn = partial($fn, 1, 2); 1159 | // apply the two arguments (a, b) and return a new function with signature (c) -> d 1160 | expect($fn(3))->equal(9); 1161 | ``` 1162 | 1163 | You can also use place holders when partially applying: 1164 | 1165 | ```php 1166 | $fn = function ($a, $b, $c) { 1167 | return ($a + $b) * $c; 1168 | }; 1169 | // _() represents a placeholder for parameter b. 1170 | $fn = partial($fn, 1, _(), 3); 1171 | // create the new func with signature (b) -> d 1172 | expect($fn(2))->equal(9); 1173 | ``` 1174 | 1175 | Full partial application also works: 1176 | 1177 | ```php 1178 | $fn = function ($a, $b) { 1179 | return [$a, $b]; 1180 | }; 1181 | $fn = partial($fn, 1, 2); 1182 | expect($fn())->equal([1, 2]); 1183 | ``` 1184 | 1185 | 1186 | 1187 |

partition(callable $partition, iterable $iter, int $numParts = 2): array

1188 | 1189 | **Name:** `Krak\Fun\partition` 1190 | 1191 | Splits an iterable into different arrays based off of a predicate. The predicate should return the index to partition the data into: 1192 | 1193 | ```php 1194 | list($left, $right) = partition(function ($v) { 1195 | return $v < 3 ? 0 : 1; 1196 | }, [1, 2, 3, 4]); 1197 | expect([$left, $right])->equal([[1, 2], [3, 4]]); 1198 | ``` 1199 | 1200 | 1201 | 1202 |

pick(iterable $fields, array $data): array

1203 | 1204 | **Name:** `Krak\Fun\pick` 1205 | 1206 | Picks only the given fields from a structured array: 1207 | 1208 | ```php 1209 | $res = pick(['a', 'b'], ['a' => 1, 'b' => 2, 'c' => 3]); 1210 | expect($res)->equal(['a' => 1, 'b' => 2]); 1211 | ``` 1212 | 1213 | Can be used in curried form: 1214 | 1215 | ```php 1216 | $res = arrayMap(Curried\pick(['id', 'name']), [['id' => 1, 'name' => 'Foo', 'slug' => 'foo'], ['id' => 2, 'name' => 'Bar', 'slug' => 'bar']]); 1217 | expect($res)->equal([['id' => 1, 'name' => 'Foo'], ['id' => 2, 'name' => 'Bar']]); 1218 | ``` 1219 | 1220 | 1221 | 1222 |

pickBy(callable $pick, array $data): array

1223 | 1224 | **Name:** `Krak\Fun\pickBy` 1225 | 1226 | Picks only the fields that match the pick function from a structured array: 1227 | 1228 | ```php 1229 | $res = pickBy(Curried\spread(function (string $key, int $value) : bool { 1230 | return $value % 2 === 0; 1231 | }), ['a' => 1, 'b' => 2, 'c' => 3]); 1232 | expect($res)->equal(['b' => 2]); 1233 | ``` 1234 | 1235 | 1236 | 1237 |

pipe(callable ...$fns)

1238 | 1239 | **Name:** `Krak\Fun\pipe` 1240 | 1241 | Creates a function that pipes values from one func to the next.: 1242 | 1243 | ```php 1244 | $add3 = Curried\op('+')(3); 1245 | $mul2 = Curried\op('*')(2); 1246 | $add3ThenMul2 = pipe($add3, $mul2); 1247 | $res = $add3ThenMul2(5); 1248 | expect($res)->equal(16); 1249 | ``` 1250 | 1251 | Allows an empty initial argument: 1252 | 1253 | ```php 1254 | $res = pipe(function () { 1255 | yield from [1, 2, 3]; 1256 | }, Curried\reduce(function ($acc, $v) { 1257 | return $acc + $v; 1258 | }, 0))(); 1259 | expect($res)->equal(6); 1260 | ``` 1261 | 1262 | `pipe` and `compose` are sister functions and do the same thing except the functions are composed in reverse order. pipe(f, g)(x) = g(f(x)) 1263 | 1264 |

product(iterable ...$iters): iterable

1265 | 1266 | **Name:** `Krak\Fun\product` 1267 | 1268 | Creates a cartesian product of multiple sets: 1269 | 1270 | ```php 1271 | $res = product([1, 2], [3, 4], [5, 6]); 1272 | expect(toArray($res))->equal([[1, 3, 5], [1, 3, 6], [1, 4, 5], [1, 4, 6], [2, 3, 5], [2, 3, 6], [2, 4, 5], [2, 4, 6]]); 1273 | ``` 1274 | 1275 | 1276 | 1277 |

prop(string $key, $data, $else = null)

1278 | 1279 | **Name:** `Krak\Fun\prop` 1280 | 1281 | Accesses a property from an object: 1282 | 1283 | ```php 1284 | $obj = new \StdClass(); 1285 | $obj->id = 1; 1286 | $res = prop('id', $obj); 1287 | expect($res)->equal(1); 1288 | ``` 1289 | 1290 | If no property exists, it will return the $else value: 1291 | 1292 | ```php 1293 | $obj = new \StdClass(); 1294 | $res = prop('id', $obj, 2); 1295 | expect($res)->equal(2); 1296 | ``` 1297 | 1298 | 1299 | 1300 |

propIn(array $props, $obj, $else = null)

1301 | 1302 | **Name:** `Krak\Fun\propIn` 1303 | 1304 | Accesses a property deep in an object tree: 1305 | 1306 | ```php 1307 | $obj = new \StdClass(); 1308 | $obj->id = 1; 1309 | $obj->child = new \StdClass(); 1310 | $obj->child->id = 2; 1311 | $res = propIn(['child', 'id'], $obj); 1312 | expect($res)->equal(2); 1313 | ``` 1314 | 1315 | If any property is missing in the tree, it will return the $else value: 1316 | 1317 | ```php 1318 | $obj = new \StdClass(); 1319 | $obj->id = 1; 1320 | $obj->child = new \StdClass(); 1321 | $res = propIn(['child', 'id'], $obj, 3); 1322 | expect($res)->equal(3); 1323 | ``` 1324 | 1325 | 1326 | 1327 |

range($start, $end, $step = null)

1328 | 1329 | **Name:** `Krak\Fun\range` 1330 | 1331 | Creates an iterable of a range of values starting from $start going to $end inclusively incrementing by $step: 1332 | 1333 | ```php 1334 | $res = range(1, 3); 1335 | expect(toArray($res))->equal([1, 2, 3]); 1336 | ``` 1337 | 1338 | It also allows a decreasing range: 1339 | 1340 | ```php 1341 | $res = range(3, 1); 1342 | expect(toArray($res))->equal([3, 2, 1]); 1343 | ``` 1344 | 1345 | An exception will be thrown if the $step provided goes in the wrong direction: 1346 | 1347 | ```php 1348 | expect(function () { 1349 | toArray(range(1, 2, -1)); 1350 | })->throw(\InvalidArgumentException::class); 1351 | expect(function () { 1352 | toArray(range(2, 1, 1)); 1353 | })->throw(\InvalidArgumentException::class); 1354 | ``` 1355 | 1356 | 1357 | 1358 |

reduce(callable $reduce, iterable $iter, $acc = null)

1359 | 1360 | **Name:** `Krak\Fun\reduce` 1361 | 1362 | Reduces an iterable into a single value: 1363 | 1364 | ```php 1365 | $res = reduce(function ($acc, $v) { 1366 | return $acc + $v; 1367 | }, range(1, 3), 0); 1368 | expect($res)->equal(6); 1369 | ``` 1370 | 1371 | 1372 | 1373 |

reduceKeyValue(callable $reduce, iterable $iter, $acc = null)

1374 | 1375 | **Name:** `Krak\Fun\reduceKeyValue` 1376 | 1377 | Reduces an iterables key value pairs into a value: 1378 | 1379 | ```php 1380 | $res = reduceKeyValue(function ($acc, $kv) { 1381 | [$key, $value] = $kv; 1382 | return $acc . $key . $value; 1383 | }, fromPairs([['a', 1], ['b', 2]]), ""); 1384 | expect($res)->equal("a1b2"); 1385 | ``` 1386 | 1387 | 1388 | 1389 |

reindex(callable $fn, iterable $iter): iterable

1390 | 1391 | **Name:** `Krak\Fun\reindex` 1392 | 1393 | Re-indexes a collection via a callable: 1394 | 1395 | ```php 1396 | $res = reindex(function ($v) { 1397 | return $v['id']; 1398 | }, [['id' => 2], ['id' => 3], ['id' => 1]]); 1399 | expect(toArrayWithKeys($res))->equal([2 => ['id' => 2], 3 => ['id' => 3], 1 => ['id' => 1]]); 1400 | ``` 1401 | 1402 | 1403 | 1404 |

retry(callable $fn, $shouldRetry = null)

1405 | 1406 | **Name:** `Krak\Fun\retry` 1407 | 1408 | Executes a function and retries if an exception is thrown: 1409 | 1410 | ```php 1411 | $i = 0; 1412 | $res = retry(function () use(&$i) { 1413 | $i += 1; 1414 | if ($i <= 1) { 1415 | throw new \Exception('bad'); 1416 | } 1417 | return $i; 1418 | }); 1419 | expect($res)->equal(2); 1420 | ``` 1421 | 1422 | Only retries $maxTries times else it gives up and bubbles the exception: 1423 | 1424 | ```php 1425 | expect(function () { 1426 | $i = 0; 1427 | retry(function () use(&$i) { 1428 | $i += 1; 1429 | throw new \Exception((string) $i); 1430 | }, 5); 1431 | })->throw('Exception', '6'); 1432 | ``` 1433 | 1434 | Retries until $shouldRetry returns false: 1435 | 1436 | ```php 1437 | $i = 0; 1438 | expect(function () { 1439 | $res = retry(function () use(&$i) { 1440 | $i += 1; 1441 | throw new \Exception((string) $i); 1442 | }, function ($numRetries, \Throwable $t = null) { 1443 | return $numRetries < 2; 1444 | }); 1445 | })->throw('Exception', '2'); 1446 | ``` 1447 | 1448 | Sends numRetries into the main fn: 1449 | 1450 | ```php 1451 | $res = retry(function ($numRetries) { 1452 | if (!$numRetries) { 1453 | throw new Exception('bad'); 1454 | } 1455 | return $numRetries; 1456 | }, 2); 1457 | expect($res)->equal(1); 1458 | ``` 1459 | 1460 | Keep in mind that maxTries determines the number of *re*-tries. This means the function will execute maxTries + 1 times since the first invocation is not a retry. 1461 | 1462 | 1463 | 1464 | **Name:** `Krak\Fun\search` 1465 | 1466 | Searches for an element in a collection where the callable returns true: 1467 | 1468 | ```php 1469 | $res = search(function ($v) { 1470 | return $v['id'] == 2; 1471 | }, [['id' => 1], ['id' => 2], ['id' => 3]]); 1472 | expect($res)->equal(['id' => 2]); 1473 | ``` 1474 | 1475 | Returns null if no element was found: 1476 | 1477 | ```php 1478 | $res = search(function ($v) { 1479 | return false; 1480 | }, [['id' => 1], ['id' => 2], ['id' => 3]]); 1481 | expect($res)->equal(null); 1482 | ``` 1483 | 1484 | 1485 | 1486 |

setIndex($key, $value, array $data)

1487 | 1488 | **Name:** `Krak\Fun\setIndex` 1489 | 1490 | Sets an index in an array: 1491 | 1492 | ```php 1493 | $res = setIndex('a', 1, []); 1494 | expect($res['a'])->equal(1); 1495 | ``` 1496 | 1497 | 1498 | 1499 |

setIndexIn(array $keys, $value, array $data)

1500 | 1501 | **Name:** `Krak\Fun\setIndexIn` 1502 | 1503 | Sets a nested index in an array: 1504 | 1505 | ```php 1506 | $res = setIndexIn(['a', 'b'], 1, ['a' => []]); 1507 | expect($res['a']['b'])->equal(1); 1508 | ``` 1509 | 1510 | 1511 | 1512 |

setProp(string $key, $value, $data)

1513 | 1514 | **Name:** `Krak\Fun\setProp` 1515 | 1516 | Sets a property in an object: 1517 | 1518 | ```php 1519 | $res = setProp('a', 1, (object) []); 1520 | expect($res->a)->equal(1); 1521 | ``` 1522 | 1523 | 1524 | 1525 |

slice(int $start, iterable $iter, $length = INF): iterable

1526 | 1527 | **Name:** `Krak\Fun\slice` 1528 | 1529 | It takes an inclusive slice from start to a given length of an interable: 1530 | 1531 | ```php 1532 | $sliced = slice(1, range(0, 4), 2); 1533 | expect(toArray($sliced))->equal([1, 2]); 1534 | ``` 1535 | 1536 | If length is not supplied it default to the end of the iterable: 1537 | 1538 | ```php 1539 | $sliced = slice(2, range(0, 4)); 1540 | expect(toArray($sliced))->equal([2, 3, 4]); 1541 | ``` 1542 | 1543 | will not consume the iterator once the slice has been yielded: 1544 | 1545 | ```php 1546 | $i = 0; 1547 | $gen = function () use(&$i) { 1548 | foreach (range(0, 4) as $v) { 1549 | $i = $v; 1550 | (yield $i); 1551 | } 1552 | }; 1553 | $sliced = toArray(slice(1, $gen(), 2)); 1554 | expect($sliced)->equal([1, 2]); 1555 | expect($i)->equal(2); 1556 | ``` 1557 | 1558 | 1559 | 1560 |

sortFromArray(callable $fn, array $orderedElements, iterable $iter): array

1561 | 1562 | **Name:** `Krak\Fun\sortFromArray` 1563 | 1564 | Sort an iterable with a given array of ordered elements to sort by: 1565 | 1566 | ```php 1567 | $data = [['id' => 1, 'name' => 'A'], ['id' => 2, 'name' => 'B'], ['id' => 3, 'name' => 'C']]; 1568 | $res = sortFromArray(Curried\index('id'), [2, 3, 1], $data); 1569 | expect(arrayMap(Curried\index('name'), $res))->equal(['B', 'C', 'A']); 1570 | ``` 1571 | 1572 | Throws an exception if any item in the iterable is not within the orderedElements: 1573 | 1574 | ```php 1575 | expect(function () { 1576 | $data = [['id' => 1]]; 1577 | $res = sortFromArray(Curried\index('id'), [], $data); 1578 | })->throw(\LogicException::class, 'Cannot sort element key 1 because it does not exist in the ordered elements.'); 1579 | ``` 1580 | 1581 | I've found this to be very useful when you fetch records from a database with a WHERE IN clause, and you need to make sure the results are in the same order as the ids in the WHERE IN clause. 1582 | 1583 |

spread(callable $fn, array $data)

1584 | 1585 | **Name:** `Krak\Fun\spread` 1586 | 1587 | Spreads an array of arguments to a callable: 1588 | 1589 | ```php 1590 | $res = spread(function ($a, $b) { 1591 | return $a . $b; 1592 | }, ['a', 'b']); 1593 | expect($res)->equal('ab'); 1594 | ``` 1595 | 1596 | Can be used in the curried form to unpack tuple arguments: 1597 | 1598 | ```php 1599 | $res = arrayMap(Curried\spread(function (string $first, int $second) { 1600 | return $first . $second; 1601 | }), [['a', 1], ['b', 2]]); 1602 | expect($res)->equal(['a1', 'b2']); 1603 | ``` 1604 | 1605 | Note: this is basically just an alias for `call_user_func_array` or simply a functional wrapper around the `...` (spread) operator. 1606 | 1607 |

take(int $num, iterable $iter): iterable

1608 | 1609 | **Name:** `Krak\Fun\take` 1610 | 1611 | Takes the first num items from an iterable: 1612 | 1613 | ```php 1614 | $res = take(2, range(0, 10)); 1615 | expect(toArray($res))->equal([0, 1]); 1616 | ``` 1617 | 1618 | 1619 | 1620 |

takeWhile(callable $predicate, iterable $iter): iterable

1621 | 1622 | **Name:** `Krak\Fun\takeWhile` 1623 | 1624 | Takes elements from an iterable while the $predicate returns true: 1625 | 1626 | ```php 1627 | $res = takeWhile(Curried\op('>')(0), [2, 1, 0, 1, 2]); 1628 | expect(toArray($res))->equal([2, 1]); 1629 | ``` 1630 | 1631 | 1632 | 1633 |

tap(callable $tap, $value)

1634 | 1635 | **Name:** `Krak\Fun\tap` 1636 | 1637 | Calls given tap function on value and returns value: 1638 | 1639 | ```php 1640 | $loggedValues = []; 1641 | $res = tap(function (string $v) use(&$loggedValues) { 1642 | $loggedValues[] = $v; 1643 | }, 'abc'); 1644 | expect([$loggedValues[0], $res])->equal(['abc', 'abc']); 1645 | ``` 1646 | 1647 | `tap` is useful anytime you need to operate on a value and do not want to modify the return value. 1648 | 1649 |

throwIf(callable $throw, callable $if, $value)

1650 | 1651 | **Name:** `Krak\Fun\throwIf` 1652 | 1653 | Throws the given exception if value given evaluates to true: 1654 | 1655 | ```php 1656 | expect(function () { 1657 | throwIf(function (int $value) { 1658 | return new RuntimeException('Error: ' . $value); 1659 | }, function (int $value) { 1660 | return $value === 0; 1661 | }, 0); 1662 | })->throw(RuntimeException::class, 'Error: 0'); 1663 | ``` 1664 | 1665 | Returns given value if value evaluates to false: 1666 | 1667 | ```php 1668 | $res = throwIf(function (int $value) { 1669 | return new RuntimeException('Error: ' . $value); 1670 | }, function (int $value) { 1671 | return $value === 0; 1672 | }, 1); 1673 | expect($res)->equal(1); 1674 | ``` 1675 | 1676 | Note: works best with short closures! 1677 | 1678 |

toArray(iterable $iter): array

1679 | 1680 | **Name:** `Krak\Fun\toArray` 1681 | 1682 | will tranform any iterable into an array: 1683 | 1684 | ```php 1685 | $res = toArray((function () { 1686 | (yield 1); 1687 | (yield 2); 1688 | (yield 3); 1689 | })()); 1690 | expect($res)->equal([1, 2, 3]); 1691 | ``` 1692 | 1693 | can also be used as a constant: 1694 | 1695 | ```php 1696 | $res = compose(toArray, id)((function () { 1697 | (yield 1); 1698 | (yield 2); 1699 | (yield 3); 1700 | })()); 1701 | expect($res)->equal([1, 2, 3]); 1702 | ``` 1703 | 1704 | 1705 | 1706 |

toArrayWithKeys(iterable $iter): array

1707 | 1708 | **Name:** `Krak\Fun\toArrayWithKeys` 1709 | 1710 | can convert to an array and keep the keys: 1711 | 1712 | ```php 1713 | $gen = function () { 1714 | (yield 'a' => 1); 1715 | (yield 'b' => 2); 1716 | }; 1717 | expect(toArrayWithKeys($gen()))->equal(['a' => 1, 'b' => 2]); 1718 | ``` 1719 | 1720 | 1721 | 1722 |

toPairs(iterable $iter): iterable

1723 | 1724 | **Name:** `Krak\Fun\toPairs` 1725 | 1726 | Transforms an associative array into an iterable of tuples [$key, $value]: 1727 | 1728 | ```php 1729 | $res = toPairs(['a' => 1, 'b' => 2]); 1730 | expect(toArray($res))->equal([['a', 1], ['b', 2]]); 1731 | ``` 1732 | 1733 | 1734 | 1735 |

updateIndexIn(array $keys, callable $update, array $data): array

1736 | 1737 | **Name:** `Krak\Fun\updateIndexIn` 1738 | 1739 | Updates a nested element within a deep array structure: 1740 | 1741 | ```php 1742 | $data = ['a' => ['b' => ['c' => 3]]]; 1743 | $data = updateIndexIn(['a', 'b', 'c'], function ($v) { 1744 | return $v * $v; 1745 | }, $data); 1746 | expect($data)->equal(['a' => ['b' => ['c' => 9]]]); 1747 | ``` 1748 | 1749 | Throws an exception if nested key does not exist: 1750 | 1751 | ```php 1752 | expect(function () { 1753 | $data = ['a' => ['b' => ['c' => 9]]]; 1754 | updateIndexIn(['a', 'c', 'c'], function () { 1755 | }, $data); 1756 | })->throw(\RuntimeException::class, 'Could not updateIn because the keys a -> c -> c could not be found.'); 1757 | ``` 1758 | 1759 | 1760 | 1761 |

values(iterable $iter): iterable

1762 | 1763 | **Name:** `Krak\Fun\values` 1764 | 1765 | Exports only the values of an iterable: 1766 | 1767 | ```php 1768 | $res = values(['a' => 1, 'b' => 2]); 1769 | expect(toArrayWithKeys($res))->equal([1, 2]); 1770 | ``` 1771 | 1772 | 1773 | 1774 |

when(callable $if, callable $then, $value)

1775 | 1776 | **Name:** `Krak\Fun\when` 1777 | 1778 | Evaluates the given value with the $then callable if the predicate returns true: 1779 | 1780 | ```php 1781 | $if = function ($v) { 1782 | return $v == 3; 1783 | }; 1784 | $then = function ($v) { 1785 | return $v * $v; 1786 | }; 1787 | $res = when($if, $then, 3); 1788 | expect($res)->equal(9); 1789 | ``` 1790 | 1791 | But will return the given value if the predicate returns false: 1792 | 1793 | ```php 1794 | $if = function ($v) { 1795 | return $v == 3; 1796 | }; 1797 | $then = function ($v) { 1798 | return $v * $v; 1799 | }; 1800 | $res = when($if, $then, 4); 1801 | expect($res)->equal(4); 1802 | ``` 1803 | 1804 | 1805 | 1806 |

withState(callable $fn, $initialState = null)

1807 | 1808 | **Name:** `Krak\Fun\withState` 1809 | 1810 | Decorate a function with accumulating state: 1811 | 1812 | ```php 1813 | $fn = withState(function ($state, $v) { 1814 | return [$state + 1, $state . ': ' . $v]; 1815 | }, 1); 1816 | $res = arrayMap($fn, iter('abcd')); 1817 | expect($res)->equal(['1: a', '2: b', '3: c', '4: d']); 1818 | ``` 1819 | 1820 | 1821 | 1822 |

within(array $fields, iterable $iter): \Iterator

1823 | 1824 | **Name:** `Krak\Fun\within` 1825 | 1826 | Only allows keys within the given array to stay: 1827 | 1828 | ```php 1829 | $data = flip(iter('abcd')); 1830 | $res = within(['a', 'c'], $data); 1831 | expect(toArrayWithKeys($res))->equal(['a' => 0, 'c' => 2]); 1832 | ``` 1833 | 1834 | 1835 | 1836 |

without(array $fields, iterable $iter): \Iterator

1837 | 1838 | **Name:** `Krak\Fun\without` 1839 | 1840 | Filters an iterable to be without the given keys: 1841 | 1842 | ```php 1843 | $data = flip(iter('abcd')); 1844 | $res = without(['a', 'c'], $data); 1845 | expect(toArrayWithKeys($res))->equal(['b' => 1, 'd' => 3]); 1846 | ``` 1847 | 1848 | 1849 | 1850 |

zip(iterable ...$iters): \Iterator

1851 | 1852 | **Name:** `Krak\Fun\zip` 1853 | 1854 | Zips multiple iterables into an iterable n-tuples: 1855 | 1856 | ```php 1857 | $res = zip(iter('abc'), range(1, 3), [4, 5, 6]); 1858 | expect(toArray($res))->equal([['a', 1, 4], ['b', 2, 5], ['c', 3, 6]]); 1859 | ``` 1860 | 1861 | Returns an empty iterable if no iters are present: 1862 | 1863 | ```php 1864 | expect(toArray(zip()))->equal([]); 1865 | ``` 1866 | 1867 | 1868 | -------------------------------------------------------------------------------- /bench/auto-curry.php: -------------------------------------------------------------------------------- 1 | $val) { 13 | yield $key => $predicate($val); 14 | } 15 | }; 16 | } 17 | 18 | function mapAutoCurried(...$args) { 19 | return autoCurry($args, 2, function(callable $predicate, $data) { 20 | foreach ($data as $key => $val) { 21 | yield $key => $predicate($val); 22 | } 23 | }); 24 | } 25 | 26 | function mapAutoCurriedStatic(...$args) { 27 | switch (count($args)) { 28 | case 1: 29 | return map1($args[0]); 30 | default: 31 | return map2(...$args); 32 | } 33 | } 34 | 35 | function map1(callable $predicate) { 36 | return function($data) use ($predicate) { 37 | return map2($predicate, $data); 38 | }; 39 | } 40 | function map2(callable $predicate, $data) { 41 | foreach ($data as $key => $val) { 42 | yield $key => $predicate($val); 43 | } 44 | } 45 | 46 | function autoCurry(array $args, $numArgs, callable $fn) { 47 | if (count($args) >= $numArgs) { 48 | return $fn(...$args); 49 | } 50 | if (count($args) == $numArgs - 1) { 51 | return partial($fn, ...$args); 52 | } 53 | if (count($args) == 0) { 54 | return curry($fn, $numArgs - 1); 55 | } 56 | 57 | return curry( 58 | partial($fn, ...$args), 59 | ($numArgs - 1 - count($args)) 60 | ); 61 | } 62 | 63 | function plusOne() { 64 | static $fn; 65 | $fn = $fn ?: function($v) { 66 | return $v + 1; 67 | }; 68 | return $fn; 69 | } 70 | 71 | $bm = new Benchmark(); 72 | $bm->add('curried', function() { 73 | $res = compose(toArray, mapCurried(plusOne()))([1,2,3]); 74 | }); 75 | // $bm->add('auto-curried', function() { 76 | // $res = compose(toArray, mapAutoCurried(plusOne()))([1,2,3]); 77 | // }); 78 | // $bm->add('auto-curried-no-curry', function() { 79 | // $res = toArray(mapAutoCurried(plusOne(), [1,2,3])); 80 | // }); 81 | $bm->add('auto-curried-static', function() { 82 | $res = compose(toArray, mapAutoCurriedStatic(plusOne()))([1,2,3]); 83 | }); 84 | // $bm->add('auto-curried-static-no-curry', function() { 85 | // $res = toArray(mapAutoCurriedStatic(plusOne(), [1,2,3])); 86 | // }); 87 | $bm->run(); 88 | -------------------------------------------------------------------------------- /bench/named-closures.php: -------------------------------------------------------------------------------- 1 | a = $a; 33 | } 34 | 35 | public function __invoke(int $b) { 36 | return new SumCurriedArg3($this->a, $b); 37 | } 38 | } 39 | 40 | final class SumCurriedArg3 { 41 | private $a; 42 | private $b; 43 | public function __construct(int $a, int $b) { 44 | $this->a = $a; 45 | $this->b = $b; 46 | } 47 | 48 | public function __invoke(int $c) { 49 | return $this->a + $this->b + $c; 50 | } 51 | } 52 | 53 | function sumCurriedClassStateMachine(int $a) { 54 | return new SumCurried($a); 55 | } 56 | 57 | final class SumCurried { 58 | private $a; 59 | private $b; 60 | private $arg = 1; 61 | 62 | public function __construct(int $a) { 63 | $this->a = $a; 64 | } 65 | 66 | public function __invoke(...$args) { 67 | if ($this->arg === 1) { 68 | $this->arg = 2; 69 | $this->arg2(...$args); 70 | return $this; 71 | } else if ($this->arg === 2) { 72 | return $this->arg3(...$args); 73 | } 74 | } 75 | 76 | private function arg2(int $b) { 77 | $this->b = $b; 78 | } 79 | 80 | private function arg3(int $c) { 81 | return $this->a + $this->b + $c; 82 | } 83 | } 84 | 85 | function sumCurriedNamed(int $a) { 86 | return namedClosure('sumCurriedArg_b', function(int $b) use ($a) { 87 | return namedClosure('sumCurriedArg_c', function(int $c) use ($a, $b) { 88 | return $a + $b + $c; 89 | }); 90 | }); 91 | } 92 | 93 | sumCurriedClass(1)(2)(null); 94 | 95 | $bm = new Benchmark(); 96 | $bm->add('standard', function() { 97 | sumCurriedStandard(1)(2)(3); 98 | }); 99 | 100 | $bm->add('class', function() { 101 | sumCurriedClass(1)(2)(3); 102 | }); 103 | 104 | $bm->add('classSM', function() { 105 | sumCurriedClassStateMachine(1)(2)(3); 106 | }); 107 | 108 | $bm->add('named', function() { 109 | sumCurriedNamed(1)(2)(3); 110 | }); 111 | 112 | $bm->run(); 113 | -------------------------------------------------------------------------------- /bench/sort-from-array.php: -------------------------------------------------------------------------------- 1 | $id, 'name' => $id]; 33 | }, $ids); 34 | $orderedElements = $ids; 35 | shuffle($orderedElements); 36 | $fn = c\index("id"); 37 | 38 | // dump(sortFromArrayKSort($fn, $orderedElements, $data)); 39 | // dump(sortFromArraySearch($fn, $orderedElements, $data)); 40 | 41 | $bm = new Benchmark(); 42 | $bm->add('sortFromArrayKSort', function() use ($data, $orderedElements, $fn) { 43 | $res = sortFromArrayKSort($fn, $orderedElements, $data); 44 | }); 45 | 46 | $bm->add('sortFromArraySearch', function() use ($data, $orderedElements, $fn) { 47 | $res = sortFromArraySearch($fn, $orderedElements, $data); 48 | }); 49 | $bm->run(); 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "krak/fn", 3 | "description": "Functional library for php with proper currying", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "functional", 8 | "php", 9 | "curry", 10 | "partial-application" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "RJ Garcia", 15 | "email": "ragboyjr@icloud.com" 16 | } 17 | ], 18 | "autoload": { 19 | "psr-4": { 20 | "Krak\\Fun\\": "src" 21 | }, 22 | "files": ["src/fn.php", "src/curried.generated.php", "src/consts.generated.php", "src/consts.ns.generated.php", "src/c.generated.php", "src/f.generated.php", "src/generate.php"] 23 | }, 24 | "require-dev": { 25 | "krak/peridocs": "dev-master", 26 | "lavoiesl/php-benchmark": "^1.4", 27 | "nikic/php-parser": "^4.0", 28 | "peridot-php/leo": "^1.6", 29 | "peridot-php/peridot": "^1.19", 30 | "symfony/polyfill-php81": "^1.27", 31 | "symfony/var-dumper": "^4.0" 32 | }, 33 | "suggest": { 34 | "symfony/polyfill-php81": "To use arrayWrap in PHP 7" 35 | }, 36 | "extra": { 37 | "branch-alias": { 38 | "dev-master": "0.1.0-dev" 39 | } 40 | }, 41 | "minimum-stability": "dev", 42 | "prefer-stable": true, 43 | "scripts": { 44 | "test": "peridot test" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | php: 5 | build: . 6 | command: "bash -c 'echo \"Container running.\" && tail -f /dev/null'" 7 | working_dir: /var/www/html 8 | volumes: 9 | - ./:/var/www/html -------------------------------------------------------------------------------- /make-code.php: -------------------------------------------------------------------------------- 1 | create(ParserFactory::PREFER_PHP7); 25 | 26 | try { 27 | $ast = $parser->parse(file_get_contents(__DIR__ . '/src/fn.php')); 28 | $ast[0]->setDocComment(createDocComment()); 29 | 30 | $ns = $ast[0]; 31 | $originalStmts = $ns->stmts; 32 | 33 | $constStmts = filter(isInstance(Function_::class), $originalStmts); 34 | $constStmts = toArray(map(function($stmt) use ($ns) { 35 | return createConstFromFunc($stmt, $ns); 36 | }, $constStmts)); 37 | 38 | $curriedStmts = filter(andf( 39 | isInstance(Function_::class), 40 | 'Krak\Fun\Generate\isCurryable' 41 | ), $originalStmts); 42 | $curriedStmts = toArray(map(function($stmt) { 43 | return curryFunction($stmt); 44 | }, $curriedStmts)); 45 | 46 | $ast = [$ns]; 47 | } catch (Error $error) { 48 | echo "Parse error: {$error->getMessage()}\n"; 49 | return; 50 | } 51 | 52 | $pp = new PrettyPrinter\Standard(); 53 | 54 | $ns->name->parts[] = 'Curried'; 55 | $ns->stmts = toArray($curriedStmts); 56 | file_put_contents(__DIR__ . '/src/curried.generated.php', $pp->prettyPrintFile($ast)); 57 | 58 | $ast[0]->name->parts[2] = 'c'; 59 | $ast[0]->stmts = toArray(chain($curriedStmts, $constStmts)); 60 | file_put_contents(__DIR__ . '/src/c.generated.php', $pp->prettyPrintFile($ast)); 61 | 62 | $ast[0]->name->parts[2] = 'f'; 63 | $ast[0]->stmts = toArray($originalStmts); 64 | file_put_contents(__DIR__ . '/src/f.generated.php', $pp->prettyPrintFile($ast)); 65 | 66 | $ast[0]->name->parts[2] = 'Consts'; 67 | $ast[0]->stmts = toArray($constStmts); 68 | file_put_contents(__DIR__ . '/src/consts.ns.generated.php', $pp->prettyPrintFile($ast)); 69 | 70 | unset($ast[0]->name->parts[2]); 71 | $ast[0]->stmts = toArray($constStmts); 72 | file_put_contents(__DIR__ . '/src/consts.generated.php', $pp->prettyPrintFile($ast)); 73 | } 74 | 75 | main(); 76 | -------------------------------------------------------------------------------- /peridot.php: -------------------------------------------------------------------------------- 1 | true, 9 | 'nsPrefix' => 'Krak\\Fun\\', 10 | 'numTableRows' => 6, 11 | ]); 12 | }); 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /src/c.generated.php: -------------------------------------------------------------------------------- 1 | {$name}(...$optionalArgs); 10 | }; 11 | } 12 | function prop(string $key, $else = null) 13 | { 14 | return function ($data) use($key, $else) { 15 | return \property_exists($data, $key) ? $data->{$key} : $else; 16 | }; 17 | } 18 | function index($key, $else = null) 19 | { 20 | return function ($data) use($key, $else) { 21 | if (\Krak\Fun\isInstance(\ArrayAccess::class, $data)) { 22 | $exists = $data->offsetExists($key); 23 | } else { 24 | $exists = \array_key_exists($key, $data); 25 | } 26 | return $exists ? $data[$key] : $else; 27 | }; 28 | } 29 | function setProp(string $key) 30 | { 31 | return function ($value) use($key) { 32 | return function ($data) use($value, $key) { 33 | $data->{$key} = $value; 34 | return $data; 35 | }; 36 | }; 37 | } 38 | function setIndex($key) 39 | { 40 | return function ($value) use($key) { 41 | return function (array $data) use($value, $key) { 42 | $data[$key] = $value; 43 | return $data; 44 | }; 45 | }; 46 | } 47 | function setIndexIn(array $keys) 48 | { 49 | return function ($value) use($keys) { 50 | return function (array $data) use($value, $keys) { 51 | return \Krak\Fun\updateIndexIn($keys, function () use($value) { 52 | return $value; 53 | }, $data); 54 | }; 55 | }; 56 | } 57 | function propIn(array $props, $else = null) 58 | { 59 | return function ($obj) use($props, $else) { 60 | foreach ($props as $prop) { 61 | if (!\is_object($obj) || !\property_exists($obj, $prop)) { 62 | return $else; 63 | } 64 | $obj = $obj->{$prop}; 65 | } 66 | return $obj; 67 | }; 68 | } 69 | function indexIn(array $keys, $else = null) 70 | { 71 | return function (array $data) use($keys, $else) { 72 | foreach ($keys as $part) { 73 | if (!\is_array($data) || !\array_key_exists($part, $data)) { 74 | return $else; 75 | } 76 | $data = $data[$part]; 77 | } 78 | return $data; 79 | }; 80 | } 81 | function hasIndexIn(array $keys) 82 | { 83 | return function (array $data) use($keys) { 84 | foreach ($keys as $key) { 85 | if (!\is_array($data) || !\array_key_exists($key, $data)) { 86 | return false; 87 | } 88 | $data = $data[$key]; 89 | } 90 | return true; 91 | }; 92 | } 93 | function updateIndexIn(array $keys) 94 | { 95 | return function (callable $update) use($keys) { 96 | return function (array $data) use($update, $keys) { 97 | $curData =& $data; 98 | foreach (\array_slice($keys, 0, -1) as $key) { 99 | if (!\array_key_exists($key, $curData)) { 100 | throw new \RuntimeException('Could not updateIn because the keys ' . \implode(' -> ', $keys) . ' could not be found.'); 101 | } 102 | $curData =& $curData[$key]; 103 | } 104 | $lastKey = $keys[count($keys) - 1]; 105 | $curData[$lastKey] = $update($curData[$lastKey] ?? null); 106 | return $data; 107 | }; 108 | }; 109 | } 110 | function assign($obj) 111 | { 112 | return function (iterable $iter) use($obj) { 113 | foreach ($iter as $key => $value) { 114 | $obj->{$key} = $value; 115 | } 116 | return $obj; 117 | }; 118 | } 119 | function join(string $sep) 120 | { 121 | return function (iterable $iter) use($sep) { 122 | return \Krak\Fun\reduce(function ($acc, $v) use($sep) { 123 | return $acc ? $acc . $sep . $v : $v; 124 | }, $iter, ""); 125 | }; 126 | } 127 | function construct($className) 128 | { 129 | return function (...$args) use($className) { 130 | return new $className(...$args); 131 | }; 132 | } 133 | function spread(callable $fn) 134 | { 135 | return function (array $data) use($fn) { 136 | return $fn(...$data); 137 | }; 138 | } 139 | function dd(callable $dump = null, callable $die = null) 140 | { 141 | return function ($value) use($dump, $die) { 142 | $dump = $dump ?: (function_exists('dump') ? 'dump' : 'var_dump'); 143 | $dump($value); 144 | ($die ?? function () { 145 | die; 146 | })(); 147 | }; 148 | } 149 | function takeWhile(callable $predicate) 150 | { 151 | return function (iterable $iter) use($predicate) { 152 | foreach ($iter as $k => $v) { 153 | if ($predicate($v)) { 154 | (yield $k => $v); 155 | } else { 156 | return; 157 | } 158 | } 159 | }; 160 | } 161 | function dropWhile(callable $predicate) 162 | { 163 | return function (iterable $iter) use($predicate) { 164 | $stillDropping = true; 165 | foreach ($iter as $k => $v) { 166 | if ($stillDropping && $predicate($v)) { 167 | continue; 168 | } else { 169 | if ($stillDropping) { 170 | $stillDropping = false; 171 | } 172 | } 173 | (yield $k => $v); 174 | } 175 | }; 176 | } 177 | function take(int $num) 178 | { 179 | return function (iterable $iter) use($num) { 180 | return \Krak\Fun\slice(0, $iter, $num); 181 | }; 182 | } 183 | function drop(int $num) 184 | { 185 | return function (iterable $iter) use($num) { 186 | return \Krak\Fun\slice($num, $iter); 187 | }; 188 | } 189 | function slice(int $start, $length = INF) 190 | { 191 | return function (iterable $iter) use($start, $length) { 192 | assert($start >= 0); 193 | $i = 0; 194 | $end = $start + $length - 1; 195 | foreach ($iter as $k => $v) { 196 | if ($start <= $i && $i <= $end) { 197 | (yield $k => $v); 198 | } 199 | $i += 1; 200 | if ($i > $end) { 201 | return; 202 | } 203 | } 204 | }; 205 | } 206 | function chunk(int $size) 207 | { 208 | return function (iterable $iter) use($size) { 209 | assert($size > 0); 210 | $chunk = []; 211 | foreach ($iter as $v) { 212 | $chunk[] = $v; 213 | if (\count($chunk) == $size) { 214 | (yield $chunk); 215 | $chunk = []; 216 | } 217 | } 218 | if ($chunk) { 219 | (yield $chunk); 220 | } 221 | }; 222 | } 223 | function chunkBy(callable $fn, ?int $maxSize = null) 224 | { 225 | return function (iterable $iter) use($fn, $maxSize) { 226 | assert($maxSize === null || $maxSize > 0); 227 | $group = []; 228 | $groupKey = null; 229 | foreach ($iter as $v) { 230 | $curGroupKey = $fn($v); 231 | $shouldYieldGroup = $groupKey !== null && $groupKey !== $curGroupKey || $maxSize !== null && \count($group) >= $maxSize; 232 | if ($shouldYieldGroup) { 233 | (yield $group); 234 | $group = []; 235 | } 236 | $group[] = $v; 237 | $groupKey = $curGroupKey; 238 | } 239 | if (\count($group)) { 240 | (yield $group); 241 | } 242 | }; 243 | } 244 | function groupBy(callable $fn, ?int $maxSize = null) 245 | { 246 | return function (iterable $iter) use($fn, $maxSize) { 247 | return \Krak\Fun\chunkBy($fn, $iter, $maxSize); 248 | }; 249 | } 250 | function range($start, $step = null) 251 | { 252 | return function ($end) use($start, $step) { 253 | if ($start == $end) { 254 | (yield $start); 255 | } else { 256 | if ($start < $end) { 257 | $step = $step ?: 1; 258 | if ($step <= 0) { 259 | throw new \InvalidArgumentException('Step must be greater than 0.'); 260 | } 261 | for ($i = $start; $i <= $end; $i += $step) { 262 | (yield $i); 263 | } 264 | } else { 265 | $step = $step ?: -1; 266 | if ($step >= 0) { 267 | throw new \InvalidArgumentException('Step must be less than 0.'); 268 | } 269 | for ($i = $start; $i >= $end; $i += $step) { 270 | (yield $i); 271 | } 272 | } 273 | } 274 | }; 275 | } 276 | function op(string $op) 277 | { 278 | return function ($b) use($op) { 279 | return function ($a) use($b, $op) { 280 | switch ($op) { 281 | case '==': 282 | case 'eq': 283 | return $a == $b; 284 | case '!=': 285 | case 'neq': 286 | return $a != $b; 287 | case '===': 288 | return $a === $b; 289 | case '!==': 290 | return $a !== $b; 291 | case '>': 292 | case 'gt': 293 | return $a > $b; 294 | case '>=': 295 | case 'gte': 296 | return $a >= $b; 297 | case '<': 298 | case 'lt': 299 | return $a < $b; 300 | case '<=': 301 | case 'lte': 302 | return $a <= $b; 303 | case '+': 304 | return $a + $b; 305 | case '-': 306 | return $a - $b; 307 | case '*': 308 | return $a * $b; 309 | case '**': 310 | return $a ** $b; 311 | case '/': 312 | return $a / $b; 313 | case '%': 314 | return $a % $b; 315 | case '.': 316 | return $a . $b; 317 | default: 318 | throw new \LogicException('Invalid operator ' . $op); 319 | } 320 | }; 321 | }; 322 | } 323 | function flatMap(callable $map) 324 | { 325 | return function (iterable $iter) use($map) { 326 | foreach ($iter as $k => $v) { 327 | foreach ($map($v) as $k => $v) { 328 | (yield $k => $v); 329 | } 330 | } 331 | }; 332 | } 333 | function flatten($levels = INF) 334 | { 335 | return function (iterable $iter) use($levels) { 336 | if ($levels == 0) { 337 | yield from $iter; 338 | } else { 339 | if ($levels == 1) { 340 | foreach ($iter as $k => $v) { 341 | if (\is_iterable($v)) { 342 | foreach ($v as $k1 => $v1) { 343 | (yield $k1 => $v1); 344 | } 345 | } else { 346 | (yield $k => $v); 347 | } 348 | } 349 | } else { 350 | foreach ($iter as $k => $v) { 351 | if (\is_iterable($v)) { 352 | foreach (flatten($v, $levels - 1) as $k1 => $v1) { 353 | (yield $k1 => $v1); 354 | } 355 | } else { 356 | (yield $k => $v); 357 | } 358 | } 359 | } 360 | } 361 | }; 362 | } 363 | function when(callable $if) 364 | { 365 | return function (callable $then) use($if) { 366 | return function ($value) use($then, $if) { 367 | return $if($value) ? $then($value) : $value; 368 | }; 369 | }; 370 | } 371 | function pick(iterable $fields) 372 | { 373 | return function (array $data) use($fields) { 374 | $pickedData = []; 375 | foreach ($fields as $field) { 376 | $pickedData[$field] = $data[$field] ?? null; 377 | } 378 | return $pickedData; 379 | }; 380 | } 381 | function pickBy(callable $pick) 382 | { 383 | return function (array $data) use($pick) { 384 | $pickedData = []; 385 | foreach ($data as $key => $value) { 386 | if ($pick([$key, $value])) { 387 | $pickedData[$key] = $value; 388 | } 389 | } 390 | return $pickedData; 391 | }; 392 | } 393 | function within(array $fields) 394 | { 395 | return function (iterable $iter) use($fields) { 396 | return \Krak\Fun\filterKeys(\Krak\Fun\Curried\inArray($fields), $iter); 397 | }; 398 | } 399 | function without(array $fields) 400 | { 401 | return function (iterable $iter) use($fields) { 402 | return \Krak\Fun\filterKeys(\Krak\Fun\Curried\not(\Krak\Fun\Curried\inArray($fields)), $iter); 403 | }; 404 | } 405 | function pad(int $size, $padValue = null) 406 | { 407 | return function (iterable $iter) use($size, $padValue) { 408 | $i = 0; 409 | foreach ($iter as $key => $value) { 410 | (yield $value); 411 | $i += 1; 412 | } 413 | if ($i >= $size) { 414 | return; 415 | } 416 | foreach (\Krak\Fun\range($i, $size - 1) as $index) { 417 | (yield $padValue); 418 | } 419 | }; 420 | } 421 | function inArray(array $set) 422 | { 423 | return function ($item) use($set) { 424 | return \in_array($item, $set); 425 | }; 426 | } 427 | function arrayMap(callable $fn) 428 | { 429 | return function (iterable $data) use($fn) { 430 | return \array_map($fn, \is_array($data) ? $data : \Krak\Fun\toArray($data)); 431 | }; 432 | } 433 | function arrayFilter(callable $fn) 434 | { 435 | return function (iterable $data) use($fn) { 436 | return \array_filter(\is_array($data) ? $data : \Krak\Fun\toArray($data), $fn); 437 | }; 438 | } 439 | function all(callable $predicate) 440 | { 441 | return function (iterable $iter) use($predicate) { 442 | foreach ($iter as $key => $value) { 443 | if (!$predicate($value)) { 444 | return false; 445 | } 446 | } 447 | return true; 448 | }; 449 | } 450 | function any(callable $predicate) 451 | { 452 | return function (iterable $iter) use($predicate) { 453 | foreach ($iter as $key => $value) { 454 | if ($predicate($value)) { 455 | return true; 456 | } 457 | } 458 | return false; 459 | }; 460 | } 461 | function search(callable $predicate) 462 | { 463 | return function (iterable $iter) use($predicate) { 464 | foreach ($iter as $value) { 465 | if ($predicate($value)) { 466 | return $value; 467 | } 468 | } 469 | }; 470 | } 471 | function indexOf(callable $predicate) 472 | { 473 | return function (iterable $iter) use($predicate) { 474 | foreach ($iter as $key => $value) { 475 | if ($predicate($value)) { 476 | return $key; 477 | } 478 | } 479 | }; 480 | } 481 | function trans(callable $trans) 482 | { 483 | return function (callable $fn) use($trans) { 484 | return function ($data) use($fn, $trans) { 485 | return $fn($trans($data)); 486 | }; 487 | }; 488 | } 489 | function not(callable $fn) 490 | { 491 | return function (...$args) use($fn) { 492 | return !$fn(...$args); 493 | }; 494 | } 495 | function isInstance($class) 496 | { 497 | return function ($item) use($class) { 498 | return $item instanceof $class; 499 | }; 500 | } 501 | function nullable(callable $fn) 502 | { 503 | return function ($value) use($fn) { 504 | return $value === null ? $value : $fn($value); 505 | }; 506 | } 507 | function partition(callable $partition, int $numParts = 2) 508 | { 509 | return function (iterable $iter) use($partition, $numParts) { 510 | $parts = \array_fill(0, $numParts, []); 511 | foreach ($iter as $val) { 512 | $index = (int) $partition($val); 513 | $parts[$index][] = $val; 514 | } 515 | return $parts; 516 | }; 517 | } 518 | function map(callable $predicate) 519 | { 520 | return function (iterable $iter) use($predicate) { 521 | foreach ($iter as $key => $value) { 522 | (yield $key => $predicate($value)); 523 | } 524 | }; 525 | } 526 | function mapKeys(callable $predicate) 527 | { 528 | return function (iterable $iter) use($predicate) { 529 | foreach ($iter as $key => $value) { 530 | (yield $predicate($key) => $value); 531 | } 532 | }; 533 | } 534 | function mapKeyValue(callable $fn) 535 | { 536 | return function (iterable $iter) use($fn) { 537 | foreach ($iter as $key => $value) { 538 | [$key, $value] = $fn([$key, $value]); 539 | (yield $key => $value); 540 | } 541 | }; 542 | } 543 | function mapOn(array $maps) 544 | { 545 | return function (iterable $iter) use($maps) { 546 | foreach ($iter as $key => $value) { 547 | if (isset($maps[$key])) { 548 | (yield $key => $maps[$key]($value)); 549 | } else { 550 | (yield $key => $value); 551 | } 552 | } 553 | }; 554 | } 555 | function mapAccum(callable $fn, $acc = null) 556 | { 557 | return function (iterable $iter) use($fn, $acc) { 558 | $data = []; 559 | foreach ($iter as $key => $value) { 560 | [$acc, $value] = $fn($acc, $value); 561 | $data[] = $value; 562 | } 563 | return [$acc, $data]; 564 | }; 565 | } 566 | function withState($initialState = null) 567 | { 568 | return function (callable $fn) use($initialState) { 569 | $state = $initialState; 570 | return function (...$args) use($fn, &$state) { 571 | [$state, $res] = $fn($state, ...$args); 572 | return $res; 573 | }; 574 | }; 575 | } 576 | function arrayReindex(callable $fn) 577 | { 578 | return function (iterable $iter) use($fn) { 579 | $res = []; 580 | foreach ($iter as $key => $value) { 581 | $res[$fn($value)] = $value; 582 | } 583 | return $res; 584 | }; 585 | } 586 | function reindex(callable $fn) 587 | { 588 | return function (iterable $iter) use($fn) { 589 | foreach ($iter as $key => $value) { 590 | (yield $fn($value) => $value); 591 | } 592 | }; 593 | } 594 | function reduce(callable $reduce, $acc = null) 595 | { 596 | return function (iterable $iter) use($reduce, $acc) { 597 | foreach ($iter as $key => $value) { 598 | $acc = $reduce($acc, $value); 599 | } 600 | return $acc; 601 | }; 602 | } 603 | function reduceKeyValue(callable $reduce, $acc = null) 604 | { 605 | return function (iterable $iter) use($reduce, $acc) { 606 | foreach ($iter as $key => $value) { 607 | $acc = $reduce($acc, [$key, $value]); 608 | } 609 | return $acc; 610 | }; 611 | } 612 | function filter(callable $predicate) 613 | { 614 | return function (iterable $iter) use($predicate) { 615 | foreach ($iter as $key => $value) { 616 | if ($predicate($value)) { 617 | (yield $key => $value); 618 | } 619 | } 620 | }; 621 | } 622 | function filterKeys(callable $predicate) 623 | { 624 | return function (iterable $iter) use($predicate) { 625 | foreach ($iter as $key => $value) { 626 | if ($predicate($key)) { 627 | (yield $key => $value); 628 | } 629 | } 630 | }; 631 | } 632 | function partial(callable $fn) 633 | { 634 | return function (...$appliedArgs) use($fn) { 635 | return function (...$args) use($fn, $appliedArgs) { 636 | list($appliedArgs, $args) = \array_reduce($appliedArgs, function ($acc, $arg) { 637 | list($appliedArgs, $args) = $acc; 638 | if ($arg === \Krak\Fun\placeholder()) { 639 | $arg = array_shift($args); 640 | } 641 | $appliedArgs[] = $arg; 642 | return [$appliedArgs, $args]; 643 | }, [[], $args]); 644 | return $fn(...$appliedArgs, ...$args); 645 | }; 646 | }; 647 | } 648 | function tap(callable $tap) 649 | { 650 | return function ($value) use($tap) { 651 | $tap($value); 652 | return $value; 653 | }; 654 | } 655 | function throwIf(callable $throw) 656 | { 657 | return function (callable $if) use($throw) { 658 | return function ($value) use($if, $throw) { 659 | if ($if($value)) { 660 | throw $throw($value); 661 | } 662 | return $value; 663 | }; 664 | }; 665 | } 666 | function differenceWith(callable $cmp) 667 | { 668 | return function (iterable $a) use($cmp) { 669 | return function (iterable $b) use($a, $cmp) { 670 | return \Krak\Fun\filter(function ($aItem) use($cmp, $b) { 671 | return \Krak\Fun\indexOf(\Krak\Fun\partial($cmp, $aItem), $b) === null; 672 | }, $a); 673 | }; 674 | }; 675 | } 676 | function sortFromArray(callable $fn) 677 | { 678 | return function (array $orderedElements) use($fn) { 679 | return function (iterable $iter) use($orderedElements, $fn) { 680 | $data = []; 681 | $flippedElements = \array_flip($orderedElements); 682 | foreach ($iter as $value) { 683 | $key = $fn($value); 684 | if (!\array_key_exists($key, $flippedElements)) { 685 | throw new \LogicException('Cannot sort element key ' . $key . ' because it does not exist in the ordered elements.'); 686 | } 687 | $data[$flippedElements[$key]] = $value; 688 | } 689 | ksort($data); 690 | return $data; 691 | }; 692 | }; 693 | } 694 | function retry($shouldRetry = null) 695 | { 696 | return function (callable $fn) use($shouldRetry) { 697 | if (\is_null($shouldRetry)) { 698 | $shouldRetry = function ($numRetries, \Throwable $t = null) { 699 | return true; 700 | }; 701 | } 702 | if (\is_int($shouldRetry)) { 703 | $maxTries = $shouldRetry; 704 | if ($maxTries < 0) { 705 | throw new \LogicException("maxTries must be greater than or equal to 0"); 706 | } 707 | $shouldRetry = function ($numRetries, \Throwable $t = null) use($maxTries) { 708 | return $numRetries <= $maxTries; 709 | }; 710 | } 711 | if (!\is_callable($shouldRetry)) { 712 | throw new \InvalidArgumentException('shouldRetry must be an int or callable'); 713 | } 714 | $numRetries = 0; 715 | do { 716 | try { 717 | return $fn($numRetries); 718 | } catch (\Throwable $t) { 719 | } 720 | $numRetries += 1; 721 | } while ($shouldRetry($numRetries, $t)); 722 | throw $t; 723 | }; 724 | } 725 | function stack(callable $last = null, callable $resolve = null) 726 | { 727 | return function (array $funcs) use($last, $resolve) { 728 | return function (...$args) use($funcs, $resolve, $last) { 729 | return \Krak\Fun\reduce(function ($acc, $func) use($resolve) { 730 | return function (...$args) use($acc, $func, $resolve) { 731 | $args[] = $acc; 732 | $func = $resolve ? $resolve($func) : $func; 733 | return $func(...$args); 734 | }; 735 | }, $funcs, $last ?: function () { 736 | throw new \LogicException('No stack handler was able to capture this request'); 737 | }); 738 | }; 739 | }; 740 | } 741 | function each(callable $handle) 742 | { 743 | return function (iterable $iter) use($handle) { 744 | foreach ($iter as $v) { 745 | $handle($v); 746 | } 747 | }; 748 | } 749 | function onEach(callable $handle) 750 | { 751 | return function (iterable $iter) use($handle) { 752 | foreach ($iter as $v) { 753 | $handle($v); 754 | } 755 | }; 756 | } 757 | const method = 'Krak\\Fun\\method'; 758 | const prop = 'Krak\\Fun\\prop'; 759 | const index = 'Krak\\Fun\\index'; 760 | const setProp = 'Krak\\Fun\\setProp'; 761 | const setIndex = 'Krak\\Fun\\setIndex'; 762 | const setIndexIn = 'Krak\\Fun\\setIndexIn'; 763 | const propIn = 'Krak\\Fun\\propIn'; 764 | const indexIn = 'Krak\\Fun\\indexIn'; 765 | const hasIndexIn = 'Krak\\Fun\\hasIndexIn'; 766 | const updateIndexIn = 'Krak\\Fun\\updateIndexIn'; 767 | const assign = 'Krak\\Fun\\assign'; 768 | const join = 'Krak\\Fun\\join'; 769 | const construct = 'Krak\\Fun\\construct'; 770 | const spread = 'Krak\\Fun\\spread'; 771 | const dd = 'Krak\\Fun\\dd'; 772 | const takeWhile = 'Krak\\Fun\\takeWhile'; 773 | const dropWhile = 'Krak\\Fun\\dropWhile'; 774 | const take = 'Krak\\Fun\\take'; 775 | const drop = 'Krak\\Fun\\drop'; 776 | const slice = 'Krak\\Fun\\slice'; 777 | const head = 'Krak\\Fun\\head'; 778 | const chunk = 'Krak\\Fun\\chunk'; 779 | const chunkBy = 'Krak\\Fun\\chunkBy'; 780 | const groupBy = 'Krak\\Fun\\groupBy'; 781 | const range = 'Krak\\Fun\\range'; 782 | const op = 'Krak\\Fun\\op'; 783 | const andf = 'Krak\\Fun\\andf'; 784 | const orf = 'Krak\\Fun\\orf'; 785 | const chain = 'Krak\\Fun\\chain'; 786 | const zip = 'Krak\\Fun\\zip'; 787 | const flatMap = 'Krak\\Fun\\flatMap'; 788 | const flatten = 'Krak\\Fun\\flatten'; 789 | const product = 'Krak\\Fun\\product'; 790 | const when = 'Krak\\Fun\\when'; 791 | const toPairs = 'Krak\\Fun\\toPairs'; 792 | const fromPairs = 'Krak\\Fun\\fromPairs'; 793 | const pick = 'Krak\\Fun\\pick'; 794 | const pickBy = 'Krak\\Fun\\pickBy'; 795 | const within = 'Krak\\Fun\\within'; 796 | const without = 'Krak\\Fun\\without'; 797 | const compact = 'Krak\\Fun\\compact'; 798 | const arrayCompact = 'Krak\\Fun\\arrayCompact'; 799 | const pad = 'Krak\\Fun\\pad'; 800 | const inArray = 'Krak\\Fun\\inArray'; 801 | const arrayMap = 'Krak\\Fun\\arrayMap'; 802 | const arrayFilter = 'Krak\\Fun\\arrayFilter'; 803 | const arrayWrap = 'Krak\\Fun\\arrayWrap'; 804 | const all = 'Krak\\Fun\\all'; 805 | const any = 'Krak\\Fun\\any'; 806 | const search = 'Krak\\Fun\\search'; 807 | const indexOf = 'Krak\\Fun\\indexOf'; 808 | const trans = 'Krak\\Fun\\trans'; 809 | const not = 'Krak\\Fun\\not'; 810 | const isInstance = 'Krak\\Fun\\isInstance'; 811 | const isNull = 'Krak\\Fun\\isNull'; 812 | const nullable = 'Krak\\Fun\\nullable'; 813 | const partition = 'Krak\\Fun\\partition'; 814 | const map = 'Krak\\Fun\\map'; 815 | const mapKeys = 'Krak\\Fun\\mapKeys'; 816 | const mapKeyValue = 'Krak\\Fun\\mapKeyValue'; 817 | const mapOn = 'Krak\\Fun\\mapOn'; 818 | const mapAccum = 'Krak\\Fun\\mapAccum'; 819 | const withState = 'Krak\\Fun\\withState'; 820 | const arrayReindex = 'Krak\\Fun\\arrayReindex'; 821 | const reindex = 'Krak\\Fun\\reindex'; 822 | const reduce = 'Krak\\Fun\\reduce'; 823 | const reduceKeyValue = 'Krak\\Fun\\reduceKeyValue'; 824 | const filter = 'Krak\\Fun\\filter'; 825 | const filterKeys = 'Krak\\Fun\\filterKeys'; 826 | const values = 'Krak\\Fun\\values'; 827 | const keys = 'Krak\\Fun\\keys'; 828 | const flip = 'Krak\\Fun\\flip'; 829 | const curry = 'Krak\\Fun\\curry'; 830 | const placeholder = 'Krak\\Fun\\placeholder'; 831 | const _ = 'Krak\\Fun\\_'; 832 | const partial = 'Krak\\Fun\\partial'; 833 | const autoCurry = 'Krak\\Fun\\autoCurry'; 834 | const toArray = 'Krak\\Fun\\toArray'; 835 | const toArrayWithKeys = 'Krak\\Fun\\toArrayWithKeys'; 836 | const id = 'Krak\\Fun\\id'; 837 | const tap = 'Krak\\Fun\\tap'; 838 | const throwIf = 'Krak\\Fun\\throwIf'; 839 | const differenceWith = 'Krak\\Fun\\differenceWith'; 840 | const sortFromArray = 'Krak\\Fun\\sortFromArray'; 841 | const retry = 'Krak\\Fun\\retry'; 842 | const pipe = 'Krak\\Fun\\pipe'; 843 | const compose = 'Krak\\Fun\\compose'; 844 | const stack = 'Krak\\Fun\\stack'; 845 | const each = 'Krak\\Fun\\each'; 846 | const onEach = 'Krak\\Fun\\onEach'; 847 | const iter = 'Krak\\Fun\\iter'; -------------------------------------------------------------------------------- /src/consts.generated.php: -------------------------------------------------------------------------------- 1 | {$name}(...$optionalArgs); 10 | }; 11 | } 12 | function prop(string $key, $else = null) 13 | { 14 | return function ($data) use($key, $else) { 15 | return \property_exists($data, $key) ? $data->{$key} : $else; 16 | }; 17 | } 18 | function index($key, $else = null) 19 | { 20 | return function ($data) use($key, $else) { 21 | if (\Krak\Fun\isInstance(\ArrayAccess::class, $data)) { 22 | $exists = $data->offsetExists($key); 23 | } else { 24 | $exists = \array_key_exists($key, $data); 25 | } 26 | return $exists ? $data[$key] : $else; 27 | }; 28 | } 29 | function setProp(string $key) 30 | { 31 | return function ($value) use($key) { 32 | return function ($data) use($value, $key) { 33 | $data->{$key} = $value; 34 | return $data; 35 | }; 36 | }; 37 | } 38 | function setIndex($key) 39 | { 40 | return function ($value) use($key) { 41 | return function (array $data) use($value, $key) { 42 | $data[$key] = $value; 43 | return $data; 44 | }; 45 | }; 46 | } 47 | function setIndexIn(array $keys) 48 | { 49 | return function ($value) use($keys) { 50 | return function (array $data) use($value, $keys) { 51 | return \Krak\Fun\updateIndexIn($keys, function () use($value) { 52 | return $value; 53 | }, $data); 54 | }; 55 | }; 56 | } 57 | function propIn(array $props, $else = null) 58 | { 59 | return function ($obj) use($props, $else) { 60 | foreach ($props as $prop) { 61 | if (!\is_object($obj) || !\property_exists($obj, $prop)) { 62 | return $else; 63 | } 64 | $obj = $obj->{$prop}; 65 | } 66 | return $obj; 67 | }; 68 | } 69 | function indexIn(array $keys, $else = null) 70 | { 71 | return function (array $data) use($keys, $else) { 72 | foreach ($keys as $part) { 73 | if (!\is_array($data) || !\array_key_exists($part, $data)) { 74 | return $else; 75 | } 76 | $data = $data[$part]; 77 | } 78 | return $data; 79 | }; 80 | } 81 | function hasIndexIn(array $keys) 82 | { 83 | return function (array $data) use($keys) { 84 | foreach ($keys as $key) { 85 | if (!\is_array($data) || !\array_key_exists($key, $data)) { 86 | return false; 87 | } 88 | $data = $data[$key]; 89 | } 90 | return true; 91 | }; 92 | } 93 | function updateIndexIn(array $keys) 94 | { 95 | return function (callable $update) use($keys) { 96 | return function (array $data) use($update, $keys) { 97 | $curData =& $data; 98 | foreach (\array_slice($keys, 0, -1) as $key) { 99 | if (!\array_key_exists($key, $curData)) { 100 | throw new \RuntimeException('Could not updateIn because the keys ' . \implode(' -> ', $keys) . ' could not be found.'); 101 | } 102 | $curData =& $curData[$key]; 103 | } 104 | $lastKey = $keys[count($keys) - 1]; 105 | $curData[$lastKey] = $update($curData[$lastKey] ?? null); 106 | return $data; 107 | }; 108 | }; 109 | } 110 | function assign($obj) 111 | { 112 | return function (iterable $iter) use($obj) { 113 | foreach ($iter as $key => $value) { 114 | $obj->{$key} = $value; 115 | } 116 | return $obj; 117 | }; 118 | } 119 | function join(string $sep) 120 | { 121 | return function (iterable $iter) use($sep) { 122 | return \Krak\Fun\reduce(function ($acc, $v) use($sep) { 123 | return $acc ? $acc . $sep . $v : $v; 124 | }, $iter, ""); 125 | }; 126 | } 127 | function construct($className) 128 | { 129 | return function (...$args) use($className) { 130 | return new $className(...$args); 131 | }; 132 | } 133 | function spread(callable $fn) 134 | { 135 | return function (array $data) use($fn) { 136 | return $fn(...$data); 137 | }; 138 | } 139 | function dd(callable $dump = null, callable $die = null) 140 | { 141 | return function ($value) use($dump, $die) { 142 | $dump = $dump ?: (function_exists('dump') ? 'dump' : 'var_dump'); 143 | $dump($value); 144 | ($die ?? function () { 145 | die; 146 | })(); 147 | }; 148 | } 149 | function takeWhile(callable $predicate) 150 | { 151 | return function (iterable $iter) use($predicate) { 152 | foreach ($iter as $k => $v) { 153 | if ($predicate($v)) { 154 | (yield $k => $v); 155 | } else { 156 | return; 157 | } 158 | } 159 | }; 160 | } 161 | function dropWhile(callable $predicate) 162 | { 163 | return function (iterable $iter) use($predicate) { 164 | $stillDropping = true; 165 | foreach ($iter as $k => $v) { 166 | if ($stillDropping && $predicate($v)) { 167 | continue; 168 | } else { 169 | if ($stillDropping) { 170 | $stillDropping = false; 171 | } 172 | } 173 | (yield $k => $v); 174 | } 175 | }; 176 | } 177 | function take(int $num) 178 | { 179 | return function (iterable $iter) use($num) { 180 | return \Krak\Fun\slice(0, $iter, $num); 181 | }; 182 | } 183 | function drop(int $num) 184 | { 185 | return function (iterable $iter) use($num) { 186 | return \Krak\Fun\slice($num, $iter); 187 | }; 188 | } 189 | function slice(int $start, $length = INF) 190 | { 191 | return function (iterable $iter) use($start, $length) { 192 | assert($start >= 0); 193 | $i = 0; 194 | $end = $start + $length - 1; 195 | foreach ($iter as $k => $v) { 196 | if ($start <= $i && $i <= $end) { 197 | (yield $k => $v); 198 | } 199 | $i += 1; 200 | if ($i > $end) { 201 | return; 202 | } 203 | } 204 | }; 205 | } 206 | function chunk(int $size) 207 | { 208 | return function (iterable $iter) use($size) { 209 | assert($size > 0); 210 | $chunk = []; 211 | foreach ($iter as $v) { 212 | $chunk[] = $v; 213 | if (\count($chunk) == $size) { 214 | (yield $chunk); 215 | $chunk = []; 216 | } 217 | } 218 | if ($chunk) { 219 | (yield $chunk); 220 | } 221 | }; 222 | } 223 | function chunkBy(callable $fn, ?int $maxSize = null) 224 | { 225 | return function (iterable $iter) use($fn, $maxSize) { 226 | assert($maxSize === null || $maxSize > 0); 227 | $group = []; 228 | $groupKey = null; 229 | foreach ($iter as $v) { 230 | $curGroupKey = $fn($v); 231 | $shouldYieldGroup = $groupKey !== null && $groupKey !== $curGroupKey || $maxSize !== null && \count($group) >= $maxSize; 232 | if ($shouldYieldGroup) { 233 | (yield $group); 234 | $group = []; 235 | } 236 | $group[] = $v; 237 | $groupKey = $curGroupKey; 238 | } 239 | if (\count($group)) { 240 | (yield $group); 241 | } 242 | }; 243 | } 244 | function groupBy(callable $fn, ?int $maxSize = null) 245 | { 246 | return function (iterable $iter) use($fn, $maxSize) { 247 | return \Krak\Fun\chunkBy($fn, $iter, $maxSize); 248 | }; 249 | } 250 | function range($start, $step = null) 251 | { 252 | return function ($end) use($start, $step) { 253 | if ($start == $end) { 254 | (yield $start); 255 | } else { 256 | if ($start < $end) { 257 | $step = $step ?: 1; 258 | if ($step <= 0) { 259 | throw new \InvalidArgumentException('Step must be greater than 0.'); 260 | } 261 | for ($i = $start; $i <= $end; $i += $step) { 262 | (yield $i); 263 | } 264 | } else { 265 | $step = $step ?: -1; 266 | if ($step >= 0) { 267 | throw new \InvalidArgumentException('Step must be less than 0.'); 268 | } 269 | for ($i = $start; $i >= $end; $i += $step) { 270 | (yield $i); 271 | } 272 | } 273 | } 274 | }; 275 | } 276 | function op(string $op) 277 | { 278 | return function ($b) use($op) { 279 | return function ($a) use($b, $op) { 280 | switch ($op) { 281 | case '==': 282 | case 'eq': 283 | return $a == $b; 284 | case '!=': 285 | case 'neq': 286 | return $a != $b; 287 | case '===': 288 | return $a === $b; 289 | case '!==': 290 | return $a !== $b; 291 | case '>': 292 | case 'gt': 293 | return $a > $b; 294 | case '>=': 295 | case 'gte': 296 | return $a >= $b; 297 | case '<': 298 | case 'lt': 299 | return $a < $b; 300 | case '<=': 301 | case 'lte': 302 | return $a <= $b; 303 | case '+': 304 | return $a + $b; 305 | case '-': 306 | return $a - $b; 307 | case '*': 308 | return $a * $b; 309 | case '**': 310 | return $a ** $b; 311 | case '/': 312 | return $a / $b; 313 | case '%': 314 | return $a % $b; 315 | case '.': 316 | return $a . $b; 317 | default: 318 | throw new \LogicException('Invalid operator ' . $op); 319 | } 320 | }; 321 | }; 322 | } 323 | function flatMap(callable $map) 324 | { 325 | return function (iterable $iter) use($map) { 326 | foreach ($iter as $k => $v) { 327 | foreach ($map($v) as $k => $v) { 328 | (yield $k => $v); 329 | } 330 | } 331 | }; 332 | } 333 | function flatten($levels = INF) 334 | { 335 | return function (iterable $iter) use($levels) { 336 | if ($levels == 0) { 337 | yield from $iter; 338 | } else { 339 | if ($levels == 1) { 340 | foreach ($iter as $k => $v) { 341 | if (\is_iterable($v)) { 342 | foreach ($v as $k1 => $v1) { 343 | (yield $k1 => $v1); 344 | } 345 | } else { 346 | (yield $k => $v); 347 | } 348 | } 349 | } else { 350 | foreach ($iter as $k => $v) { 351 | if (\is_iterable($v)) { 352 | foreach (flatten($v, $levels - 1) as $k1 => $v1) { 353 | (yield $k1 => $v1); 354 | } 355 | } else { 356 | (yield $k => $v); 357 | } 358 | } 359 | } 360 | } 361 | }; 362 | } 363 | function when(callable $if) 364 | { 365 | return function (callable $then) use($if) { 366 | return function ($value) use($then, $if) { 367 | return $if($value) ? $then($value) : $value; 368 | }; 369 | }; 370 | } 371 | function pick(iterable $fields) 372 | { 373 | return function (array $data) use($fields) { 374 | $pickedData = []; 375 | foreach ($fields as $field) { 376 | $pickedData[$field] = $data[$field] ?? null; 377 | } 378 | return $pickedData; 379 | }; 380 | } 381 | function pickBy(callable $pick) 382 | { 383 | return function (array $data) use($pick) { 384 | $pickedData = []; 385 | foreach ($data as $key => $value) { 386 | if ($pick([$key, $value])) { 387 | $pickedData[$key] = $value; 388 | } 389 | } 390 | return $pickedData; 391 | }; 392 | } 393 | function within(array $fields) 394 | { 395 | return function (iterable $iter) use($fields) { 396 | return \Krak\Fun\filterKeys(\Krak\Fun\Curried\inArray($fields), $iter); 397 | }; 398 | } 399 | function without(array $fields) 400 | { 401 | return function (iterable $iter) use($fields) { 402 | return \Krak\Fun\filterKeys(\Krak\Fun\Curried\not(\Krak\Fun\Curried\inArray($fields)), $iter); 403 | }; 404 | } 405 | function pad(int $size, $padValue = null) 406 | { 407 | return function (iterable $iter) use($size, $padValue) { 408 | $i = 0; 409 | foreach ($iter as $key => $value) { 410 | (yield $value); 411 | $i += 1; 412 | } 413 | if ($i >= $size) { 414 | return; 415 | } 416 | foreach (\Krak\Fun\range($i, $size - 1) as $index) { 417 | (yield $padValue); 418 | } 419 | }; 420 | } 421 | function inArray(array $set) 422 | { 423 | return function ($item) use($set) { 424 | return \in_array($item, $set); 425 | }; 426 | } 427 | function arrayMap(callable $fn) 428 | { 429 | return function (iterable $data) use($fn) { 430 | return \array_map($fn, \is_array($data) ? $data : \Krak\Fun\toArray($data)); 431 | }; 432 | } 433 | function arrayFilter(callable $fn) 434 | { 435 | return function (iterable $data) use($fn) { 436 | return \array_filter(\is_array($data) ? $data : \Krak\Fun\toArray($data), $fn); 437 | }; 438 | } 439 | function all(callable $predicate) 440 | { 441 | return function (iterable $iter) use($predicate) { 442 | foreach ($iter as $key => $value) { 443 | if (!$predicate($value)) { 444 | return false; 445 | } 446 | } 447 | return true; 448 | }; 449 | } 450 | function any(callable $predicate) 451 | { 452 | return function (iterable $iter) use($predicate) { 453 | foreach ($iter as $key => $value) { 454 | if ($predicate($value)) { 455 | return true; 456 | } 457 | } 458 | return false; 459 | }; 460 | } 461 | function search(callable $predicate) 462 | { 463 | return function (iterable $iter) use($predicate) { 464 | foreach ($iter as $value) { 465 | if ($predicate($value)) { 466 | return $value; 467 | } 468 | } 469 | }; 470 | } 471 | function indexOf(callable $predicate) 472 | { 473 | return function (iterable $iter) use($predicate) { 474 | foreach ($iter as $key => $value) { 475 | if ($predicate($value)) { 476 | return $key; 477 | } 478 | } 479 | }; 480 | } 481 | function trans(callable $trans) 482 | { 483 | return function (callable $fn) use($trans) { 484 | return function ($data) use($fn, $trans) { 485 | return $fn($trans($data)); 486 | }; 487 | }; 488 | } 489 | function not(callable $fn) 490 | { 491 | return function (...$args) use($fn) { 492 | return !$fn(...$args); 493 | }; 494 | } 495 | function isInstance($class) 496 | { 497 | return function ($item) use($class) { 498 | return $item instanceof $class; 499 | }; 500 | } 501 | function nullable(callable $fn) 502 | { 503 | return function ($value) use($fn) { 504 | return $value === null ? $value : $fn($value); 505 | }; 506 | } 507 | function partition(callable $partition, int $numParts = 2) 508 | { 509 | return function (iterable $iter) use($partition, $numParts) { 510 | $parts = \array_fill(0, $numParts, []); 511 | foreach ($iter as $val) { 512 | $index = (int) $partition($val); 513 | $parts[$index][] = $val; 514 | } 515 | return $parts; 516 | }; 517 | } 518 | function map(callable $predicate) 519 | { 520 | return function (iterable $iter) use($predicate) { 521 | foreach ($iter as $key => $value) { 522 | (yield $key => $predicate($value)); 523 | } 524 | }; 525 | } 526 | function mapKeys(callable $predicate) 527 | { 528 | return function (iterable $iter) use($predicate) { 529 | foreach ($iter as $key => $value) { 530 | (yield $predicate($key) => $value); 531 | } 532 | }; 533 | } 534 | function mapKeyValue(callable $fn) 535 | { 536 | return function (iterable $iter) use($fn) { 537 | foreach ($iter as $key => $value) { 538 | [$key, $value] = $fn([$key, $value]); 539 | (yield $key => $value); 540 | } 541 | }; 542 | } 543 | function mapOn(array $maps) 544 | { 545 | return function (iterable $iter) use($maps) { 546 | foreach ($iter as $key => $value) { 547 | if (isset($maps[$key])) { 548 | (yield $key => $maps[$key]($value)); 549 | } else { 550 | (yield $key => $value); 551 | } 552 | } 553 | }; 554 | } 555 | function mapAccum(callable $fn, $acc = null) 556 | { 557 | return function (iterable $iter) use($fn, $acc) { 558 | $data = []; 559 | foreach ($iter as $key => $value) { 560 | [$acc, $value] = $fn($acc, $value); 561 | $data[] = $value; 562 | } 563 | return [$acc, $data]; 564 | }; 565 | } 566 | function withState($initialState = null) 567 | { 568 | return function (callable $fn) use($initialState) { 569 | $state = $initialState; 570 | return function (...$args) use($fn, &$state) { 571 | [$state, $res] = $fn($state, ...$args); 572 | return $res; 573 | }; 574 | }; 575 | } 576 | function arrayReindex(callable $fn) 577 | { 578 | return function (iterable $iter) use($fn) { 579 | $res = []; 580 | foreach ($iter as $key => $value) { 581 | $res[$fn($value)] = $value; 582 | } 583 | return $res; 584 | }; 585 | } 586 | function reindex(callable $fn) 587 | { 588 | return function (iterable $iter) use($fn) { 589 | foreach ($iter as $key => $value) { 590 | (yield $fn($value) => $value); 591 | } 592 | }; 593 | } 594 | function reduce(callable $reduce, $acc = null) 595 | { 596 | return function (iterable $iter) use($reduce, $acc) { 597 | foreach ($iter as $key => $value) { 598 | $acc = $reduce($acc, $value); 599 | } 600 | return $acc; 601 | }; 602 | } 603 | function reduceKeyValue(callable $reduce, $acc = null) 604 | { 605 | return function (iterable $iter) use($reduce, $acc) { 606 | foreach ($iter as $key => $value) { 607 | $acc = $reduce($acc, [$key, $value]); 608 | } 609 | return $acc; 610 | }; 611 | } 612 | function filter(callable $predicate) 613 | { 614 | return function (iterable $iter) use($predicate) { 615 | foreach ($iter as $key => $value) { 616 | if ($predicate($value)) { 617 | (yield $key => $value); 618 | } 619 | } 620 | }; 621 | } 622 | function filterKeys(callable $predicate) 623 | { 624 | return function (iterable $iter) use($predicate) { 625 | foreach ($iter as $key => $value) { 626 | if ($predicate($key)) { 627 | (yield $key => $value); 628 | } 629 | } 630 | }; 631 | } 632 | function partial(callable $fn) 633 | { 634 | return function (...$appliedArgs) use($fn) { 635 | return function (...$args) use($fn, $appliedArgs) { 636 | list($appliedArgs, $args) = \array_reduce($appliedArgs, function ($acc, $arg) { 637 | list($appliedArgs, $args) = $acc; 638 | if ($arg === \Krak\Fun\placeholder()) { 639 | $arg = array_shift($args); 640 | } 641 | $appliedArgs[] = $arg; 642 | return [$appliedArgs, $args]; 643 | }, [[], $args]); 644 | return $fn(...$appliedArgs, ...$args); 645 | }; 646 | }; 647 | } 648 | function tap(callable $tap) 649 | { 650 | return function ($value) use($tap) { 651 | $tap($value); 652 | return $value; 653 | }; 654 | } 655 | function throwIf(callable $throw) 656 | { 657 | return function (callable $if) use($throw) { 658 | return function ($value) use($if, $throw) { 659 | if ($if($value)) { 660 | throw $throw($value); 661 | } 662 | return $value; 663 | }; 664 | }; 665 | } 666 | function differenceWith(callable $cmp) 667 | { 668 | return function (iterable $a) use($cmp) { 669 | return function (iterable $b) use($a, $cmp) { 670 | return \Krak\Fun\filter(function ($aItem) use($cmp, $b) { 671 | return \Krak\Fun\indexOf(\Krak\Fun\partial($cmp, $aItem), $b) === null; 672 | }, $a); 673 | }; 674 | }; 675 | } 676 | function sortFromArray(callable $fn) 677 | { 678 | return function (array $orderedElements) use($fn) { 679 | return function (iterable $iter) use($orderedElements, $fn) { 680 | $data = []; 681 | $flippedElements = \array_flip($orderedElements); 682 | foreach ($iter as $value) { 683 | $key = $fn($value); 684 | if (!\array_key_exists($key, $flippedElements)) { 685 | throw new \LogicException('Cannot sort element key ' . $key . ' because it does not exist in the ordered elements.'); 686 | } 687 | $data[$flippedElements[$key]] = $value; 688 | } 689 | ksort($data); 690 | return $data; 691 | }; 692 | }; 693 | } 694 | function retry($shouldRetry = null) 695 | { 696 | return function (callable $fn) use($shouldRetry) { 697 | if (\is_null($shouldRetry)) { 698 | $shouldRetry = function ($numRetries, \Throwable $t = null) { 699 | return true; 700 | }; 701 | } 702 | if (\is_int($shouldRetry)) { 703 | $maxTries = $shouldRetry; 704 | if ($maxTries < 0) { 705 | throw new \LogicException("maxTries must be greater than or equal to 0"); 706 | } 707 | $shouldRetry = function ($numRetries, \Throwable $t = null) use($maxTries) { 708 | return $numRetries <= $maxTries; 709 | }; 710 | } 711 | if (!\is_callable($shouldRetry)) { 712 | throw new \InvalidArgumentException('shouldRetry must be an int or callable'); 713 | } 714 | $numRetries = 0; 715 | do { 716 | try { 717 | return $fn($numRetries); 718 | } catch (\Throwable $t) { 719 | } 720 | $numRetries += 1; 721 | } while ($shouldRetry($numRetries, $t)); 722 | throw $t; 723 | }; 724 | } 725 | function stack(callable $last = null, callable $resolve = null) 726 | { 727 | return function (array $funcs) use($last, $resolve) { 728 | return function (...$args) use($funcs, $resolve, $last) { 729 | return \Krak\Fun\reduce(function ($acc, $func) use($resolve) { 730 | return function (...$args) use($acc, $func, $resolve) { 731 | $args[] = $acc; 732 | $func = $resolve ? $resolve($func) : $func; 733 | return $func(...$args); 734 | }; 735 | }, $funcs, $last ?: function () { 736 | throw new \LogicException('No stack handler was able to capture this request'); 737 | }); 738 | }; 739 | }; 740 | } 741 | function each(callable $handle) 742 | { 743 | return function (iterable $iter) use($handle) { 744 | foreach ($iter as $v) { 745 | $handle($v); 746 | } 747 | }; 748 | } 749 | function onEach(callable $handle) 750 | { 751 | return function (iterable $iter) use($handle) { 752 | foreach ($iter as $v) { 753 | $handle($v); 754 | } 755 | }; 756 | } -------------------------------------------------------------------------------- /src/f.generated.php: -------------------------------------------------------------------------------- 1 | {$name}(...$optionalArgs); 10 | } 11 | function prop(string $key, $data, $else = null) 12 | { 13 | return \property_exists($data, $key) ? $data->{$key} : $else; 14 | } 15 | function index($key, $data, $else = null) 16 | { 17 | if (\Krak\Fun\isInstance(\ArrayAccess::class, $data)) { 18 | $exists = $data->offsetExists($key); 19 | } else { 20 | $exists = \array_key_exists($key, $data); 21 | } 22 | return $exists ? $data[$key] : $else; 23 | } 24 | function setProp(string $key, $value, $data) 25 | { 26 | $data->{$key} = $value; 27 | return $data; 28 | } 29 | function setIndex($key, $value, array $data) 30 | { 31 | $data[$key] = $value; 32 | return $data; 33 | } 34 | function setIndexIn(array $keys, $value, array $data) 35 | { 36 | return \Krak\Fun\updateIndexIn($keys, function () use($value) { 37 | return $value; 38 | }, $data); 39 | } 40 | function propIn(array $props, $obj, $else = null) 41 | { 42 | foreach ($props as $prop) { 43 | if (!\is_object($obj) || !\property_exists($obj, $prop)) { 44 | return $else; 45 | } 46 | $obj = $obj->{$prop}; 47 | } 48 | return $obj; 49 | } 50 | function indexIn(array $keys, array $data, $else = null) 51 | { 52 | foreach ($keys as $part) { 53 | if (!\is_array($data) || !\array_key_exists($part, $data)) { 54 | return $else; 55 | } 56 | $data = $data[$part]; 57 | } 58 | return $data; 59 | } 60 | function hasIndexIn(array $keys, array $data) : bool 61 | { 62 | foreach ($keys as $key) { 63 | if (!\is_array($data) || !\array_key_exists($key, $data)) { 64 | return false; 65 | } 66 | $data = $data[$key]; 67 | } 68 | return true; 69 | } 70 | function updateIndexIn(array $keys, callable $update, array $data) : array 71 | { 72 | $curData =& $data; 73 | foreach (\array_slice($keys, 0, -1) as $key) { 74 | if (!\array_key_exists($key, $curData)) { 75 | throw new \RuntimeException('Could not updateIn because the keys ' . \implode(' -> ', $keys) . ' could not be found.'); 76 | } 77 | $curData =& $curData[$key]; 78 | } 79 | $lastKey = $keys[count($keys) - 1]; 80 | $curData[$lastKey] = $update($curData[$lastKey] ?? null); 81 | return $data; 82 | } 83 | // UTILITY 84 | function assign($obj, iterable $iter) 85 | { 86 | foreach ($iter as $key => $value) { 87 | $obj->{$key} = $value; 88 | } 89 | return $obj; 90 | } 91 | function join(string $sep, iterable $iter) 92 | { 93 | return \Krak\Fun\reduce(function ($acc, $v) use($sep) { 94 | return $acc ? $acc . $sep . $v : $v; 95 | }, $iter, ""); 96 | } 97 | function construct($className, ...$args) 98 | { 99 | return new $className(...$args); 100 | } 101 | function spread(callable $fn, array $data) 102 | { 103 | return $fn(...$data); 104 | } 105 | function dd($value, callable $dump = null, callable $die = null) 106 | { 107 | $dump = $dump ?: (function_exists('dump') ? 'dump' : 'var_dump'); 108 | $dump($value); 109 | ($die ?? function () { 110 | die; 111 | })(); 112 | } 113 | // SLICING 114 | function takeWhile(callable $predicate, iterable $iter) : iterable 115 | { 116 | foreach ($iter as $k => $v) { 117 | if ($predicate($v)) { 118 | (yield $k => $v); 119 | } else { 120 | return; 121 | } 122 | } 123 | } 124 | function dropWhile(callable $predicate, iterable $iter) : iterable 125 | { 126 | $stillDropping = true; 127 | foreach ($iter as $k => $v) { 128 | if ($stillDropping && $predicate($v)) { 129 | continue; 130 | } else { 131 | if ($stillDropping) { 132 | $stillDropping = false; 133 | } 134 | } 135 | (yield $k => $v); 136 | } 137 | } 138 | function take(int $num, iterable $iter) : iterable 139 | { 140 | return \Krak\Fun\slice(0, $iter, $num); 141 | } 142 | function drop(int $num, iterable $iter) : iterable 143 | { 144 | return \Krak\Fun\slice($num, $iter); 145 | } 146 | function slice(int $start, iterable $iter, $length = INF) : iterable 147 | { 148 | assert($start >= 0); 149 | $i = 0; 150 | $end = $start + $length - 1; 151 | foreach ($iter as $k => $v) { 152 | if ($start <= $i && $i <= $end) { 153 | (yield $k => $v); 154 | } 155 | $i += 1; 156 | if ($i > $end) { 157 | return; 158 | } 159 | } 160 | } 161 | function head(iterable $iter) 162 | { 163 | foreach ($iter as $v) { 164 | return $v; 165 | } 166 | } 167 | function chunk(int $size, iterable $iter) : iterable 168 | { 169 | assert($size > 0); 170 | $chunk = []; 171 | foreach ($iter as $v) { 172 | $chunk[] = $v; 173 | if (\count($chunk) == $size) { 174 | (yield $chunk); 175 | $chunk = []; 176 | } 177 | } 178 | if ($chunk) { 179 | (yield $chunk); 180 | } 181 | } 182 | function chunkBy(callable $fn, iterable $iter, ?int $maxSize = null) : iterable 183 | { 184 | assert($maxSize === null || $maxSize > 0); 185 | $group = []; 186 | $groupKey = null; 187 | foreach ($iter as $v) { 188 | $curGroupKey = $fn($v); 189 | $shouldYieldGroup = $groupKey !== null && $groupKey !== $curGroupKey || $maxSize !== null && \count($group) >= $maxSize; 190 | if ($shouldYieldGroup) { 191 | (yield $group); 192 | $group = []; 193 | } 194 | $group[] = $v; 195 | $groupKey = $curGroupKey; 196 | } 197 | if (\count($group)) { 198 | (yield $group); 199 | } 200 | } 201 | function groupBy(callable $fn, iterable $iter, ?int $maxSize = null) : iterable 202 | { 203 | return \Krak\Fun\chunkBy($fn, $iter, $maxSize); 204 | } 205 | // GENERATORS 206 | function range($start, $end, $step = null) 207 | { 208 | if ($start == $end) { 209 | (yield $start); 210 | } else { 211 | if ($start < $end) { 212 | $step = $step ?: 1; 213 | if ($step <= 0) { 214 | throw new \InvalidArgumentException('Step must be greater than 0.'); 215 | } 216 | for ($i = $start; $i <= $end; $i += $step) { 217 | (yield $i); 218 | } 219 | } else { 220 | $step = $step ?: -1; 221 | if ($step >= 0) { 222 | throw new \InvalidArgumentException('Step must be less than 0.'); 223 | } 224 | for ($i = $start; $i >= $end; $i += $step) { 225 | (yield $i); 226 | } 227 | } 228 | } 229 | } 230 | // OPERATORS 231 | function op(string $op, $b, $a) 232 | { 233 | switch ($op) { 234 | case '==': 235 | case 'eq': 236 | return $a == $b; 237 | case '!=': 238 | case 'neq': 239 | return $a != $b; 240 | case '===': 241 | return $a === $b; 242 | case '!==': 243 | return $a !== $b; 244 | case '>': 245 | case 'gt': 246 | return $a > $b; 247 | case '>=': 248 | case 'gte': 249 | return $a >= $b; 250 | case '<': 251 | case 'lt': 252 | return $a < $b; 253 | case '<=': 254 | case 'lte': 255 | return $a <= $b; 256 | case '+': 257 | return $a + $b; 258 | case '-': 259 | return $a - $b; 260 | case '*': 261 | return $a * $b; 262 | case '**': 263 | return $a ** $b; 264 | case '/': 265 | return $a / $b; 266 | case '%': 267 | return $a % $b; 268 | case '.': 269 | return $a . $b; 270 | default: 271 | throw new \LogicException('Invalid operator ' . $op); 272 | } 273 | } 274 | function andf(callable ...$fns) 275 | { 276 | return function ($el) use($fns) { 277 | foreach ($fns as $fn) { 278 | if (!$fn($el)) { 279 | return false; 280 | } 281 | } 282 | return true; 283 | }; 284 | } 285 | function orf(callable ...$fns) 286 | { 287 | return function ($el) use($fns) { 288 | foreach ($fns as $fn) { 289 | if ($fn($el)) { 290 | return true; 291 | } 292 | } 293 | return false; 294 | }; 295 | } 296 | function chain(iterable ...$iters) 297 | { 298 | foreach ($iters as $iter) { 299 | foreach ($iter as $k => $v) { 300 | (yield $k => $v); 301 | } 302 | } 303 | } 304 | function zip(iterable ...$iters) : \Iterator 305 | { 306 | if (count($iters) == 0) { 307 | return; 308 | } 309 | $iters = \array_map(iter::class, $iters); 310 | while (true) { 311 | $tup = []; 312 | foreach ($iters as $iter) { 313 | if (!$iter->valid()) { 314 | return; 315 | } 316 | $tup[] = $iter->current(); 317 | $iter->next(); 318 | } 319 | (yield $tup); 320 | } 321 | } 322 | function flatMap(callable $map, iterable $iter) : iterable 323 | { 324 | foreach ($iter as $k => $v) { 325 | foreach ($map($v) as $k => $v) { 326 | (yield $k => $v); 327 | } 328 | } 329 | } 330 | function flatten(iterable $iter, $levels = INF) : iterable 331 | { 332 | if ($levels == 0) { 333 | yield from $iter; 334 | } else { 335 | if ($levels == 1) { 336 | foreach ($iter as $k => $v) { 337 | if (\is_iterable($v)) { 338 | foreach ($v as $k1 => $v1) { 339 | (yield $k1 => $v1); 340 | } 341 | } else { 342 | (yield $k => $v); 343 | } 344 | } 345 | } else { 346 | foreach ($iter as $k => $v) { 347 | if (\is_iterable($v)) { 348 | foreach (flatten($v, $levels - 1) as $k1 => $v1) { 349 | (yield $k1 => $v1); 350 | } 351 | } else { 352 | (yield $k => $v); 353 | } 354 | } 355 | } 356 | } 357 | } 358 | function product(iterable ...$iters) : iterable 359 | { 360 | if (count($iters) === 0) { 361 | yield from []; 362 | return; 363 | } 364 | if (count($iters) === 1) { 365 | yield from \Krak\Fun\map(function ($v) { 366 | return [$v]; 367 | }, $iters[0]); 368 | return; 369 | } 370 | foreach ($iters[0] as $value) { 371 | yield from \Krak\Fun\map(function (array $tup) use($value) { 372 | array_unshift($tup, $value); 373 | return $tup; 374 | }, \Krak\Fun\product(...\array_slice($iters, 1))); 375 | } 376 | } 377 | function when(callable $if, callable $then, $value) 378 | { 379 | return $if($value) ? $then($value) : $value; 380 | } 381 | function toPairs(iterable $iter) : iterable 382 | { 383 | foreach ($iter as $key => $val) { 384 | (yield [$key, $val]); 385 | } 386 | } 387 | function fromPairs(iterable $iter) : iterable 388 | { 389 | foreach ($iter as list($key, $val)) { 390 | (yield $key => $val); 391 | } 392 | } 393 | function pick(iterable $fields, array $data) : array 394 | { 395 | $pickedData = []; 396 | foreach ($fields as $field) { 397 | $pickedData[$field] = $data[$field] ?? null; 398 | } 399 | return $pickedData; 400 | } 401 | function pickBy(callable $pick, array $data) : array 402 | { 403 | $pickedData = []; 404 | foreach ($data as $key => $value) { 405 | if ($pick([$key, $value])) { 406 | $pickedData[$key] = $value; 407 | } 408 | } 409 | return $pickedData; 410 | } 411 | function within(array $fields, iterable $iter) : \Iterator 412 | { 413 | return \Krak\Fun\filterKeys(\Krak\Fun\Curried\inArray($fields), $iter); 414 | } 415 | function without(array $fields, iterable $iter) : \Iterator 416 | { 417 | return \Krak\Fun\filterKeys(\Krak\Fun\Curried\not(\Krak\Fun\Curried\inArray($fields)), $iter); 418 | } 419 | function compact(iterable $iter) : iterable 420 | { 421 | foreach ($iter as $key => $val) { 422 | if ($val !== null) { 423 | (yield $key => $val); 424 | } 425 | } 426 | } 427 | function arrayCompact(iterable $iter) : array 428 | { 429 | $vals = []; 430 | foreach ($iter as $key => $val) { 431 | if ($val !== null) { 432 | $vals[$key] = $val; 433 | } 434 | } 435 | return $vals; 436 | } 437 | function pad(int $size, iterable $iter, $padValue = null) : iterable 438 | { 439 | $i = 0; 440 | foreach ($iter as $key => $value) { 441 | (yield $value); 442 | $i += 1; 443 | } 444 | if ($i >= $size) { 445 | return; 446 | } 447 | foreach (\Krak\Fun\range($i, $size - 1) as $index) { 448 | (yield $padValue); 449 | } 450 | } 451 | // ALIASES 452 | function inArray(array $set, $item) : bool 453 | { 454 | return \in_array($item, $set); 455 | } 456 | function arrayMap(callable $fn, iterable $data) : array 457 | { 458 | return \array_map($fn, \is_array($data) ? $data : \Krak\Fun\toArray($data)); 459 | } 460 | function arrayFilter(callable $fn, iterable $data) : array 461 | { 462 | return \array_filter(\is_array($data) ? $data : \Krak\Fun\toArray($data), $fn); 463 | } 464 | function arrayWrap($value) 465 | { 466 | return is_array($value) && array_is_list($value) ? $value : [$value]; 467 | } 468 | function all(callable $predicate, iterable $iter) : bool 469 | { 470 | foreach ($iter as $key => $value) { 471 | if (!$predicate($value)) { 472 | return false; 473 | } 474 | } 475 | return true; 476 | } 477 | function any(callable $predicate, iterable $iter) : bool 478 | { 479 | foreach ($iter as $key => $value) { 480 | if ($predicate($value)) { 481 | return true; 482 | } 483 | } 484 | return false; 485 | } 486 | function search(callable $predicate, iterable $iter) 487 | { 488 | foreach ($iter as $value) { 489 | if ($predicate($value)) { 490 | return $value; 491 | } 492 | } 493 | } 494 | function indexOf(callable $predicate, iterable $iter) 495 | { 496 | foreach ($iter as $key => $value) { 497 | if ($predicate($value)) { 498 | return $key; 499 | } 500 | } 501 | } 502 | function trans(callable $trans, callable $fn, $data) 503 | { 504 | return $fn($trans($data)); 505 | } 506 | function not(callable $fn, ...$args) : bool 507 | { 508 | return !$fn(...$args); 509 | } 510 | function isInstance($class, $item) 511 | { 512 | return $item instanceof $class; 513 | } 514 | function isNull($val) 515 | { 516 | return \is_null($val); 517 | } 518 | function nullable(callable $fn, $value) 519 | { 520 | return $value === null ? $value : $fn($value); 521 | } 522 | function partition(callable $partition, iterable $iter, int $numParts = 2) : array 523 | { 524 | $parts = \array_fill(0, $numParts, []); 525 | foreach ($iter as $val) { 526 | $index = (int) $partition($val); 527 | $parts[$index][] = $val; 528 | } 529 | return $parts; 530 | } 531 | function map(callable $predicate, iterable $iter) : iterable 532 | { 533 | foreach ($iter as $key => $value) { 534 | (yield $key => $predicate($value)); 535 | } 536 | } 537 | function mapKeys(callable $predicate, iterable $iter) : iterable 538 | { 539 | foreach ($iter as $key => $value) { 540 | (yield $predicate($key) => $value); 541 | } 542 | } 543 | function mapKeyValue(callable $fn, iterable $iter) : iterable 544 | { 545 | foreach ($iter as $key => $value) { 546 | [$key, $value] = $fn([$key, $value]); 547 | (yield $key => $value); 548 | } 549 | } 550 | function mapOn(array $maps, iterable $iter) : iterable 551 | { 552 | foreach ($iter as $key => $value) { 553 | if (isset($maps[$key])) { 554 | (yield $key => $maps[$key]($value)); 555 | } else { 556 | (yield $key => $value); 557 | } 558 | } 559 | } 560 | function mapAccum(callable $fn, iterable $iter, $acc = null) 561 | { 562 | $data = []; 563 | foreach ($iter as $key => $value) { 564 | [$acc, $value] = $fn($acc, $value); 565 | $data[] = $value; 566 | } 567 | return [$acc, $data]; 568 | } 569 | function withState(callable $fn, $initialState = null) 570 | { 571 | $state = $initialState; 572 | return function (...$args) use($fn, &$state) { 573 | [$state, $res] = $fn($state, ...$args); 574 | return $res; 575 | }; 576 | } 577 | function arrayReindex(callable $fn, iterable $iter) : array 578 | { 579 | $res = []; 580 | foreach ($iter as $key => $value) { 581 | $res[$fn($value)] = $value; 582 | } 583 | return $res; 584 | } 585 | function reindex(callable $fn, iterable $iter) : iterable 586 | { 587 | foreach ($iter as $key => $value) { 588 | (yield $fn($value) => $value); 589 | } 590 | } 591 | function reduce(callable $reduce, iterable $iter, $acc = null) 592 | { 593 | foreach ($iter as $key => $value) { 594 | $acc = $reduce($acc, $value); 595 | } 596 | return $acc; 597 | } 598 | function reduceKeyValue(callable $reduce, iterable $iter, $acc = null) 599 | { 600 | foreach ($iter as $key => $value) { 601 | $acc = $reduce($acc, [$key, $value]); 602 | } 603 | return $acc; 604 | } 605 | function filter(callable $predicate, iterable $iter) : iterable 606 | { 607 | foreach ($iter as $key => $value) { 608 | if ($predicate($value)) { 609 | (yield $key => $value); 610 | } 611 | } 612 | } 613 | function filterKeys(callable $predicate, iterable $iter) : iterable 614 | { 615 | foreach ($iter as $key => $value) { 616 | if ($predicate($key)) { 617 | (yield $key => $value); 618 | } 619 | } 620 | } 621 | function values(iterable $iter) : iterable 622 | { 623 | foreach ($iter as $v) { 624 | (yield $v); 625 | } 626 | } 627 | function keys(iterable $iter) : iterable 628 | { 629 | foreach ($iter as $k => $v) { 630 | (yield $k); 631 | } 632 | } 633 | function flip(iterable $iter) : iterable 634 | { 635 | foreach ($iter as $k => $v) { 636 | (yield $v => $k); 637 | } 638 | } 639 | function curry(callable $fn, int $num = 1) 640 | { 641 | if ($num == 0) { 642 | return $fn; 643 | } 644 | return function ($arg1) use($fn, $num) { 645 | return curry(function (...$args) use($fn, $arg1) { 646 | return $fn($arg1, ...$args); 647 | }, $num - 1); 648 | }; 649 | } 650 | function placeholder() 651 | { 652 | static $v; 653 | $v = $v ?: new class 654 | { 655 | }; 656 | return $v; 657 | } 658 | function _() 659 | { 660 | return placeholder(); 661 | } 662 | function partial(callable $fn, ...$appliedArgs) 663 | { 664 | return function (...$args) use($fn, $appliedArgs) { 665 | list($appliedArgs, $args) = \array_reduce($appliedArgs, function ($acc, $arg) { 666 | list($appliedArgs, $args) = $acc; 667 | if ($arg === \Krak\Fun\placeholder()) { 668 | $arg = array_shift($args); 669 | } 670 | $appliedArgs[] = $arg; 671 | return [$appliedArgs, $args]; 672 | }, [[], $args]); 673 | return $fn(...$appliedArgs, ...$args); 674 | }; 675 | } 676 | function autoCurry(array $args, $numArgs, callable $fn) 677 | { 678 | if (\count($args) >= $numArgs) { 679 | return $fn(...$args); 680 | } 681 | if (\count($args) == $numArgs - 1) { 682 | return \Krak\Fun\partial($fn, ...$args); 683 | } 684 | if (\count($args) == 0) { 685 | return \Krak\Fun\curry($fn, $numArgs - 1); 686 | } 687 | return \Krak\Fun\curry(\Krak\Fun\partial($fn, ...$args), $numArgs - 1 - \count($args)); 688 | } 689 | function toArray(iterable $iter) : array 690 | { 691 | $data = []; 692 | foreach ($iter as $key => $val) { 693 | $data[] = $val; 694 | } 695 | return $data; 696 | } 697 | function toArrayWithKeys(iterable $iter) : array 698 | { 699 | $data = []; 700 | foreach ($iter as $key => $val) { 701 | $data[$key] = $val; 702 | } 703 | return $data; 704 | } 705 | function id($v) 706 | { 707 | return $v; 708 | } 709 | // UTILITY 710 | function tap(callable $tap, $value) 711 | { 712 | $tap($value); 713 | return $value; 714 | } 715 | function throwIf(callable $throw, callable $if, $value) 716 | { 717 | if ($if($value)) { 718 | throw $throw($value); 719 | } 720 | return $value; 721 | } 722 | function differenceWith(callable $cmp, iterable $a, iterable $b) 723 | { 724 | return \Krak\Fun\filter(function ($aItem) use($cmp, $b) { 725 | return \Krak\Fun\indexOf(\Krak\Fun\partial($cmp, $aItem), $b) === null; 726 | }, $a); 727 | } 728 | function sortFromArray(callable $fn, array $orderedElements, iterable $iter) : array 729 | { 730 | $data = []; 731 | $flippedElements = \array_flip($orderedElements); 732 | foreach ($iter as $value) { 733 | $key = $fn($value); 734 | if (!\array_key_exists($key, $flippedElements)) { 735 | throw new \LogicException('Cannot sort element key ' . $key . ' because it does not exist in the ordered elements.'); 736 | } 737 | $data[$flippedElements[$key]] = $value; 738 | } 739 | ksort($data); 740 | return $data; 741 | } 742 | function retry(callable $fn, $shouldRetry = null) 743 | { 744 | if (\is_null($shouldRetry)) { 745 | $shouldRetry = function ($numRetries, \Throwable $t = null) { 746 | return true; 747 | }; 748 | } 749 | if (\is_int($shouldRetry)) { 750 | $maxTries = $shouldRetry; 751 | if ($maxTries < 0) { 752 | throw new \LogicException("maxTries must be greater than or equal to 0"); 753 | } 754 | $shouldRetry = function ($numRetries, \Throwable $t = null) use($maxTries) { 755 | return $numRetries <= $maxTries; 756 | }; 757 | } 758 | if (!\is_callable($shouldRetry)) { 759 | throw new \InvalidArgumentException('shouldRetry must be an int or callable'); 760 | } 761 | $numRetries = 0; 762 | do { 763 | try { 764 | return $fn($numRetries); 765 | } catch (\Throwable $t) { 766 | } 767 | $numRetries += 1; 768 | } while ($shouldRetry($numRetries, $t)); 769 | throw $t; 770 | } 771 | function pipe(callable ...$fns) 772 | { 773 | return function (...$args) use($fns) { 774 | $isFirstPass = true; 775 | foreach ($fns as $fn) { 776 | if ($isFirstPass) { 777 | $arg = $fn(...$args); 778 | $isFirstPass = false; 779 | } else { 780 | $arg = $fn($arg); 781 | } 782 | } 783 | return $arg; 784 | }; 785 | } 786 | function compose(callable ...$fns) 787 | { 788 | return \Krak\Fun\pipe(...\array_reverse($fns)); 789 | } 790 | function stack(array $funcs, callable $last = null, callable $resolve = null) 791 | { 792 | return function (...$args) use($funcs, $resolve, $last) { 793 | return \Krak\Fun\reduce(function ($acc, $func) use($resolve) { 794 | return function (...$args) use($acc, $func, $resolve) { 795 | $args[] = $acc; 796 | $func = $resolve ? $resolve($func) : $func; 797 | return $func(...$args); 798 | }; 799 | }, $funcs, $last ?: function () { 800 | throw new \LogicException('No stack handler was able to capture this request'); 801 | }); 802 | }; 803 | } 804 | function each(callable $handle, iterable $iter) 805 | { 806 | foreach ($iter as $v) { 807 | $handle($v); 808 | } 809 | } 810 | /** @deprecated */ 811 | function onEach(callable $handle, iterable $iter) 812 | { 813 | foreach ($iter as $v) { 814 | $handle($v); 815 | } 816 | } 817 | function iter($iter) : \Iterator 818 | { 819 | if (\is_array($iter)) { 820 | return new \ArrayIterator($iter); 821 | } else { 822 | if ($iter instanceof \Iterator) { 823 | return $iter; 824 | } else { 825 | if (\is_object($iter) || \is_iterable($iter)) { 826 | return (function ($iter) { 827 | foreach ($iter as $key => $value) { 828 | (yield $key => $value); 829 | } 830 | })($iter); 831 | } else { 832 | if (\is_string($iter)) { 833 | return (function ($s) { 834 | for ($i = 0; $i < \strlen($s); $i++) { 835 | (yield $i => $s[$i]); 836 | } 837 | })($iter); 838 | } 839 | } 840 | } 841 | } 842 | throw new \LogicException('Iter could not be converted into an iterable.'); 843 | } -------------------------------------------------------------------------------- /src/fn.php: -------------------------------------------------------------------------------- 1 | {$name}(...$optionalArgs); 9 | } 10 | function prop(string $key, /* object */ $data, $else = null) { 11 | return \property_exists($data, $key) ? $data->{$key} : $else; 12 | } 13 | function index(/* string|int */ $key, /* array|ArrayAccess */ $data, $else = null) { 14 | if (\Krak\Fun\isInstance(\ArrayAccess::class, $data)) { 15 | $exists = $data->offsetExists($key); 16 | } 17 | else { 18 | $exists = \array_key_exists($key, $data); 19 | } 20 | return $exists ? $data[$key] : $else; 21 | } 22 | 23 | function setProp(string $key, $value, /* object */ $data) { 24 | $data->{$key} = $value; 25 | return $data; 26 | } 27 | 28 | function setIndex(/* string|int */ $key, $value, array $data) { 29 | $data[$key] = $value; 30 | return $data; 31 | } 32 | 33 | function setIndexIn(array $keys, $value, array $data) { 34 | return \Krak\Fun\updateIndexIn($keys, function() use ($value) { 35 | return $value; 36 | }, $data); 37 | } 38 | 39 | function propIn(array $props, /* object */ $obj, $else = null) { 40 | foreach ($props as $prop) { 41 | if (!\is_object($obj) || !\property_exists($obj, $prop)) { 42 | return $else; 43 | } 44 | 45 | $obj = $obj->{$prop}; 46 | } 47 | 48 | return $obj; 49 | } 50 | 51 | function indexIn(array $keys, array $data, $else = null) { 52 | foreach ($keys as $part) { 53 | if (!\is_array($data) || !\array_key_exists($part, $data)) { 54 | return $else; 55 | } 56 | 57 | $data = $data[$part]; 58 | } 59 | 60 | return $data; 61 | } 62 | 63 | function hasIndexIn(array $keys, array $data): bool { 64 | foreach ($keys as $key) { 65 | if (!\is_array($data) || !\array_key_exists($key, $data)) { 66 | return false; 67 | } 68 | $data = $data[$key]; 69 | } 70 | 71 | return true; 72 | } 73 | 74 | function updateIndexIn(array $keys, callable $update, array $data): array { 75 | $curData = &$data; 76 | foreach (\array_slice($keys, 0, -1) as $key) { 77 | if (!\array_key_exists($key, $curData)) { 78 | throw new \RuntimeException('Could not updateIn because the keys ' . \implode(' -> ', $keys) . ' could not be found.'); 79 | } 80 | $curData = &$curData[$key]; 81 | } 82 | 83 | $lastKey = $keys[count($keys) - 1]; 84 | $curData[$lastKey] = $update($curData[$lastKey] ?? null); 85 | 86 | return $data; 87 | } 88 | 89 | // UTILITY 90 | 91 | function assign($obj, iterable $iter) { 92 | foreach ($iter as $key => $value) { 93 | $obj->{$key} = $value; 94 | } 95 | return $obj; 96 | } 97 | 98 | function join(string $sep, iterable $iter) { 99 | return \Krak\Fun\reduce(function($acc, $v) use ($sep) { 100 | return $acc ? $acc . $sep . $v : $v; 101 | }, $iter, ""); 102 | } 103 | 104 | function construct($className, ...$args) { 105 | return new $className(...$args); 106 | } 107 | 108 | function spread(callable $fn, array $data) { 109 | return $fn(...$data); 110 | } 111 | 112 | function dd($value, callable $dump = null, callable $die = null) { 113 | $dump = $dump ?: (function_exists('dump') ? 'dump' : 'var_dump'); 114 | $dump($value); 115 | ($die ?? function() { die; })(); 116 | } 117 | 118 | // SLICING 119 | 120 | function takeWhile(callable $predicate, iterable $iter): iterable { 121 | foreach ($iter as $k => $v) { 122 | if ($predicate($v)) { 123 | yield $k => $v; 124 | } else { 125 | return; 126 | } 127 | } 128 | } 129 | 130 | function dropWhile(callable $predicate, iterable $iter): iterable { 131 | $stillDropping = true; 132 | foreach ($iter as $k => $v) { 133 | if ($stillDropping && $predicate($v)) { 134 | continue; 135 | } else if ($stillDropping) { 136 | $stillDropping = false; 137 | } 138 | 139 | yield $k => $v; 140 | } 141 | } 142 | 143 | function take(int $num, iterable $iter): iterable { 144 | return \Krak\Fun\slice(0, $iter, $num); 145 | } 146 | 147 | function drop(int $num, iterable $iter): iterable { 148 | return \Krak\Fun\slice($num, $iter); 149 | } 150 | 151 | function slice(int $start, iterable $iter, $length = INF): iterable { 152 | assert($start >= 0); 153 | 154 | $i = 0; 155 | $end = $start + $length - 1; 156 | foreach ($iter as $k => $v) { 157 | if ($start <= $i && $i <= $end) { 158 | yield $k => $v; 159 | } 160 | 161 | $i += 1; 162 | if ($i > $end) { 163 | return; 164 | } 165 | } 166 | } 167 | 168 | function head(iterable $iter) { 169 | foreach ($iter as $v) { 170 | return $v; 171 | } 172 | } 173 | 174 | function chunk(int $size, iterable $iter): iterable { 175 | assert($size > 0); 176 | 177 | $chunk = []; 178 | foreach ($iter as $v) { 179 | $chunk[] = $v; 180 | if (\count($chunk) == $size) { 181 | yield $chunk; 182 | $chunk = []; 183 | } 184 | } 185 | 186 | if ($chunk) { 187 | yield $chunk; 188 | } 189 | } 190 | 191 | function chunkBy(callable $fn, iterable $iter, ?int $maxSize = null): iterable { 192 | assert($maxSize === null || $maxSize > 0); 193 | $group = []; 194 | $groupKey = null; 195 | foreach ($iter as $v) { 196 | $curGroupKey = $fn($v); 197 | $shouldYieldGroup = ($groupKey !== null && $groupKey !== $curGroupKey) 198 | || ($maxSize !== null && \count($group) >= $maxSize); 199 | if ($shouldYieldGroup) { 200 | yield $group; 201 | $group = []; 202 | } 203 | 204 | $group[] = $v; 205 | $groupKey = $curGroupKey; 206 | } 207 | 208 | if (\count($group)) { 209 | yield $group; 210 | } 211 | } 212 | 213 | function groupBy(callable $fn, iterable $iter, ?int $maxSize = null): iterable { 214 | return \Krak\Fun\chunkBy($fn, $iter, $maxSize); 215 | } 216 | 217 | 218 | // GENERATORS 219 | 220 | function range($start, $end, $step = null) { 221 | if ($start == $end) { 222 | yield $start; 223 | } else if ($start < $end) { 224 | $step = $step ?: 1; 225 | if ($step <= 0) { 226 | throw new \InvalidArgumentException('Step must be greater than 0.'); 227 | } 228 | for ($i = $start; $i <= $end; $i += $step) { 229 | yield $i; 230 | } 231 | } else { 232 | $step = $step ?: -1; 233 | if ($step >= 0) { 234 | throw new \InvalidArgumentException('Step must be less than 0.'); 235 | } 236 | for ($i = $start; $i >= $end; $i += $step) { 237 | yield $i; 238 | } 239 | } 240 | } 241 | 242 | // OPERATORS 243 | 244 | function op(string $op, $b, $a) { 245 | switch ($op) { 246 | case '==': 247 | case 'eq': 248 | return $a == $b; 249 | case '!=': 250 | case 'neq': 251 | return $a != $b; 252 | case '===': 253 | return $a === $b; 254 | case '!==': 255 | return $a !== $b; 256 | case '>': 257 | case 'gt': 258 | return $a > $b; 259 | case '>=': 260 | case 'gte': 261 | return $a >= $b; 262 | case '<': 263 | case 'lt': 264 | return $a < $b; 265 | case '<=': 266 | case 'lte': 267 | return $a <= $b; 268 | case '+': 269 | return $a + $b; 270 | case '-': 271 | return $a - $b; 272 | case '*': 273 | return $a * $b; 274 | case '**': 275 | return $a ** $b; 276 | case '/': 277 | return $a / $b; 278 | case '%': 279 | return $a % $b; 280 | case '.': 281 | return $a . $b; 282 | default: 283 | throw new \LogicException('Invalid operator '.$op); 284 | } 285 | } 286 | 287 | function andf(callable ...$fns) { 288 | return function($el) use ($fns) { 289 | foreach ($fns as $fn) { 290 | if (!$fn($el)) { 291 | return false; 292 | } 293 | } 294 | return true; 295 | }; 296 | } 297 | function orf(callable ...$fns) { 298 | return function($el) use ($fns) { 299 | foreach ($fns as $fn) { 300 | if ($fn($el)) { 301 | return true; 302 | } 303 | } 304 | return false; 305 | }; 306 | } 307 | 308 | function chain(iterable ...$iters) { 309 | foreach ($iters as $iter) { 310 | foreach ($iter as $k => $v) { 311 | yield $k => $v; 312 | } 313 | } 314 | } 315 | 316 | function zip(iterable ...$iters): \Iterator { 317 | if (count($iters) == 0) { 318 | return; 319 | } 320 | 321 | $iters = \array_map(iter::class, $iters); 322 | 323 | while (true) { 324 | $tup = []; 325 | foreach ($iters as $iter) { 326 | if (!$iter->valid()) { 327 | return; 328 | } 329 | $tup[] = $iter->current(); 330 | $iter->next(); 331 | } 332 | yield $tup; 333 | } 334 | } 335 | 336 | 337 | function flatMap(callable $map, iterable $iter): iterable { 338 | foreach ($iter as $k => $v) { 339 | foreach ($map($v) as $k => $v) { 340 | yield $k => $v; 341 | } 342 | } 343 | } 344 | 345 | function flatten(iterable $iter, $levels = INF): iterable { 346 | if ($levels == 0) { 347 | yield from $iter; 348 | } else if ($levels == 1) { 349 | foreach ($iter as $k => $v) { 350 | if (\is_iterable($v)) { 351 | foreach ($v as $k1 => $v1) { 352 | yield $k1 => $v1; 353 | } 354 | } else { 355 | yield $k => $v; 356 | } 357 | } 358 | } else { 359 | foreach ($iter as $k => $v) { 360 | if (\is_iterable($v)) { 361 | foreach (flatten($v, $levels - 1) as $k1 => $v1) { 362 | yield $k1 => $v1; 363 | } 364 | } else { 365 | yield $k => $v; 366 | } 367 | } 368 | } 369 | } 370 | 371 | 372 | function product(iterable ...$iters): iterable { 373 | if (count($iters) === 0) { 374 | yield from []; 375 | return; 376 | } 377 | if (count($iters) === 1) { 378 | yield from \Krak\Fun\map(function($v) { return [$v]; }, $iters[0]); 379 | return; 380 | } 381 | 382 | foreach ($iters[0] as $value) { 383 | yield from \Krak\Fun\map(function(array $tup) use ($value) { 384 | array_unshift($tup, $value); 385 | return $tup; 386 | }, \Krak\Fun\product(...\array_slice($iters, 1))); 387 | } 388 | } 389 | 390 | 391 | function when(callable $if, callable $then, $value) { 392 | return $if($value) ? $then($value) : $value; 393 | } 394 | 395 | function toPairs(iterable $iter): iterable { 396 | foreach ($iter as $key => $val) { 397 | yield [$key, $val]; 398 | } 399 | } 400 | function fromPairs(iterable $iter): iterable { 401 | foreach ($iter as list($key, $val)) { 402 | yield $key => $val; 403 | } 404 | } 405 | 406 | function pick(iterable $fields, array $data): array { 407 | $pickedData = []; 408 | foreach ($fields as $field) { 409 | $pickedData[$field] = $data[$field] ?? null; 410 | } 411 | return $pickedData; 412 | } 413 | function pickBy(callable $pick, array $data): array { 414 | $pickedData = []; 415 | foreach ($data as $key => $value) { 416 | if ($pick([$key, $value])) { 417 | $pickedData[$key] = $value; 418 | } 419 | } 420 | return $pickedData; 421 | } 422 | 423 | function within(array $fields, iterable $iter): \Iterator { 424 | return \Krak\Fun\filterKeys(\Krak\Fun\Curried\inArray($fields), $iter); 425 | } 426 | function without(array $fields, iterable $iter): \Iterator { 427 | return \Krak\Fun\filterKeys(\Krak\Fun\Curried\not(\Krak\Fun\Curried\inArray($fields)), $iter); 428 | } 429 | 430 | function compact(iterable $iter): iterable { 431 | foreach ($iter as $key => $val) { 432 | if ($val !== null) { 433 | yield $key => $val; 434 | } 435 | } 436 | } 437 | 438 | function arrayCompact(iterable $iter): array { 439 | $vals = []; 440 | foreach ($iter as $key => $val) { 441 | if ($val !== null) { 442 | $vals[$key] = $val; 443 | } 444 | } 445 | return $vals; 446 | } 447 | 448 | function pad(int $size, iterable $iter, $padValue = null): iterable { 449 | $i = 0; 450 | foreach ($iter as $key => $value) { 451 | yield $value; 452 | $i += 1; 453 | } 454 | 455 | if ($i >= $size) { 456 | return; 457 | } 458 | 459 | foreach (\Krak\Fun\range($i, $size - 1) as $index) { 460 | yield $padValue; 461 | } 462 | } 463 | 464 | 465 | // ALIASES 466 | 467 | function inArray(array $set, $item): bool { 468 | return \in_array($item, $set); 469 | } 470 | 471 | function arrayMap(callable $fn, iterable $data): array { 472 | return \array_map($fn, \is_array($data) ? $data : \Krak\Fun\toArray($data)); 473 | } 474 | 475 | function arrayFilter(callable $fn, iterable $data): array { 476 | return \array_filter(\is_array($data) ? $data : \Krak\Fun\toArray($data), $fn); 477 | } 478 | 479 | function arrayWrap($value) { 480 | return is_array($value) && array_is_list($value) ? $value : [$value]; 481 | } 482 | 483 | function all(callable $predicate, iterable $iter): bool { 484 | foreach ($iter as $key => $value) { 485 | if (!$predicate($value)) { 486 | return false; 487 | } 488 | } 489 | 490 | return true; 491 | } 492 | function any(callable $predicate, iterable $iter): bool { 493 | foreach ($iter as $key => $value) { 494 | if ($predicate($value)) { 495 | return true; 496 | } 497 | } 498 | 499 | return false; 500 | } 501 | function search(callable $predicate, iterable $iter) { 502 | foreach ($iter as $value) { 503 | if ($predicate($value)) { 504 | return $value; 505 | } 506 | } 507 | } 508 | function indexOf(callable $predicate, iterable $iter) { 509 | foreach ($iter as $key => $value) { 510 | if ($predicate($value)) { 511 | return $key; 512 | } 513 | } 514 | } 515 | 516 | function trans(callable $trans, callable $fn, $data) { 517 | return $fn($trans($data)); 518 | } 519 | function not(callable $fn, ...$args): bool { 520 | return !$fn(...$args); 521 | } 522 | function isInstance($class, $item) { 523 | return $item instanceof $class; 524 | } 525 | 526 | function isNull($val) { 527 | return \is_null($val); 528 | } 529 | function nullable(callable $fn, $value) { 530 | return $value === null ? $value : $fn($value); 531 | } 532 | 533 | function partition(callable $partition, iterable $iter, int $numParts = 2): array { 534 | $parts = \array_fill(0, $numParts, []); 535 | foreach ($iter as $val) { 536 | $index = (int) $partition($val); 537 | $parts[$index][] = $val; 538 | } 539 | 540 | return $parts; 541 | } 542 | 543 | function map(callable $predicate, iterable $iter): iterable { 544 | foreach ($iter as $key => $value) { 545 | yield $key => $predicate($value); 546 | } 547 | } 548 | 549 | function mapKeys(callable $predicate, iterable $iter): iterable { 550 | foreach ($iter as $key => $value) { 551 | yield $predicate($key) => $value; 552 | } 553 | } 554 | 555 | function mapKeyValue(callable $fn , iterable $iter): iterable { 556 | foreach ($iter as $key => $value) { 557 | [$key, $value] = $fn([$key, $value]); 558 | yield $key => $value; 559 | } 560 | } 561 | 562 | function mapOn(array $maps, iterable $iter): iterable { 563 | foreach ($iter as $key => $value) { 564 | if (isset($maps[$key])) { 565 | yield $key => $maps[$key]($value); 566 | } else { 567 | yield $key => $value; 568 | } 569 | } 570 | } 571 | 572 | function mapAccum(callable $fn, iterable $iter, $acc = null) { 573 | $data = []; 574 | foreach ($iter as $key => $value) { 575 | [$acc, $value] = $fn($acc, $value); 576 | $data[] = $value; 577 | } 578 | 579 | return [$acc, $data]; 580 | } 581 | 582 | function withState(callable $fn, $initialState = null) { 583 | $state = $initialState; 584 | return function(...$args) use ($fn, &$state) { 585 | [$state, $res] = $fn($state, ...$args); 586 | return $res; 587 | }; 588 | } 589 | 590 | function arrayReindex(callable $fn, iterable $iter): array { 591 | $res = []; 592 | foreach ($iter as $key => $value) { 593 | $res[$fn($value)] = $value; 594 | } 595 | return $res; 596 | } 597 | 598 | function reindex(callable $fn, iterable $iter): iterable { 599 | foreach ($iter as $key => $value) { 600 | yield $fn($value) => $value; 601 | } 602 | } 603 | 604 | function reduce(callable $reduce, iterable $iter, $acc = null) { 605 | foreach ($iter as $key => $value) { 606 | $acc = $reduce($acc, $value); 607 | } 608 | return $acc; 609 | } 610 | 611 | function reduceKeyValue(callable $reduce, iterable $iter, $acc = null) { 612 | foreach ($iter as $key => $value) { 613 | $acc = $reduce($acc, [$key, $value]); 614 | } 615 | return $acc; 616 | } 617 | 618 | function filter(callable $predicate, iterable $iter): iterable { 619 | foreach ($iter as $key => $value) { 620 | if ($predicate($value)) { 621 | yield $key => $value; 622 | } 623 | } 624 | } 625 | function filterKeys(callable $predicate, iterable $iter): iterable { 626 | foreach ($iter as $key => $value) { 627 | if ($predicate($key)) { 628 | yield $key => $value; 629 | } 630 | } 631 | } 632 | 633 | function values(iterable $iter): iterable { 634 | foreach ($iter as $v) { 635 | yield $v; 636 | } 637 | } 638 | 639 | function keys(iterable $iter): iterable { 640 | foreach ($iter as $k => $v) { 641 | yield $k; 642 | } 643 | } 644 | 645 | function flip(iterable $iter): iterable { 646 | foreach ($iter as $k => $v) { 647 | yield $v => $k; 648 | } 649 | } 650 | 651 | function curry(callable $fn, int $num = 1) { 652 | if ($num == 0) { 653 | return $fn; 654 | } 655 | 656 | return function($arg1) use ($fn, $num) { 657 | return curry(function(...$args) use ($fn, $arg1) { 658 | return $fn($arg1, ...$args); 659 | }, $num - 1); 660 | }; 661 | } 662 | 663 | function placeholder() { 664 | static $v; 665 | 666 | $v = $v ?: new class {}; 667 | return $v; 668 | } 669 | function _() { 670 | return placeholder(); 671 | } 672 | 673 | function partial(callable $fn, ...$appliedArgs) { 674 | return function(...$args) use ($fn, $appliedArgs) { 675 | list($appliedArgs, $args) = \array_reduce($appliedArgs, function($acc, $arg) { 676 | list($appliedArgs, $args) = $acc; 677 | if ($arg === \Krak\Fun\placeholder()) { 678 | $arg = array_shift($args); 679 | } 680 | 681 | $appliedArgs[] = $arg; 682 | return [$appliedArgs, $args]; 683 | }, [[], $args]); 684 | 685 | return $fn(...$appliedArgs, ...$args); 686 | }; 687 | } 688 | 689 | function autoCurry(array $args, $numArgs, callable $fn) { 690 | if (\count($args) >= $numArgs) { 691 | return $fn(...$args); 692 | } 693 | if (\count($args) == $numArgs - 1) { 694 | return \Krak\Fun\partial($fn, ...$args); 695 | } 696 | if (\count($args) == 0) { 697 | return \Krak\Fun\curry($fn, $numArgs - 1); 698 | } 699 | 700 | return \Krak\Fun\curry( 701 | \Krak\Fun\partial($fn, ...$args), 702 | ($numArgs - 1 - \count($args)) 703 | ); 704 | } 705 | 706 | function toArray(iterable $iter): array { 707 | $data = []; 708 | foreach ($iter as $key => $val) { 709 | $data[] = $val; 710 | } 711 | return $data; 712 | } 713 | 714 | function toArrayWithKeys(iterable $iter): array { 715 | $data = []; 716 | foreach ($iter as $key => $val) { 717 | $data[$key] = $val; 718 | } 719 | return $data; 720 | } 721 | 722 | function id($v) { 723 | return $v; 724 | } 725 | 726 | 727 | // UTILITY 728 | 729 | function tap(callable $tap, $value) { 730 | $tap($value); 731 | return $value; 732 | } 733 | 734 | function throwIf(callable $throw, callable $if, $value) { 735 | if ($if($value)) { 736 | throw $throw($value); 737 | } 738 | 739 | return $value; 740 | } 741 | 742 | function differenceWith(callable $cmp, iterable $a, iterable $b) { 743 | return \Krak\Fun\filter(function($aItem) use ($cmp, $b) { 744 | return \Krak\Fun\indexOf(\Krak\Fun\partial($cmp, $aItem), $b) === null; 745 | }, $a); 746 | } 747 | 748 | function sortFromArray(callable $fn, array $orderedElements, iterable $iter): array { 749 | $data = []; 750 | $flippedElements = \array_flip($orderedElements); 751 | 752 | foreach ($iter as $value) { 753 | $key = $fn($value); 754 | if (!\array_key_exists($key, $flippedElements)) { 755 | throw new \LogicException('Cannot sort element key ' . $key . ' because it does not exist in the ordered elements.'); 756 | } 757 | 758 | $data[$flippedElements[$key]] = $value; 759 | } 760 | 761 | ksort($data); 762 | return $data; 763 | } 764 | 765 | function retry(callable $fn, $shouldRetry = null) { 766 | if (\is_null($shouldRetry)) { 767 | $shouldRetry = function($numRetries, \Throwable $t = null) { return true; }; 768 | } 769 | if (\is_int($shouldRetry)) { 770 | $maxTries = $shouldRetry; 771 | if ($maxTries < 0) { 772 | throw new \LogicException("maxTries must be greater than or equal to 0"); 773 | } 774 | $shouldRetry = function($numRetries, \Throwable $t = null) use ($maxTries) { return $numRetries <= $maxTries; }; 775 | } 776 | if (!\is_callable($shouldRetry)) { 777 | throw new \InvalidArgumentException('shouldRetry must be an int or callable'); 778 | } 779 | 780 | $numRetries = 0; 781 | do { 782 | try { 783 | return $fn($numRetries); 784 | } catch (\Throwable $t) {} 785 | $numRetries += 1; 786 | } while ($shouldRetry($numRetries, $t)); 787 | 788 | throw $t; 789 | } 790 | 791 | function pipe(callable ...$fns) { 792 | return function(...$args) use ($fns) { 793 | $isFirstPass = true; 794 | foreach ($fns as $fn) { 795 | if ($isFirstPass) { 796 | $arg = $fn(...$args); 797 | $isFirstPass = false; 798 | } else { 799 | $arg = $fn($arg); 800 | } 801 | 802 | } 803 | return $arg; 804 | }; 805 | } 806 | 807 | function compose(callable ...$fns) { 808 | return \Krak\Fun\pipe(...\array_reverse($fns)); 809 | } 810 | 811 | function stack(array $funcs, callable $last = null, callable $resolve = null) { 812 | return function(...$args) use ($funcs, $resolve, $last) { 813 | return \Krak\Fun\reduce(function($acc, $func) use ($resolve) { 814 | return function(...$args) use ($acc, $func, $resolve) { 815 | $args[] = $acc; 816 | $func = $resolve ? $resolve($func) : $func; 817 | return $func(...$args); 818 | }; 819 | }, $funcs, $last ?: function() { throw new \LogicException('No stack handler was able to capture this request'); }); 820 | }; 821 | } 822 | 823 | function each(callable $handle, iterable $iter) { 824 | foreach ($iter as $v) { 825 | $handle($v); 826 | } 827 | } 828 | /** @deprecated */ 829 | function onEach(callable $handle, iterable $iter) { 830 | foreach ($iter as $v) { 831 | $handle($v); 832 | } 833 | } 834 | 835 | function iter($iter): \Iterator { 836 | if (\is_array($iter)) { 837 | return new \ArrayIterator($iter); 838 | } else if ($iter instanceof \Iterator) { 839 | return $iter; 840 | } else if (\is_object($iter) || \is_iterable($iter)) { 841 | return (function($iter) { 842 | foreach ($iter as $key => $value) { 843 | yield $key => $value; 844 | } 845 | })($iter); 846 | } else if (\is_string($iter)) { 847 | return (function($s) { 848 | for ($i = 0; $i < \strlen($s); $i++) { 849 | yield $i => $s[$i]; 850 | } 851 | })($iter); 852 | } 853 | 854 | throw new \LogicException('Iter could not be converted into an iterable.'); 855 | } 856 | -------------------------------------------------------------------------------- /src/generate.php: -------------------------------------------------------------------------------- 1 | name . '\\' . $fn->name; 27 | return new Stmt\Const_([ 28 | new Const_($fn->name, BuilderHelpers::normalizeValue($constVal)) 29 | ]); 30 | } 31 | 32 | function curryFunction(Function_ $fn) { 33 | list($params, $optionalParams) = partition(function($param) { 34 | return $param->default 35 | || ($param->variadic && strpos((string) $param->var->name, 'optional') === 0); 36 | }, $fn->params); 37 | 38 | // return all params but the first and reverse them 39 | $revParams = array_merge(array_reverse($params), $optionalParams); 40 | 41 | // if we have only one requried arg, then we allow zero required args in curried implementation 42 | $numParams = count($params) > 1 ? count($params) - 2 : count($params) - 1; 43 | $stmts = array_reduce(range(0, $numParams), function($stmts, $i) use ($revParams) { 44 | $remainingArgsForUse = array_slice($revParams, $i + 1); 45 | $curParam = $revParams[$i]; 46 | return [new Return_(new Closure([ 47 | 'params' => [clone $curParam], 48 | 'uses' => array_map(function($param) { 49 | return new ClosureUse(clone $param->var); 50 | }, $remainingArgsForUse), 51 | 'stmts' => $stmts, 52 | ]))]; 53 | }, $fn->getStmts()); 54 | 55 | return new Function_($fn->name, [ 56 | 'params' => array_merge( 57 | count($params) > 1 ? [$params[0]] : [], 58 | $optionalParams 59 | ), 60 | 'stmts' => $stmts, 61 | ]); 62 | } 63 | 64 | function isCurryable($func) { 65 | if (in_array((string) $func->name, ['curry', 'autoCurry'])) { 66 | return false; 67 | } 68 | $numReqParams = count(toArray(filter(function($param) { 69 | return $param->default === null; 70 | }, $func->params))); 71 | return $numReqParams > 1 || ($numReqParams == 1 && count($func->params) > 1); 72 | } 73 | -------------------------------------------------------------------------------- /test/api.spec.php: -------------------------------------------------------------------------------- 1 | equal('2,4,6,8'); 14 | }); 15 | it('can utilize the pre-1.0 generated api helpers', function() { 16 | $res = _f\compose( 17 | _c\join(','), 18 | _f\toArray, 19 | _c\map(_c\op('*')(2)) 20 | )([1,2,3,4]); 21 | expect($res)->equal('2,4,6,8'); 22 | }); 23 | it('can run the home page example', function() { 24 | $res = f\compose( 25 | c\toArray, 26 | c\map(function($tup) { 27 | return $tup[0] + $tup[1]; 28 | }), 29 | c\toPairs 30 | )([1,2,3]); 31 | expect($res)->equal([1,3,5]); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/fn.spec.php: -------------------------------------------------------------------------------- 1 | ', 2)))([1,2,3,4]); 16 | expect($res)->equal([9, 12]); 17 | }); 18 | 19 | describe('autoCurry', function() { 20 | it('can call a function if all args are available', function() { 21 | $res = autoCurry([1,2,3], 3, _idArgs::class); 22 | expect($res)->equal([1,2,3]); 23 | }); 24 | it('can partially apply a function if all but one arg is available', function() { 25 | $res = autoCurry([1,2], 3, _idArgs::class)(3); 26 | expect($res)->equal([1,2,3]); 27 | }); 28 | it('can curry a partially applied function if less than n - 1 args are available', function() { 29 | $res = autoCurry([1], 3, _idArgs::class)(2)(3); 30 | expect($res)->equal([1,2,3]); 31 | }); 32 | it('can curry a function if no args are available', function() { 33 | $res = autoCurry([], 3, _idArgs::class)(1)(2)(3); 34 | expect($res)->equal([1,2,3]); 35 | }); 36 | }); 37 | describe('all', function() { 38 | docFn(all::class); 39 | test('Returns true if the predicate returns true on all of the items', function() { 40 | $res = all(function($v) { return $v % 2 == 0; }, [2,4,6]); 41 | expect($res)->equal(true); 42 | }); 43 | test('Returns false if the predicate returns false on any of the items', function() { 44 | $res = all(function($v) { return $v % 2 == 0; }, [1,2,4,6]); 45 | expect($res)->equal(false); 46 | }); 47 | }); 48 | describe('any', function() { 49 | docFn(any::class); 50 | test('Returns true if the predicate returns true on any of the items', function() { 51 | $res = any(function($v) { return $v % 2 == 0; }, [1,3,4,5]); 52 | expect($res)->equal(true); 53 | }); 54 | test('Returns false if the predicate returns false on all of the items', function() { 55 | $res = any(function($v) { return $v % 2 == 0; }, [1,3,5]); 56 | expect($res)->equal(false); 57 | }); 58 | }); 59 | describe('arrayCompact', function() { 60 | docFn(arrayCompact::class); 61 | test('It will remove all nulls from an iterable and return an array', function() { 62 | $res = arrayCompact([1,2,null,null,3]); 63 | expect(\array_values($res))->equal([1,2,3]); 64 | }); 65 | docOutro('Keep in mind that the keys will be preserved when using arrayCompact, so make sure to use array_values if you want to ignore keys.'); 66 | }); 67 | describe('arrayFilter', function() { 68 | docFn(arrayFilter::class); 69 | test('Alias of array_filter', function() { 70 | $res = arrayFilter(partial(op, '<', 2), [1,2,3]); 71 | expect($res)->equal([1]); 72 | }); 73 | test('Filters iterables as well as arrays', function() { 74 | $res = arrayFilter(partial(op, '<', 2), range(1, 3)); 75 | expect($res)->equal([1]); 76 | }); 77 | }); 78 | describe('arrayMap', function() { 79 | docFn(arrayMap::class); 80 | test('Alias of array_map', function() { 81 | $res = arrayMap(partial(op, '*', 2), [1,2,3]); 82 | expect($res)->equal([2,4,6]); 83 | }); 84 | test('Maps iterables as well as arrays', function() { 85 | $res = arrayMap(partial(op, '*', 2), range(1, 3)); 86 | expect($res)->equal([2,4,6]); 87 | }); 88 | }); 89 | describe('arrayReindex', function() { 90 | docFn(arrayReindex::class); 91 | test('Re-indexes a collection via a callable into an associative array', function() { 92 | $res = arrayReindex(function($v) { 93 | return $v['id']; 94 | }, [['id' => 2], ['id' => 3], ['id' => 1]]); 95 | 96 | expect($res)->equal([ 97 | 2 => ['id' => 2], 98 | 3 => ['id' => 3], 99 | 1 => ['id' => 1], 100 | ]); 101 | }); 102 | }); 103 | describe('arrayWrap', function() { 104 | docFn(arrayWrap::class); 105 | test('Wraps any non list array into an array', function() { 106 | $results = arrayMap(arrayWrap, [ 107 | 1, 108 | 'abc', 109 | ['a' => 1], 110 | ]); 111 | 112 | expect($results)->equal([ 113 | [1], 114 | ['abc'], 115 | [['a' => 1]], 116 | ]); 117 | }); 118 | test('List based arrays are left as is', function() { 119 | $results = arrayMap(arrayWrap, [ 120 | [], 121 | [1,2,3], 122 | ]); 123 | expect($results)->equal([ 124 | [], 125 | [1,2,3], 126 | ]); 127 | }); 128 | docOutro('Note: `array_is_list` which requires php 8.1 or symfony/polyfill-php81'); 129 | }); 130 | describe('assign', function() { 131 | docFn(assign::class); 132 | test('Assigns iterable keys and values to an object', function() { 133 | $obj = new \StdClass(); 134 | $obj = assign($obj, ['a' => 1, 'b' => 2]); 135 | expect($obj->a)->equal(1); 136 | expect($obj->b)->equal(2); 137 | }); 138 | }); 139 | 140 | describe('chain', function() { 141 | docFn(chain::class); 142 | test('Chains iterables together into one iterable', function() { 143 | $res = chain([1], range(2, 3)); 144 | expect(toArray($res))->equal([1,2,3]); 145 | }); 146 | }); 147 | describe('chunk', function() { 148 | docFn(chunk::class); 149 | test('Chunks an iterable into equal sized chunks.', function() { 150 | $res = chunk(2, [1,2,3,4]); 151 | expect(toArray($res))->equal([[1,2], [3,4]]); 152 | }); 153 | test('If there is any remainder, it is yielded as is', function() { 154 | $res = chunk(3, [1,2,3,4]); 155 | expect(toArray($res))->equal([[1,2,3], [4]]); 156 | }); 157 | }); 158 | describe('chunkBy', function() { 159 | docFn(chunkBy::class); 160 | test('Chunks items together off of the result from the callable', function() { 161 | $items = ['aa', 'ab', 'ac', 'ba', 'bb', 'bc', 'ca', 'cb', 'cc']; 162 | $chunks = chunkBy(function(string $item) { 163 | return $item[0]; // return first char 164 | }, $items); 165 | expect(toArray($chunks))->equal([ 166 | ['aa', 'ab', 'ac'], 167 | ['ba', 'bb', 'bc'], 168 | ['ca', 'cb', 'cc'] 169 | ]); 170 | }); 171 | test('Allows a maxSize to prevent chunks from exceeding a limit', function() { 172 | $items = ['aa', 'ab', 'ac', 'ba', 'bb', 'bc', 'ca', 'cb', 'cc']; 173 | $chunks = chunkBy(function(string $item) { 174 | return $item[0]; // return first char 175 | }, $items, 2); 176 | expect(toArray($chunks))->equal([ 177 | ['aa', 'ab'], ['ac'], 178 | ['ba', 'bb'], ['bc'], 179 | ['ca', 'cb'], ['cc'] 180 | ]); 181 | }); 182 | }); 183 | describe('compact', function() { 184 | docFn(compact::class); 185 | 186 | test('Removes all null values from an iterable', function() { 187 | $res = compact([1,null,2,3,null,null,4]); 188 | expect(toArray($res))->equal([1,2,3,4]); 189 | }); 190 | }); 191 | describe('compose', function() { 192 | docFn(compose::class); 193 | 194 | test('Composes functions together. compose(f, g)(x) == f(g(x))', function() { 195 | $mul2 = Curried\op('*')(2); 196 | $add3 = Curried\op('+')(3); 197 | 198 | $add3ThenMul2 = compose($mul2, $add3); 199 | $res = $add3ThenMul2(5); 200 | expect($res)->equal(16); 201 | }); 202 | test('Allows an empty initial argument', function() { 203 | $res = compose( 204 | Curried\reduce(function($acc, $v) { return $acc + $v; }, 0), 205 | function() { yield from [1,2,3]; } 206 | )(); 207 | expect($res)->equal(6); 208 | }); 209 | }); 210 | describe('construct', function() { 211 | docFn(construct::class); 212 | test('Constructs (instantiates) a new class with the given arguments', function() { 213 | $res = construct(\ArrayObject::class, [1,2,3]); 214 | expect($res->count())->equal(3); 215 | }); 216 | }); 217 | describe('curry', function() { 218 | docFn(curry::class); 219 | 220 | test('currys the given function $n times', function() { 221 | $res = curry(_idArgs::class, 2)(1)(2)(3); 222 | expect($res)->equal([1,2,3]); 223 | }); 224 | docOutro('Given a function definition: (a, b) -> c. A curried version will look like (a) -> (b) -> c'); 225 | }); 226 | describe('differenceWith', function() { 227 | docFn(differenceWith::class); 228 | test('Takes the difference between two iterables with a given comparator', function() { 229 | $res = differenceWith(partial(op, '==='), [1,2,3,4,5], [2,3,4]); 230 | expect(toArray($res))->equal([1,5]); 231 | }); 232 | }); 233 | describe('dd', function() { 234 | docFn(dd::class); 235 | test('dumps and dies', function() { 236 | $res = null; 237 | $died = false; 238 | $dump = function($v) use (&$res) {$res = $v;}; 239 | $die = function() use (&$died) { $died = true; }; 240 | dd(1, $dump, $die); 241 | expect($res)->equal(1); 242 | expect($died)->equal(true); 243 | }); 244 | }); 245 | describe('drop', function() { 246 | docFn(drop::class); 247 | test('Drops the first num items from an iterable', function() { 248 | $res = drop(2, range(0, 3)); 249 | expect(toArray($res))->equal([2, 3]); 250 | }); 251 | }); 252 | describe('dropWhile', function() { 253 | docFn(dropWhile::class); 254 | test('Drops elements from the iterable while the predicate returns true', function() { 255 | $res = dropWhile(Curried\op('>')(0), [2, 1, 0, 1, 2]); 256 | expect(toArray($res))->equal([0, 1, 2]); 257 | }); 258 | }); 259 | describe('each', function() { 260 | docFn(each::class); 261 | test('Invokes a callable on each item in an iterable', function() { 262 | $state = [ 263 | (object) ['id' => 1], 264 | (object) ['id' => 2], 265 | ]; 266 | each(function($item) { 267 | $item->id += 1; 268 | }, $state); 269 | 270 | expect([$state[0]->id, $state[1]->id])->equal([2,3]); 271 | }); 272 | docOutro('Normally using php foreach should suffice for iterating over an iterable; however, php variables in foreach loops are not scoped whereas closures are.'); 273 | }); 274 | describe('filter', function() { 275 | docFn(filter::class); 276 | it('Lazily filters an iterable off of a predicate that should return true or false. If true, keep the data, else remove the data from the iterable', function() { 277 | $values = filter(partial(op, '>', 2), [1,2,3,4]); // keep all items that are greater than 2 278 | expect(toArray($values))->equal([3,4]); 279 | }); 280 | }); 281 | describe('filterKeys', function() { 282 | docFn(filterKeys::class); 283 | test('Filters an iterable off of the keys', function() { 284 | $res = filterKeys(Curried\inArray(['a', 'b']), [ 285 | 'a' => 1, 286 | 'b' => 2, 287 | 'c' => 3, 288 | ]); 289 | expect(toArrayWithKeys($res))->equal(['a' => 1, 'b' => 2]); 290 | }); 291 | }); 292 | describe('flatMap', function() { 293 | docFn(flatMap::class); 294 | 295 | test('Maps and then flattens an iterable', function() { 296 | $res = flatMap(function($v) { 297 | return [-$v, $v]; 298 | }, range(1, 3)); 299 | 300 | expect(toArray($res))->equal([-1, 1, -2, 2, -3, 3]); 301 | }); 302 | 303 | docOutro('flatMap is perfect for when you want to map an iterable and also add elements to the resulting iterable.'); 304 | }); 305 | describe('flatten', function() { 306 | docFn(flatten::class); 307 | test('Flattens nested iterables into a flattened set of elements', function() { 308 | $res = flatten([1, [2, [3, [4]]]]); 309 | expect(toArray($res))->equal([1,2,3,4]); 310 | }); 311 | test('Can flatten a specific number of levels', function() { 312 | $res = flatten([1,[2, [3]]], 1); 313 | expect(toArray($res))->equal([1, 2, [3]]); 314 | }); 315 | test('Flattening zero levels does nothing', function() { 316 | $res = flatten([1, [2]], 0); 317 | expect(toArray($res))->equal([1,[2]]); 318 | }); 319 | }); 320 | describe('flip', function() { 321 | docFn(flip::class); 322 | test('Flips the keys => values of an iterable to values => keys', function() { 323 | $res = flip(['a' => 0, 'b' => 1]); 324 | expect(toArray($res))->equal(['a', 'b']); 325 | }); 326 | }); 327 | describe('fromPairs', function() { 328 | docFn(fromPairs::class); 329 | test('Converts an iterable of tuples [$key, $value] into an associative iterable', function() { 330 | $res = fromPairs([ 331 | ['a', 1], 332 | ['b', 2] 333 | ]); 334 | expect(toArrayWithKeys($res))->equal([ 335 | 'a' => 1, 336 | 'b' => 2 337 | ]); 338 | }); 339 | }); 340 | describe('groupBy', function() { 341 | docFn(groupBy::class); 342 | docIntro('Alias of chunkBy'); 343 | test('Groups items together off of the result from the callable', function() { 344 | $items = ['aa', 'ab', 'ac', 'ba', 'bb', 'bc', 'ca', 'cb', 'cc']; 345 | $groupedItems = groupBy(function(string $item) { 346 | return $item[0]; // return first char 347 | }, $items); 348 | expect(toArray($groupedItems))->equal([ 349 | ['aa', 'ab', 'ac'], 350 | ['ba', 'bb', 'bc'], 351 | ['ca', 'cb', 'cc'] 352 | ]); 353 | }); 354 | test('Allows a maxSize to prevent groups from exceeding a limit', function() { 355 | $items = ['aa', 'ab', 'ac', 'ba', 'bb', 'bc', 'ca', 'cb', 'cc']; 356 | $groupedItems = groupBy(function(string $item) { 357 | return $item[0]; // return first char 358 | }, $items, 2); 359 | expect(toArray($groupedItems))->equal([ 360 | ['aa', 'ab'], ['ac'], 361 | ['ba', 'bb'], ['bc'], 362 | ['ca', 'cb'], ['cc'] 363 | ]); 364 | }); 365 | }); 366 | describe('hasIndexIn', function() { 367 | docFn(hasIndexIn::class); 368 | 369 | test('Checks if a nested index exists in the given data', function() { 370 | $res = hasIndexIn(['a', 'b', 'c'], [ 371 | 'a' => ['b' => ['c' => null]] 372 | ]); 373 | 374 | expect($res)->equal(true); 375 | }); 376 | test('Returns false if any of the indexes do not exist in the data', function() { 377 | $res = hasIndexIn(['a', 'b', 'c'], [ 378 | 'a' => ['b' => []] 379 | ]); 380 | 381 | expect($res)->equal(false); 382 | }); 383 | }); 384 | describe('head', function() { 385 | docFn(head::class); 386 | test('Returns the fist element in an iterable', function() { 387 | $res = head([1,2,3]); 388 | expect($res)->equal(1); 389 | }); 390 | test('But returns null if the iterable is empty', function() { 391 | $res = head([]); 392 | expect($res)->equal(null); 393 | }); 394 | }); 395 | describe('inArray', function() { 396 | docFn(inArray::class); 397 | it('Checks if an item is within an array of items', function() { 398 | $res = inArray([1,2,3], 2); 399 | expect($res)->equal(true); 400 | }); 401 | }); 402 | describe('index', function() { 403 | docFn(index::class); 404 | test('Accesses an index in an array', function() { 405 | $res = index('a', ['a' => 1]); 406 | expect($res)->equal(1); 407 | }); 408 | test('If no value exists at the given index, $else will be returned', function() { 409 | $res = index('a', ['b' => 1], 2); 410 | expect($res)->equal(2); 411 | }); 412 | test('Also works with objects that implement ArrayAccess', function() { 413 | class MyClass implements \ArrayAccess 414 | { 415 | private $container = []; 416 | 417 | public function __construct() { 418 | $this->container = ['one' => 1, 'two' => 2]; 419 | } 420 | 421 | public function offsetExists($offset) { 422 | return isset($this->container[$offset]); 423 | } 424 | 425 | public function offsetGet($offset) { 426 | return isset($this->container[$offset]) ? $this->container[$offset] : null; 427 | } 428 | 429 | public function offsetSet($offset, $value) { 430 | /* ... */ 431 | } 432 | 433 | public function offsetUnset($offset) { 434 | /* ... */ 435 | } 436 | } 437 | 438 | $object = new MyClass(); 439 | expect(index('two', $object))->equal(2); 440 | expect(index('three', $object, 'else'))->equal('else'); 441 | }); 442 | }); 443 | describe('indexIn', function() { 444 | docFn(indexIn::class); 445 | test('Accesses a nested index in a deep array structure', function() { 446 | $res = indexIn(['a', 'b'], ['a' => ['b' => 1]]); 447 | expect($res)->equal(1); 448 | }); 449 | test('If any of the indexes do not exist, $else will be returned', function() { 450 | $res = indexIn(['a', 'b'], ['a' => ['c' => 1]], 2); 451 | expect($res)->equal(2); 452 | }); 453 | }); 454 | describe('indexOf', function() { 455 | docFn(indexOf::class); 456 | test('Searches for an element and returns the key if found', function() { 457 | $res = indexOf(partial(op, '==', 'b'), ['a', 'b', 'c']); 458 | expect($res)->equal(1); 459 | }); 460 | }); 461 | describe('isNull', function() { 462 | docFn(isNull::class); 463 | test('alias for is_null', function() { 464 | expect(isNull(null))->equal(true); 465 | expect(isNull(0))->equal(false); 466 | }); 467 | }); 468 | describe('iter', function() { 469 | docFn(iter::class); 470 | docIntro('Converts any iterable into a proper instance of Iterator.'); 471 | test('Can convert arrays', function() { 472 | expect(iter([1,2,3]))->instanceof('Iterator'); 473 | }); 474 | test('Can convert an Iterator', function() { 475 | expect(iter(new \ArrayIterator([1,2,3])))->instanceof('Iterator'); 476 | }); 477 | test('Can convert objects', function() { 478 | $obj = (object) ['a' => 1, 'b' => 2]; 479 | expect(iter($obj))->instanceof('Iterator'); 480 | expect(toArrayWithKeys(iter($obj)))->equal(['a' => 1, 'b' => 2]); 481 | }); 482 | test('Can convert any iterable', function() { 483 | $a = new class() implements \IteratorAggregate { 484 | public function getIterator() { 485 | return new \ArrayIterator([1,2,3]); 486 | } 487 | }; 488 | expect(iter($a))->instanceof('Iterator'); 489 | expect(toArray(iter($a)))->equal([1,2,3]); 490 | }); 491 | test('Can convert strings', function() { 492 | expect(iter('abc'))->instanceof('Iterator'); 493 | expect(toArray(iter('abc')))->equal(['a', 'b', 'c']); 494 | }); 495 | test('Will throw an exception otherwise', function() { 496 | expect(function() { 497 | iter(1); 498 | })->throw('LogicException', 'Iter could not be converted into an iterable.'); 499 | }); 500 | }); 501 | describe('join', function() { 502 | docFn(join::class); 503 | test('Joins an iterable with a given separator', function() { 504 | $res = join(",", range(1,3)); 505 | expect($res)->equal("1,2,3"); 506 | }); 507 | }); 508 | describe('keys', function() { 509 | docFn(keys::class); 510 | test('Yields only the keys of an in iterable', function() { 511 | $keys = keys(['a' => 1, 'b' => 2]); 512 | expect(toArray($keys))->equal(['a', 'b']); 513 | }); 514 | }); 515 | describe('map', function() { 516 | docFn(map::class); 517 | it('Lazily maps an iterable\'s values to a different set', function() { 518 | $values = map(partial(op, '*', 2), [1,2,3,4]); 519 | expect(toArray($values))->equal([2,4,6,8]); 520 | }); 521 | }); 522 | describe('mapAccum', function() { 523 | docFn(mapAccum::class); 524 | 525 | test('Maps a function to each element of a list while passing in an accumulator to accumulate over every iteration', function() { 526 | $data = iter('abcd'); 527 | [$totalSort, $values] = mapAccum(function($acc, $value) { 528 | return [$acc + 1, ['name' => $value, 'sort' => $acc]]; 529 | }, iter('abcd'), 0); 530 | 531 | expect($totalSort)->equal(4); 532 | expect($values)->equal([ 533 | ['name' => 'a', 'sort' => 0], 534 | ['name' => 'b', 'sort' => 1], 535 | ['name' => 'c', 'sort' => 2], 536 | ['name' => 'd', 'sort' => 3], 537 | ]); 538 | }); 539 | 540 | docOutro('Note: mapAccum converts the interable into an array and is not lazy like most of the other functions in this library'); 541 | }); 542 | describe('mapKeys', function() { 543 | docFn(mapKeys::class); 544 | it('Lazily maps an iterable\'s keys to a different set', function() { 545 | $keys = mapKeys(partial(op, '.', '_'), ['a' => 1, 'b' => 2]); 546 | expect(toArrayWithKeys($keys))->equal(['a_' => 1, 'b_' => 2]); 547 | }); 548 | }); 549 | describe('mapKeyValue', function() { 550 | docFn(mapKeyValue::class); 551 | it('Lazily maps an iterable\'s key/value tuples to a different set', function() { 552 | $keys = mapKeyValue(function($kv) { 553 | [$key, $value] = $kv; 554 | return ["{$key}_", $value * $value]; 555 | }, ['a' => 1, 'b' => 2]); 556 | expect(toArrayWithKeys($keys))->equal(['a_' => 1, 'b_' => 4]); 557 | }); 558 | }); 559 | describe('mapOn', function() { 560 | docFn(mapOn::class); 561 | it('Maps values on specific keys', function() { 562 | $values = mapOn([ 563 | 'a' => partial(op, '*', 3), 564 | 'b' => partial(op, '+', 1), 565 | ], [ 566 | 'a' => 1, 567 | 'b' => 2, 568 | 'c' => 3, 569 | ]); 570 | 571 | expect(toArray($values))->equal([3,3,3]); 572 | }); 573 | }); 574 | describe('nullable', function() { 575 | docFn(nullable::class); 576 | test('Performs the callable if the value is not null', function() { 577 | expect(nullable('intval', '0'))->equal(0); 578 | }); 579 | test('Returns null if the value is null', function() { 580 | expect(nullable('intval', null))->equal(null); 581 | }); 582 | }); 583 | describe('onEach', function() { 584 | docFn(onEach::class); 585 | docIntro('Duplicate of each.'); 586 | test('Invokes a callable on each item in an iterable', function() { 587 | $state = [ 588 | (object) ['id' => 1], 589 | (object) ['id' => 2], 590 | ]; 591 | onEach(function($item) { 592 | $item->id += 1; 593 | }, $state); 594 | 595 | expect([$state[0]->id, $state[1]->id])->equal([2,3]); 596 | }); 597 | docOutro('Normally using php foreach should suffice for iterating over an iterable; however, php variables in foreach loops are not scoped whereas closures are.'); 598 | }); 599 | describe('op', function() { 600 | docFn(op::class); 601 | 602 | $intro = <<<'INTRO' 603 | op evaluates binary operations. It expects the right hand operator first which makes most sense when currying or partially applying the op function. 604 | When reading the op func, it should be read: `evaluate $op with $b with $a` e.g.: 605 | 606 | ``` 607 | op('+', 2, 3) -> add 2 with 3 608 | op('-', 2, 3) -> subtract 2 from 3 609 | op('>', 2, 3) => compare greater than 2 with 3 610 | ``` 611 | INTRO; 612 | docIntro($intro); 613 | test('Evaluates two values with a given operator', function() { 614 | $res = op('<', 2, 1); 615 | expect($res)->equal(true); 616 | }); 617 | test('Supports equality operators', function() { 618 | $obj = new stdClass(); 619 | $ops = [ 620 | ['==', [1, 1]], 621 | ['eq', [2, 2]], 622 | ['!=', [1, 2]], 623 | ['neq', [2, 3]], 624 | ['===', [$obj, $obj]], 625 | ['!==', [new stdClass(), new stdClass()]], 626 | ['>', [1, 2]], 627 | ['gt', [1, 3]], 628 | ['>=', [1, 2]], 629 | ['gte', [1, 1]], 630 | ['<', [2, 1]], 631 | ['lt', [3, 1]], 632 | ['<=', [2, 1]], 633 | ['lte', [1, 1]], 634 | ]; 635 | 636 | foreach ($ops as list($op, list($b, $a))) { 637 | $res = op($op, $b, $a); 638 | expect($res)->equal(true); 639 | } 640 | }); 641 | test('Supports other operators', function() { 642 | $ops = [ 643 | ['+', [2, 3], 5], 644 | ['-', [2, 3], 1], 645 | ['*', [2, 3], 6], 646 | ['**', [2, 3], 9], 647 | ['/', [2, 3], 1.5], 648 | ['%', [2, 3], 1], 649 | ['.', ['b', 'a'], 'ab'], 650 | ]; 651 | 652 | foreach ($ops as list($op, list($b, $a), $expected)) { 653 | $res = op($op, $b, $a); 654 | expect($res)->equal($expected); 655 | } 656 | }); 657 | test('Is more useful partially applied or curried', function() { 658 | $add2 = Curried\op('+')(2); 659 | $mul3 = partial(op, '*', 3); 660 | $sub4 = Curried\op('-')(4); 661 | 662 | // ((2 + 2) * 3) - 4 663 | $res = compose($sub4, $mul3, $add2)(2); 664 | expect($res)->equal(8); 665 | }); 666 | }); 667 | describe('pad', function() { 668 | docFn(pad::class); 669 | test('Pads an iterable to a specific size', function() { 670 | $res = pad(5, [1,2,3]); 671 | expect(toArray($res))->equal([1,2,3,null,null]); 672 | }); 673 | test('Allows custom pad values', function() { 674 | $res = pad(5, [1,2,3], 0); 675 | expect(toArray($res))->equal([1,2,3,0,0]); 676 | }); 677 | test('Pads nothing if iterable is the same size as pad size', function() { 678 | $res = pad(5, [1,2,3,4,5]); 679 | expect(toArray($res))->equal([1,2,3,4,5]); 680 | }); 681 | test('Pads nothing if iterable is greater than pad size', function() { 682 | $res = pad(5, [1,2,3,4,5,6]); 683 | expect(toArray($res))->equal([1,2,3,4,5,6]); 684 | }); 685 | test('Ignores keys of original iterable', function() { 686 | $res = pad(3, ['a' => 1, 'b' => 2]); 687 | expect(toArrayWithKeys($res))->equal([1,2,null]); 688 | }); 689 | }); 690 | describe('partial', function() { 691 | docFn(partial::class); 692 | test('Partially applies arguments to a function. Given a function signature like f = (a, b, c) -> d, partial(f, a, b) -> (c) -> d', function() { 693 | $fn = function($a, $b, $c) { 694 | return ($a + $b) * $c; 695 | }; 696 | $fn = partial($fn, 1, 2); // apply the two arguments (a, b) and return a new function with signature (c) -> d 697 | expect($fn(3))->equal(9); 698 | }); 699 | test('You can also use place holders when partially applying', function() { 700 | $fn = function($a, $b, $c) { return ($a + $b) * $c; }; 701 | 702 | // _() represents a placeholder for parameter b. 703 | $fn = partial($fn, 1, _(), 3); // create the new func with signature (b) -> d 704 | 705 | expect($fn(2))->equal(9); 706 | }); 707 | test('Full partial application also works', function() { 708 | $fn = function($a, $b) { return [$a, $b]; }; 709 | $fn = partial($fn, 1, 2); 710 | expect($fn())->equal([1,2]); 711 | }); 712 | }); 713 | describe('partition', function() { 714 | docFn(partition::class); 715 | 716 | it('Splits an iterable into different arrays based off of a predicate. The predicate should return the index to partition the data into', function() { 717 | list($left, $right) = partition(function($v) { 718 | return $v < 3 ? 0 : 1; 719 | }, [1,2,3,4]); 720 | 721 | expect([$left, $right])->equal([[1,2], [3,4]]); 722 | }); 723 | }); 724 | describe('pick', function() { 725 | docFn(pick::class); 726 | test('Picks only the given fields from a structured array', function() { 727 | $res = pick(['a', 'b'], [ 728 | 'a' => 1, 729 | 'b' => 2, 730 | 'c' => 3, 731 | ]); 732 | expect($res)->equal(['a' => 1, 'b' => 2]); 733 | }); 734 | test('Can be used in curried form', function() { 735 | $res = arrayMap(Curried\pick(['id', 'name']), [ 736 | ['id' => 1, 'name' => 'Foo', 'slug' => 'foo'], 737 | ['id' => 2, 'name' => 'Bar', 'slug' => 'bar'], 738 | ]); 739 | expect($res)->equal([ 740 | ['id' => 1, 'name' => 'Foo'], 741 | ['id' => 2, 'name' => 'Bar'], 742 | ]); 743 | }); 744 | }); 745 | describe('pickBy', function() { 746 | docFn(pickBy::class); 747 | test('Picks only the fields that match the pick function from a structured array', function() { 748 | $res = pickBy(Curried\spread(function(string $key, int $value): bool { 749 | return $value % 2 === 0; 750 | }), [ 751 | 'a' => 1, 752 | 'b' => 2, 753 | 'c' => 3, 754 | ]); 755 | expect($res)->equal(['b' => 2]); 756 | }); 757 | }); 758 | describe('pipe', function() { 759 | docFn(pipe::class); 760 | 761 | test('Creates a function that pipes values from one func to the next.', function() { 762 | $add3 = Curried\op('+')(3); 763 | $mul2 = Curried\op('*')(2); 764 | 765 | $add3ThenMul2 = pipe($add3, $mul2); 766 | $res = $add3ThenMul2(5); 767 | expect($res)->equal(16); 768 | }); 769 | test('Allows an empty initial argument', function() { 770 | $res = pipe( 771 | function() { yield from [1,2,3]; }, 772 | Curried\reduce(function($acc, $v) { return $acc + $v; }, 0) 773 | )(); 774 | expect($res)->equal(6); 775 | }); 776 | 777 | docOutro('`pipe` and `compose` are sister functions and do the same thing except the functions are composed in reverse order. pipe(f, g)(x) = g(f(x))'); 778 | }); 779 | describe('product', function() { 780 | docFn(product::class); 781 | 782 | test('Creates a cartesian product of multiple sets', function() { 783 | $res = product([1,2], [3,4], [5, 6]); 784 | expect(toArray($res))->equal([ 785 | [1,3,5], 786 | [1,3,6], 787 | [1,4,5], 788 | [1,4,6], 789 | [2,3,5], 790 | [2,3,6], 791 | [2,4,5], 792 | [2,4,6], 793 | ]); 794 | }); 795 | }); 796 | describe('prop', function() { 797 | docFn(prop::class); 798 | 799 | test('Accesses a property from an object', function() { 800 | $obj = new \StdClass(); 801 | $obj->id = 1; 802 | $res = prop('id', $obj); 803 | expect($res)->equal(1); 804 | }); 805 | test('If no property exists, it will return the $else value', function() { 806 | $obj = new \StdClass(); 807 | $res = prop('id', $obj, 2); 808 | expect($res)->equal(2); 809 | }); 810 | }); 811 | describe('propIn', function() { 812 | docFn(propIn::class); 813 | 814 | test('Accesses a property deep in an object tree', function() { 815 | $obj = new \StdClass(); 816 | $obj->id = 1; 817 | $obj->child = new \StdClass(); 818 | $obj->child->id = 2; 819 | $res = propIn(['child', 'id'], $obj); 820 | expect($res)->equal(2); 821 | }); 822 | test('If any property is missing in the tree, it will return the $else value', function() { 823 | $obj = new \StdClass(); 824 | $obj->id = 1; 825 | $obj->child = new \StdClass(); 826 | $res = propIn(['child', 'id'], $obj, 3); 827 | expect($res)->equal(3); 828 | }); 829 | }); 830 | describe('range', function() { 831 | docFn(range::class); 832 | test('Creates an iterable of a range of values starting from $start going to $end inclusively incrementing by $step', function() { 833 | $res = range(1, 3); 834 | expect(toArray($res))->equal([1,2,3]); 835 | }); 836 | test('It also allows a decreasing range', function() { 837 | $res = range(3, 1); 838 | expect(toArray($res))->equal([3,2,1]); 839 | }); 840 | test('An exception will be thrown if the $step provided goes in the wrong direction', function() { 841 | expect(function() { 842 | toArray(range(1, 2, -1)); 843 | })->throw(\InvalidArgumentException::class); 844 | expect(function() { 845 | toArray(range(2, 1, 1)); 846 | })->throw(\InvalidArgumentException::class); 847 | }); 848 | }); 849 | describe('reduce', function() { 850 | docFn(reduce::class); 851 | test('Reduces an iterable into a single value', function() { 852 | $res = reduce(function($acc, $v) { 853 | return $acc + $v; 854 | }, range(1,3), 0); 855 | expect($res)->equal(6); 856 | }); 857 | }); 858 | describe('reduceKeyValue', function() { 859 | docFn(reduceKeyValue::class); 860 | test('Reduces an iterables key value pairs into a value', function() { 861 | $res = reduceKeyValue(function($acc, $kv) { 862 | [$key, $value] = $kv; 863 | return $acc . $key . $value; 864 | }, fromPairs([['a', 1], ['b', 2]]), ""); 865 | expect($res)->equal("a1b2"); 866 | }); 867 | }); 868 | describe('reindex', function() { 869 | docFn(reindex::class); 870 | test('Re-indexes a collection via a callable', function() { 871 | $res = reindex(function($v) { 872 | return $v['id']; 873 | }, [['id' => 2], ['id' => 3], ['id' => 1]]); 874 | 875 | expect(toArrayWithKeys($res))->equal([ 876 | 2 => ['id' => 2], 877 | 3 => ['id' => 3], 878 | 1 => ['id' => 1], 879 | ]); 880 | }); 881 | }); 882 | describe('retry', function() { 883 | docFn(retry::class); 884 | test('Executes a function and retries if an exception is thrown', function() { 885 | $i = 0; 886 | $res = retry(function() use (&$i) { 887 | $i += 1; 888 | if ($i <= 1) { 889 | throw new \Exception('bad'); 890 | } 891 | 892 | return $i; 893 | }); 894 | 895 | expect($res)->equal(2); 896 | }); 897 | test('Only retries $maxTries times else it gives up and bubbles the exception', function() { 898 | expect(function() { 899 | $i = 0; 900 | retry(function() use (&$i) { 901 | $i += 1; 902 | throw new \Exception((string) $i); 903 | }, 5); 904 | })->throw('Exception', '6'); 905 | }); 906 | test('Retries until $shouldRetry returns false', function() { 907 | $i = 0; 908 | 909 | expect(function() { 910 | $res = retry(function() use (&$i) { 911 | $i += 1; 912 | throw new \Exception((string) $i); 913 | }, function($numRetries, \Throwable $t = null) { 914 | return $numRetries < 2; 915 | }); 916 | })->throw('Exception', '2'); 917 | }); 918 | test("Sends numRetries into the main fn", function() { 919 | $res = retry(function($numRetries) { 920 | if (!$numRetries) { 921 | throw new Exception('bad'); 922 | } 923 | 924 | return $numRetries; 925 | }, 2); 926 | expect($res)->equal(1); 927 | }); 928 | docOutro('Keep in mind that maxTries determines the number of *re*-tries. This means the function will execute maxTries + 1 times since the first invocation is not a retry.'); 929 | }); 930 | describe('search', function() { 931 | docFn(search::class); 932 | test('Searches for an element in a collection where the callable returns true', function() { 933 | $res = search(function($v) { 934 | return $v['id'] == 2; 935 | }, [['id' => 1], ['id' => 2], ['id' => 3]]); 936 | expect($res)->equal(['id' => 2]); 937 | }); 938 | test('Returns null if no element was found', function() { 939 | $res = search(function($v) { 940 | return false; 941 | }, [['id' => 1], ['id' => 2], ['id' => 3]]); 942 | expect($res)->equal(null); 943 | }); 944 | }); 945 | describe('setIndex', function() { 946 | docFn(setIndex::class); 947 | test('Sets an index in an array', function() { 948 | $res = setIndex('a', 1, []); 949 | expect($res['a'])->equal(1); 950 | }); 951 | }); 952 | describe('setIndexIn', function() { 953 | docFn(setIndexIn::class); 954 | test('Sets a nested index in an array', function() { 955 | $res = setIndexIn(['a', 'b'], 1, ['a' => []]); 956 | expect($res['a']['b'])->equal(1); 957 | }); 958 | }); 959 | describe('setProp', function() { 960 | docFn(setProp::class); 961 | test('Sets a property in an object', function() { 962 | $res = setProp('a', 1, (object) []); 963 | expect($res->a)->equal(1); 964 | }); 965 | }); 966 | describe('slice', function() { 967 | docFn(slice::class); 968 | test('It takes an inclusive slice from start to a given length of an interable', function() { 969 | $sliced = slice(1, range(0, 4), 2); 970 | expect(toArray($sliced))->equal([1, 2]); 971 | }); 972 | test('If length is not supplied it default to the end of the iterable', function() { 973 | $sliced = slice(2, range(0, 4)); 974 | expect(toArray($sliced))->equal([2,3,4]); 975 | }); 976 | test('will not consume the iterator once the slice has been yielded', function() { 977 | $i = 0; 978 | $gen = function() use (&$i) { 979 | foreach (range(0, 4) as $v) { 980 | $i = $v; 981 | yield $i; 982 | } 983 | }; 984 | $sliced = toArray(slice(1, $gen(), 2)); 985 | expect($sliced)->equal([1, 2]); 986 | expect($i)->equal(2); 987 | }); 988 | }); 989 | describe('sortFromArray', function() { 990 | docFn(sortFromArray::class); 991 | test("Sort an iterable with a given array of ordered elements to sort by", function() { 992 | $data = [ 993 | ['id' => 1, 'name' => 'A'], 994 | ['id' => 2, 'name' => 'B'], 995 | ['id' => 3, 'name' => 'C'], 996 | ]; 997 | $res = sortFromArray(Curried\index('id'), [2,3,1], $data); 998 | expect(arrayMap(Curried\index('name'), $res))->equal(['B', 'C', 'A']); 999 | }); 1000 | test('Throws an exception if any item in the iterable is not within the orderedElements', function() { 1001 | expect(function() { 1002 | $data = [['id' => 1]]; 1003 | $res = sortFromArray(Curried\index('id'), [], $data); 1004 | })->throw(\LogicException::class, 'Cannot sort element key 1 because it does not exist in the ordered elements.'); 1005 | }); 1006 | 1007 | docOutro("I've found this to be very useful when you fetch records from a database with a WHERE IN clause, and you need to make sure the results are in the same order as the ids in the WHERE IN clause."); 1008 | }); 1009 | describe('spread', function() { 1010 | docFn(spread::class); 1011 | test("Spreads an array of arguments to a callable", function() { 1012 | $res = spread(function($a, $b) { 1013 | return $a . $b; 1014 | }, ['a', 'b']); 1015 | expect($res)->equal('ab'); 1016 | }); 1017 | test('Can be used in the curried form to unpack tuple arguments', function() { 1018 | $res = arrayMap(Curried\spread(function(string $first, int $second) { 1019 | return $first . $second; 1020 | }), [['a', 1], ['b', 2]]); 1021 | expect($res)->equal(['a1', 'b2']); 1022 | }); 1023 | docOutro("Note: this is basically just an alias for `call_user_func_array` or simply a functional wrapper around the `...` (spread) operator."); 1024 | }); 1025 | describe('take', function() { 1026 | docFn(take::class); 1027 | test('Takes the first num items from an iterable', function() { 1028 | $res = take(2, range(0, 10)); 1029 | expect(toArray($res))->equal([0, 1]); 1030 | }); 1031 | }); 1032 | describe('takeWhile', function() { 1033 | docFn(takeWhile::class); 1034 | test('Takes elements from an iterable while the $predicate returns true', function() { 1035 | $res = takeWhile(Curried\op('>')(0), [2, 1, 0, 1, 2]); 1036 | expect(toArray($res))->equal([2,1]); 1037 | }); 1038 | }); 1039 | describe('tap', function() { 1040 | docFn(tap::class); 1041 | it('Calls given tap function on value and returns value', function() { 1042 | $loggedValues = []; 1043 | $res = tap(function(string $v) use (&$loggedValues) { 1044 | $loggedValues[] = $v; 1045 | }, 'abc'); 1046 | expect([$loggedValues[0], $res])->equal(['abc', 'abc']); 1047 | }); 1048 | docOutro('`tap` is useful anytime you need to operate on a value and do not want to modify the return value.'); 1049 | }); 1050 | describe('throwIf', function() { 1051 | docFn(throwIf::class); 1052 | it('Throws the given exception if value given evaluates to true', function() { 1053 | expect(function() { 1054 | throwIf( 1055 | function(int $value) { return new RuntimeException('Error: ' . $value); }, 1056 | function(int $value) { return $value === 0; }, 1057 | 0 1058 | ); 1059 | })->throw(RuntimeException::class, 'Error: 0'); 1060 | }); 1061 | it('Returns given value if value evaluates to false', function() { 1062 | $res = throwIf( 1063 | function(int $value) { return new RuntimeException('Error: ' . $value); }, 1064 | function(int $value) { return $value === 0; }, 1065 | 1 1066 | ); 1067 | expect($res)->equal(1); 1068 | }); 1069 | docOutro('Note: works best with short closures!'); 1070 | }); 1071 | describe('toArray', function() { 1072 | docFn(toArray::class); 1073 | it('will tranform any iterable into an array', function() { 1074 | $res = toArray((function() { yield 1; yield 2; yield 3; })()); 1075 | expect($res)->equal([1,2,3]); 1076 | }); 1077 | it('can also be used as a constant', function() { 1078 | $res = compose(toArray, id)((function() {yield 1; yield 2; yield 3;})()); 1079 | expect($res)->equal([1,2,3]); 1080 | }); 1081 | }); 1082 | describe('toArrayWithKeys', function() { 1083 | docFn(toArrayWithKeys::class); 1084 | it('can convert to an array and keep the keys', function() { 1085 | $gen = function() { yield 'a' => 1; yield 'b' => 2; }; 1086 | expect(toArrayWithKeys($gen()))->equal(['a' => 1, 'b' => 2]); 1087 | }); 1088 | }); 1089 | describe('toPairs', function() { 1090 | docFn(toPairs::class); 1091 | test('Transforms an associative array into an iterable of tuples [$key, $value]', function() { 1092 | $res = toPairs([ 1093 | 'a' => 1, 1094 | 'b' => 2, 1095 | ]); 1096 | expect(toArray($res))->equal([ 1097 | ['a', 1], 1098 | ['b', 2] 1099 | ]); 1100 | }); 1101 | }); 1102 | describe('updateIndexIn', function() { 1103 | docFn(updateIndexIn::class); 1104 | 1105 | test('Updates a nested element within a deep array structure', function() { 1106 | $data = ['a' => ['b' => ['c' => 3]]]; 1107 | 1108 | $data = updateIndexIn(['a', 'b', 'c'], function($v) { 1109 | return $v * $v; 1110 | }, $data); 1111 | 1112 | expect($data)->equal(['a' => ['b' => ['c' => 9]]]); 1113 | }); 1114 | test('Throws an exception if nested key does not exist', function() { 1115 | expect(function() { 1116 | $data = ['a' => ['b' => ['c' => 9]]]; 1117 | updateIndexIn(['a', 'c', 'c'], function() {}, $data); 1118 | })->throw(\RuntimeException::class, 'Could not updateIn because the keys a -> c -> c could not be found.'); 1119 | }); 1120 | }); 1121 | describe('values', function() { 1122 | docFn(values::class); 1123 | test('Exports only the values of an iterable', function() { 1124 | $res = values(['a' => 1, 'b' => 2]); 1125 | expect(toArrayWithKeys($res))->equal([1,2]); 1126 | }); 1127 | }); 1128 | describe('when', function() { 1129 | docFn(when::class); 1130 | it('Evaluates the given value with the $then callable if the predicate returns true', function() { 1131 | $if = function($v) { return $v == 3; }; 1132 | $then = function($v) { return $v * $v; }; 1133 | $res = when($if, $then, 3); 1134 | expect($res)->equal(9); 1135 | }); 1136 | test('But will return the given value if the predicate returns false', function() { 1137 | $if = function($v) { return $v == 3; }; 1138 | $then = function($v) { return $v * $v; }; 1139 | $res = when($if, $then, 4); 1140 | expect($res)->equal(4); 1141 | }); 1142 | }); 1143 | describe('withState', function() { 1144 | docFn(withState::class); 1145 | 1146 | test('Decorate a function with accumulating state', function() { 1147 | $fn = withState(function($state, $v) { 1148 | return [$state + 1, $state . ': ' . $v]; 1149 | }, 1); 1150 | $res = arrayMap($fn, iter('abcd')); 1151 | expect($res)->equal(['1: a', '2: b', '3: c', '4: d']); 1152 | }); 1153 | }); 1154 | describe('within', function() { 1155 | docFn(within::class); 1156 | 1157 | test('Only allows keys within the given array to stay', function() { 1158 | $data = flip(iter('abcd')); 1159 | $res = within(['a', 'c'], $data); 1160 | expect(toArrayWithKeys($res))->equal(['a' => 0, 'c' => 2]); 1161 | }); 1162 | }); 1163 | describe('without', function() { 1164 | docFn(without::class); 1165 | 1166 | test('Filters an iterable to be without the given keys', function() { 1167 | $data = flip(iter('abcd')); 1168 | $res = without(['a', 'c'], $data); 1169 | expect(toArrayWithKeys($res))->equal(['b' => 1, 'd' => 3]); 1170 | }); 1171 | }); 1172 | describe('zip', function() { 1173 | docFn(zip::class); 1174 | test('Zips multiple iterables into an iterable n-tuples', function() { 1175 | $res = zip(iter('abc'), range(1,3), [4,5,6]); 1176 | expect(toArray($res))->equal([ 1177 | ['a', 1, 4], 1178 | ['b', 2, 5], 1179 | ['c', 3, 6], 1180 | ]); 1181 | }); 1182 | test('Returns an empty iterable if no iters are present', function() { 1183 | expect(toArray(zip()))->equal([]); 1184 | }); 1185 | }); 1186 | }); 1187 | --------------------------------------------------------------------------------