├── .codeclimate.yml ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── build.sh ├── composer.json ├── docs ├── Monad Classes.puml ├── Test-Contract.md └── monad-classes.png ├── phpunit.travis.xml ├── src └── chippyash │ └── Monad │ ├── CallFunctionAble.php │ ├── Collection.php │ ├── FMatch.php │ ├── FTry.php │ ├── FTry │ ├── Failure.php │ └── Success.php │ ├── FlattenAble.php │ ├── Identity.php │ ├── Map.php │ ├── Monad.php │ ├── Monadic.php │ ├── MutableCollection.php │ ├── Option.php │ ├── Option │ ├── None.php │ └── Some.php │ ├── ReturnValueAble.php │ └── Set.php └── test ├── .gitignore ├── phpunit.xml └── src └── chippyash ├── FTry ├── FailureTest.php └── SuccessTest.php └── Monad ├── CollectionTest.php ├── FMatchTest.php ├── FTryTest.php ├── IdentityTest.php ├── MapTest.php ├── MonadTest.php ├── MutableCollectionTest.php ├── Option ├── NoneTest.php └── SomeTest.php ├── OptionTest.php └── SetTest.php /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | # Save as .codeclimate.yml (note leading .) in project root directory 2 | languages: 3 | Ruby: false 4 | JavaScript: false 5 | PHP: true 6 | Python: false 7 | exclude_paths: 8 | - "test/*" 9 | - "docs/*" 10 | - "examples/*" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nbproject* 2 | vendor* 3 | composer.lock 4 | .directory 5 | .idea 6 | .phpunit.result.cache 7 | build 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # see http://about.travis-ci.org/docs/user/languages/php/ for more hints 2 | language: php 3 | #possible workaround for https://github.com/composer/composer/issues/6342 4 | 5 | # list any PHP version you want to test against 6 | php: 7 | # aliased to a recent 8.x version 8 | - 8.0 9 | # hhvm 10 | #- hhvm 11 | 12 | # omitting "script:" will default to phpunit 13 | # use the $DB env variable to determine the phpunit.xml to use 14 | before_script: 15 | - composer install --no-interaction 16 | - mkdir -p build/logs 17 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 18 | - chmod +x ./cc-test-reporter 19 | - ./cc-test-reporter before-build 20 | 21 | script: 22 | - vendor/phpunit/phpunit/phpunit --configuration ./phpunit.travis.xml test 23 | 24 | after_success: 25 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 26 | 27 | # configure notifications (email, IRC, campfire etc) 28 | notifications: 29 | email: "ashley@zf4.biz" 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 Ashley Kitson, UK 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list 7 | of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or other 11 | materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors may be 14 | used to endorse or promote products derived from this software without specific 15 | prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 18 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 22 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 23 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 26 | DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chippyash/Monad 2 | 3 | ## Quality Assurance 4 | 5 | ![PHP 8.0](https://img.shields.io/badge/PHP-8.0-blue.svg) 6 | [![Build Status](https://travis-ci.org/chippyash/Monad.svg?branch=master)](https://travis-ci.org/chippyash/Monad) 7 | [![Test Coverage](https://codeclimate.com/github/chippyash/Monad/badges/coverage.svg)](https://codeclimate.com/github/chippyash/Monad/coverage) 8 | [![Code Climate](https://codeclimate.com/github/chippyash/Monad/badges/gpa.svg)](https://codeclimate.com/github/chippyash/Monad) 9 | 10 | The above badges represent the current development branch. As a rule, I don't push 11 | to GitHub unless tests, coverage and usability are acceptable. This may not be 12 | true for short periods of time; on holiday, need code for some other downstream 13 | project etc. If you need stable code, use a tagged version. Read 'Further Documentation' 14 | and 'Installation'. 15 | 16 | [Test Contract](https://github.com/chippyash/Monad/blob/master/docs/Test-Contract.md) in the docs directory. 17 | 18 | Developer support for PHP5.4 & 5.5 was withdrawn at version 2.0.0 of this library. 19 | If you need support for PHP 5.4 or 5.5, please use a version`>=1,<2` 20 | 21 | Developer support for PHP <8 was withdrawn at version 3.0.0 of this library. 22 | If you need support for PHP 7.x, please use a version`>=2,<3` 23 | 24 | ## What? 25 | 26 | Provides a Monadic type 27 | 28 | According to my mentor, Monads are either difficult to explain or difficult to code, 29 | i.e. you can say `how` or `what` but not at the same time. If 30 | you need further illumination, start with [wikipedia](https://en.wikipedia.org/wiki/Monad_(functional_programming)) 31 | 32 | ### Types supported 33 | 34 | * Monadic Interface 35 | * Abstract Monad 36 | * Identity Monad 37 | * Option Monad 38 | * Some 39 | * None 40 | * FTry Monad 41 | * Success 42 | * Failure 43 | * FMatch Monad 44 | * Collection Monad 45 | * Map Monad 46 | * Set Monad 47 | 48 | ## Why? 49 | 50 | PHP is coming under increasing attack from functional hybrid languages such as Scala. 51 | The difference is the buzzword of `functional programming`. PHP can support this 52 | paradigm, and this library introduces some basic monadic types. Indeed, learning 53 | functional programming practices can make solutions in PHP far more robust. 54 | 55 | Much of the power of monadic types comes through the use of the functional FMatch, 56 | Try and For Comprehension language constructs. PHP doesn't have these. This library provides: 57 | 58 | - FMatch 59 | - FTry 60 | - FFor This is provided in the [Assembly-Builder package](https://github.com/chippyash/Assembly-Builder) 61 | 62 | Key to functional programming is the use of strict typing and elevating functions as 63 | first class citizens within the language syntax. PHP5.4+ allows functions to be used as 64 | a typed parameter (Closure). It also appears that PHP devs are coming to terms with 65 | strict or hard types as the uptake of my [strong-type library](https://packagist.org/packages/chippyash/strong-type) testifies. 66 | 67 | ## How 68 | 69 | ### The Monadic interface 70 | 71 | A Monad has three things (according to my understanding of it): 72 | 73 | - a value (which may be no value at all, a simple type, an object or a function) 74 | - method of getting its value, often referred to as return() 75 | - a way of binding (or using) the value into some function, often referred to as bind(), 76 | the return value of which is another Monad, often but not always of the same type as 77 | the donor Monad. (Rarely, it could be another class type.) 78 | 79 | The Monadic Interface supplied here defines 80 | 81 | - bind(\Closure $function, array $args = []):Monadic 82 | - The function signature should contain at least one parameter to receive the value 83 | of the Monad. e.g. `function($val){return $val * 2;}` If you are using additional 84 | arguments in the $args array, you'll need to add them to the parameter list e.g. 85 |
 86 | $ret = $m->bind(function($val, $mult){return $val * $mult;}, [4]);
 87 | 
88 | Bear in mind that you can use the `use` clause as normal when defining your function 89 | to expose external parameter values. Caveat: start using this stuff in pure Async 90 | PHP programming and you can't use the `use` clause. You have been warned! 91 | 92 | 93 | - value():mixed - the return() method as `return` is a reserved word in PHP 94 | 95 | Additionally, two helper methods are defined for the interface 96 | 97 | - flatten():mixed - the monadic value `flattened` to a PHP native type or non Monadic object 98 | - static create(mixed $value):Monadic A factory method to create an instance of the concrete descendant Monad 99 | 100 | Monads have an immutable value, that is to say, the result of the bind() 101 | method is another (Monadic) class. The original value is left alone. 102 | 103 | ### The Monad Abstract class 104 | 105 | Contains the Monad value holder and a `syntatic sugar` helper magic \__invoke() method that 106 | proxies to value() if no parameters supplied or bind() if a Closure (with/without optional arguments) 107 | are supplied. 108 | 109 | Neither the Monadic interface or the abstract Monad class define how to set a value on 110 | construction of a concrete Monad. It usually makes sense to set the Monad's value on construction. 111 | Therefore in most circumstances you would create concrete Monad classes with some 112 | form of constructor. 113 | 114 | ### Concrete Monad classes supplied 115 | 116 | #### Identity 117 | The simplest type of Monad 118 | 119 |
120 | use Monad\Identity;
121 | 
122 | $id = new Identity('foo');
123 | //or
124 | $id = Identity::create('foo');
125 | 
126 | $fConcat = function($value, $fudge){return $value . $fudge};
127 | $concat = $id->bind($fConcat, ['bar'])
128 |              ->bind($fConcat, ['baz']);
129 | 
130 | echo $concat->value();      //'foobarbaz'
131 | echo $id->value();          //'foo'
132 | 
133 | 134 | #### Option 135 | 136 | An Option is a polymorphic `Maybe Monad` that can exist in one of two states: 137 | 138 | - Some - an option with a value 139 | - None - an option with no value 140 | 141 | As PHP does not have the language construct to create a polymorphic object by construction, 142 | you'll need to use the Option::create() static method. You can however use Option as 143 | a type hint for other class methods and returns 144 | 145 |
146 | use Monad\Option;
147 | use Monad\Option\Some;
148 | use Monad\Option\None;
149 | 
150 | /**
151 |  * @param Option $opt
152 |  * @return Option
153 |  */
154 | function doSomethingWithAnOption(Option $opt) {
155 |     if ($opt instanceof None) {
156 |         return $opt;
157 |     }
158 |     
159 |     //must be a Some
160 |     return $opt(doMyOtherThing()); //use magic invoke to bind
161 | 
162 | }
163 | 
164 | $someOption = Option::create('foo');
165 | $noneOption = Option::create();
166 | 
167 | $one = doSomethingWithAnOption($someOption);
168 | $two = doSomethingWithAnOption($noneOption);
169 | 
170 | 171 | Under normal circumstances, Option uses the `null` value to determine whether or not 172 | to create a Some or a None; that is, the value passed into create() is tested against 173 | `null`. If it === null, then a None is created, else a Some. You can provide an 174 | alternative test value as a second parameter to create() 175 | 176 |
177 | $mySome = Option::create(true, false);
178 | $myNone = Option::create(false, false);
179 | 
180 | 181 | Once a None, always a None. No amount of binding will return anything other than a None. 182 | On the other hand, a Some can become a None through binding, (or rather the result of 183 | the bind() as of course the original Some remains immutable.) To assist in this, 184 | Some->bind() can take an optional third parameter, which is the value to test against 185 | for None (i.e. like the optional second parameter to Option::create() ) 186 | 187 | You should also note that calling ->value() on a None will generate a RuntimeException 188 | because of course, a None does not have a value! 189 | 190 | ##### Other methods supported 191 | 192 | * getOrElse(mixed:elseValue) If the Option is a Some, return the Option->value() else 193 | return the elseValue 194 | 195 | #### FTry 196 | 197 | An FTry is a polymorphic `Try Monad` that can exist in one of two states: 198 | 199 | - Success - an FTry with a value 200 | - Failure - an FTry with a PHP Exception as a value 201 | 202 | `Try` is a reserved word in PHP, so I have called this class FTry to mean `Functional Try`. 203 | 204 | As PHP does not have the language construct to create a polymorphic object by construction, 205 | you'll need to use the FTry::with() (or FTry::create()) static method. You can however 206 | use FTry as a type hint for other class methods and returns 207 | 208 | FTry::on(value) will catch any Exception incurred in processing the value, and return 209 | a Success or Failure class appropriately. This makes it ideal for the simple case 210 | of wrapping a PHP transaction in a Try - Catch block: 211 | 212 |
213 | use Monad\FTry;
214 | use Monad\FMatch;
215 | 
216 | FMatch::on(FTry::with($myFunction($initialValue())))
217 |     ->Monad_FTry_Success(function ($v) {doSomethingGood($v);})
218 |     ->Monad_FTry_Failure(
219 |         function (\Exception $e) {
220 |             echo "Exception: " . $e->getMessage(); 
221 |         }
222 |     );
223 | 
224 | 225 | A fairly simplistic example, and one where you might question its value, as it could have 226 | been written as easily using conventional PHP. But: A Success or Failure is still 227 | a Monad, and so you can still bind (map) onto the resultant class, flatten it etc. 228 | 229 | Like Option, FTry also supports the `getOrElse(mixed:elseValue)` method allowing for implementing 230 | default behaviours: 231 | 232 |
233 | echo FTry::with(myComplexPrintableTransaction())
234 |     ->getOrElse('Sorry - that failed');
235 | 
236 | 237 | For completeness, FTry also supports `isSuccess()`: 238 | 239 |
240 | echo 'The colour is' . FTry::with(myTest())->isSuccess() ? 'blue' : 'red';
241 | 
242 | 243 | Once a Failure, always a Failure. However, A Success can yield either a Success 244 | or a Failure as a result of binding. 245 | 246 | If you really want to throw the exception contained in a Failure use the `pass()` method 247 | 248 |
249 | $try = FTry::with($myFunction());
250 | if (!$try->isSuccess()) $try->pass();
251 | 
252 | 253 | #### FMatch 254 | 255 | The FMatch Monad allows you to carry out type pattern matching to create powerful and 256 | dynamic functional equivalents of `case statements`. 257 | 258 | 'Match' is a reserved word since PHP8. Thus for V3 of this library, I've 259 | renamed Match to FMatch. 260 | 261 | The basic syntax is 262 | 263 |
264 | use Monad\FMatch;
265 | 
266 | $result = FMatch::on($initialValue)
267 |             ->test()
268 |             ->test()
269 |             ->value();
270 | 
271 | 272 | where test() can be the name of a native PHP type or the name of a class, e.g.: 273 | 274 |
275 | $result = FMatch::on($initialValue)
276 |             ->string()
277 |             ->Monad_Option()
278 |             ->Monad_Identity()
279 |             ->value()
280 | 
281 | 282 | You can use the FMatch::any() method to catch anything not matched by a specific matcher: 283 | 284 |
285 | $result = FMatch::on($initialValue)
286 |             ->string()
287 |             ->int()
288 |             ->any()
289 |             ->value();
290 | 
291 | 292 | You can provide a concrete value as a parameter to each test, or a function. e.g. 293 | 294 |
295 | $result = FMatch::on($initialValue)
296 |               ->string('foo')
297 |               ->Monad_Option(
298 |                   function ($v) {
299 |                       return FMatch::on($v)
300 |                           ->Monad_Option_Some(function ($v) {
301 |                               return $v->value();
302 |                           })
303 |                           ->Monad_Option_None(function () {
304 |                               throw new \Exception();
305 |                           })
306 |                           ->value();
307 |                       }
308 |               )
309 |               ->Monad_Identity(
310 |                   function ($v) {
311 |                       return $v->value() . 'bar';
312 |                   }
313 |               )
314 |               ->any(function(){return 'any';})
315 |               ->value();
316 | 
317 | 318 | You can find this being tested in FMatchTest::testYouCanNestFMatches() 319 | 320 | ##### Supported native type matches 321 | 322 | - string 323 | - integer|int|long 324 | - float|double|real 325 | - null 326 | - array 327 | - bool|boolean 328 | - callable|function|closure 329 | - file 330 | - dir|directory 331 | - object 332 | - scalar 333 | - numeric 334 | - resource 335 | 336 | ##### Supported class matching 337 | 338 | Use the fully namespaced name of the class to match, substituting the backslash \\ 339 | with an underscore e.g. to test for `Monad\Option` use `Monad_Option` 340 | 341 | #### Collection 342 | 343 | The Monad Collection provides a structured array that behaves as a Monad. It is based 344 | on the SPL ArrayObject. 345 | 346 | Very important to note however is that unlike a PHP array, the Collection is type 347 | specific, i.e. you specify Collection type specifically or by default as the first member 348 | of its construction array. 349 | 350 | Another 'gotcha': As the Collection is an object, calling Collection->value() will 351 | just return the Collection itself. If you want to get a PHP array from the Collection 352 | then use `toArray()` which proxies the underlying `getArrayCopy()` and is provided 353 | as most PHPers are familiar with `toArray` as being a missing 'magic' call. 354 | 355 | Why re-invent the wheel? ArrayObject (underpinning Collection,) behaves in subtly 356 | different ways than a plain vanilla array. One: it's an object and can therefore 357 | be passed by reference, Two: because of One, it (hopefully TBC,) stops segfaults 358 | occurring in a multi thread environment. Even if Two doesn't pan out, then One still 359 | holds. 360 | 361 |
362 | use Monad\Collection;
363 | 
364 | $c = Collection::create([1,2,3,4]);
365 | //or
366 | $c = Collection::create([1,2,3,4], 'integer');
367 | 
368 | //to create an empty collection, you must specify type
369 | $c = Collection::create([], 'integer');
370 | $c = Collection::create([], 'Monad\Option');
371 | 
372 | 373 | You can get and test a Collection: 374 | 375 |
376 | $c = Collection::create([1,2,3,4]);
377 | $v = $c[2] // == 3
378 | 
379 | if (!isset($c[6]) { 
380 | ... 
381 | }
382 | 
383 | 384 | Although the Collection implements the ArrayAccess interface, trying to set or unset 385 | a value `$mCollection[0] = 'foo'` or `unset($mCollection[0])` *will* throw an 386 | exception, as Collections are *immutable* by default. In some circumstances, you 387 | may want to change this. Use the MutableCollection to allow mutability. 388 | 389 | As usual, this is not really a problem, as you can bind() or use each() on a Collection 390 | to return another Collection, (which can contain values of a different type.) 391 | Wherever possible, I've expressed the Collection implementation in terms of FMatch 392 | statements, not only because it usually means tighter code, but as something that 393 | you can look at (and criticise hopefully!) by example. 394 | 395 | You can append to a Collection, returning a new Collection 396 | 397 |
398 | $s1 = new Collection([1,2,3]);
399 | $s2 = $s1->append(4);
400 | //or
401 | $s2 = $s1->append(['foo'=>4]);
402 | 
403 | 404 | You can get the difference of two collections: 405 | 406 |
407 | $s1 = Collection::create([1, 2, 3, 6, 7]);
408 | $s2 = Collection::create([6,7]);
409 | $s3 = $s1->vDiff($s2);  //difference on values
410 | $s4 = $s1->kDiff($s2);  //difference on keys
411 | 
412 | 413 | And the intersection: 414 | 415 |
416 | $s1 = Collection::create([1, 2, 3, 6, 7]);
417 | $s2 = Collection::create([6,7]);
418 | $s3 = $s1->vIntersect($s2); //intersect on values
419 | $s4 = $s1->kIntersect($s2); //intersect on keys
420 | 
421 | 422 | `uDiff`, `kDiff`, `vIntersect` and `kIntersect` can take a second optional Closure parameter which is used 423 | as the comparator method. 424 | 425 | You can get the union of two collections, either by value or key: 426 | 427 |
428 | $s1 = Collection::create([1, 2, 3, 6, 7]);
429 | $s2 = Collection::create([3, 6, 7, 8]);
430 | $valueUnion = $s1->vUnion($s2);
431 | $keyUnion =  $s1->kUnion($s2);
432 | 
433 | 434 | You can get the head and the tail of a collection: 435 | 436 |
437 | $s1 = Collection::create([1, 2, 3, 6, 7]);
438 | echo $s1->head()[0] // 1
439 | echo $s1->tail()[0] // 2
440 | echo $s1->tail()[3] // 7
441 | 
442 | 443 | There are four function mapping methods for a Collection: 444 | 445 | - the standard Monadic bind(), whose function takes the entire `value array` of the 446 | Collection as its parameter. You should return an array as a result of the function 447 | but in the event that you do not, it will be forced to a Collection. 448 | 449 | - the each() method. Like bind(), this takes a function and an optional array of 450 | additional parameter values to pass on. However, the each function is called for 451 | each member of the collection. The results of the function are collected into a new 452 | Collection and returned. In this way, it behaves rather like the PHP native array_map. 453 | 454 | - the reduce() method. Acts just like array_reduce and returns a single value as a result 455 | of function passed in as a paramter. 456 | 457 | - the filter() method. Acts just like array_filter, but returns a new Collection as a 458 | result of the reduction. 459 | 460 | Note that you can change the base type of a resultant Collection as a result of these 461 | mapping methods(). 462 | 463 | I chose Collection as the name as it doesn't clash with `list` which is a PHP reserved name. 464 | In essence, Collection will to all intents and purposes be a List, but for die hard PHPers 465 | still behave as an array. 466 | 467 | A secondary design consideration, is that you should be able to use Collection 468 | oblivious of that fact that it is a Monad, except that it is type specific. 469 | 470 | #### Map 471 | 472 | A Map is a simple extension of a Collection that requires its entries to have a string (hash) 473 | key. It obeys all the rules of a Collection except that 474 | 475 |
476 | use Monad/Map;
477 | 
478 | $m1 = new Map(['foo']);
479 | 
480 | 481 | will not work, but 482 | 483 |
484 | $m1 = new Map(['foo'=>'bar']);
485 | 
486 | 487 | will work. You can as usual, specify the type as a second parameter. The 488 | `vUnion`, `vIntersect` and `vDiff` methods are unspecified for Maps and will throw a 489 | `BadMethodCallException`. 490 | 491 | #### Set 492 | 493 | A Set is a simple extension of Collection that enforces the following rules 494 | 495 | - A Set can only have unique values (of the same type) 496 | - A Set doesn't care about the keys, its the values that are important 497 | - Operations on a Set return a Set 498 | 499 |
500 | use Monad/Set;
501 | 
502 | $setA = new Set(['a','b','c']);
503 | $setB = new Set(['a','c']);
504 | 
505 | $setC = $setA->vIntersect($setB);
506 | $setD = $setA->vUnion($setB);
507 | $setE = $setA->vDiff($setB);
508 | 
509 | 510 | As with a Collection, you can specify an empty construction value and a second type value. 511 | You can also append to a Set to return a new Set. 512 | 513 | The `kUnion`, `kIntersect` and `kDiff` methods are unspecified for Maps and will throw a 514 | `BadMethodCallException`. 515 | 516 | All other Collection methods are supported, returning Sets where expected. 517 | 518 | The ->vIntersect(), ->vUnion() and ->diff() methods all accept a second equality function 519 | parameter as per Collection. However, for Set, if none is provided it will default 520 | to using a sane default, that is to casting non stringifiable values to a serialized 521 | hash of the value and using that for comparison. Supply your own functions if this 522 | default is inadequate for your purposes. 523 | 524 | ## Further documentation 525 | 526 | Please note that what you are seeing of this documentation displayed on Github is 527 | always the latest dev-master. The features it describes may not be in a released version 528 | yet. Please check the documentation of the version you Compose in, or download. 529 | 530 | [Test Contract](https://github.com/chippyash/Monad/blob/master/docs/Test-Contract.md) in the docs directory. 531 | 532 | Check out [ZF4 Packages](http://zf4.biz/packages?utm_source=github&utm_medium=web&utm_campaign=blinks&utm_content=monad) for more packages 533 | 534 | ### UML 535 | 536 | ![class diagram](https://github.com/chippyash/Monad/blob/master/docs/monad-classes.png) 537 | 538 | ## Changing the library 539 | 540 | 1. fork it 541 | 2. write the test 542 | 3. amend it 543 | 4. do a pull request 544 | 545 | Found a bug you can't figure out? 546 | 547 | 1. fork it 548 | 2. write the test 549 | 3. do a pull request 550 | 551 | NB. Make sure you rebase to HEAD before your pull request 552 | 553 | Or - raise an issue ticket. 554 | 555 | ## Where? 556 | 557 | The library is hosted at [Github](https://github.com/chippyash/Monad). It is 558 | available at [Packagist.org](https://packagist.org/packages/chippyash/monad) 559 | 560 | ### Installation 561 | 562 | Install [Composer](https://getcomposer.org/) 563 | 564 | #### For production 565 | 566 |
567 |     "chippyash/monad": "~3.0"
568 | 
569 | 570 | Or to use the latest, possibly unstable version: 571 | 572 |
573 |     "chippyash/monad": "dev-master"
574 | 
575 | 576 | 577 | #### For development 578 | 579 | Clone this repo, and then run Composer in local repo root to pull in dependencies 580 | 581 |
582 |     git clone git@github.com:chippyash/Monad.git Monad
583 |     cd Monad
584 |     composer install
585 | 
586 | 587 | To run the tests: 588 | 589 |
590 |     cd Monad
591 |     vendor/bin/phpunit -c test/phpunit.xml test/
592 | 
593 | 594 | ##### Debugging 595 | 596 | Because PHP doesn't really support functional programming at it's core level, debugging 597 | using XDebug etc becomes a nested mess. Some things I've found helpful: 598 | 599 | - isolate your tests, at least at the initial stage. If you get a problem, create a test 600 | that does one thing - the thing you are trying to debug. Use that as your start point. 601 | 602 | - be mindful of value() and flatten(), the former gets the immediate Monad value, the 603 | latter gives you a PHP fundamental. 604 | 605 | - when constructing FMatches, ensure the value contained in the FMatch conforms to the 606 | type you are expecting. Remember, FMatch returns a FMatch with a value. And yes, I've 607 | tripped up on this myself. 608 | 609 | - keep running the other tests. Seems simple, but in the headlong pursuit of your 610 | single objective, it's easy to forget that the library is interdependent (and will 611 | become increasingly so as we are able to wrap new functionality back into the original 612 | code. e.g. Collection is dependent on FMatch: when FFor is implemented, FMatch will change.) 613 | Run the whole test suite on a regular basis. That way you catch anything that has broken 614 | upstream functionality. This library will be complete when it, as far as possible, 615 | expresses itself in terms of itself! 616 | 617 | - the tests that are in place are there for a good reason: open an issue if you think 618 | they are wrong headed, misguided etc 619 | 620 | ## License 621 | 622 | This software library is released under the [BSD 3 Clause license](https://opensource.org/licenses/BSD-3-Clause) 623 | 624 | This software library is Copyright (c) 2015,2021 Ashley Kitson, UK 625 | 626 | This software library contains code items that are derived from other works: 627 | 628 | None of the contained code items breaks the overriding license, or vice versa, as 629 | far as I can tell. If at all unsure, please seek appropriate advice. 630 | 631 | If the original copyright owners of the derived code items object to this inclusion, please contact the author. 632 | 633 | ## Thanks 634 | 635 | I didn't do this by myself. I'm deeply indebted to those that trod the path before me. 636 | 637 | The following have done work on which this library is based: 638 | 639 | [Sean Crystal](https://github.com/spiralout/Phonads) 640 | 641 | [Anthony Ferrara](http://blog.ircmaxell.com/2013/07/taking-monads-to-oop-php.html) 642 | 643 | [Johannes Schmidt](https://github.com/schmittjoh/php-option) 644 | 645 | ## History 646 | 647 | V1.0.0 Initial Release 648 | 649 | V1.1.0 Added FTry 650 | 651 | V1.2.0 Added Collection 652 | 653 | V1.2.1 fixes on Collection 654 | 655 | V1.2.2 add sort order for vUnion method 656 | 657 | V1.2.3 allow descendent monadic types 658 | 659 | V1.2.4 add each() method to Collection 660 | 661 | V1.2.5 move from coveralls to codeclimate 662 | 663 | V1.2.6 Add link to packages 664 | 665 | V1.2.7 Code cleanup - verify PHP7 compatibility 666 | 667 | V1.3.0 Collection is immutable. Added MutableCollection for convenience 668 | 669 | V1.4.0 Add Map class - enforced string type keys for collection members 670 | 671 | Add convenience method append() to Collection === ->vUnion(new Collection([$nValue])) 672 | 673 | V1.5.0 Add Set class 674 | 675 | V1.5.1 Add additional checking for Maps and Sets. diff() and intersect() 676 | deprecated, use the kDiff(), uDiff, kIntersect() and uIntersect() methods; 677 | 678 | V1.5.2 build script update 679 | 680 | V1.5.3 update composer - forced by packagist composer.json format change 681 | 682 | V2.0.0 BC Break. Support for PHP <5.6 withdrawn 683 | 684 | V2.0.1 fixes for PHP >= 7.1 685 | 686 | V2.1.0 Change of license from GPL V3 to BSD 3 Clause 687 | 688 | V2.1.1 Flatten value in the bind method of FTry, so in case the binded function 689 | returns a Success, we do not end up with nested Success. PR by [josselinauguste](https://github.com/josselinauguste) 690 | 691 | V3.0.0 BC Break. Support for PHP <8 withdrawn. Match renamed to FMatch. -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd ~/Projects/chippyash/source/Monad 3 | vendor/phpunit/phpunit/phpunit -c test/phpunit.xml --testdox-html contract.html test/ 4 | tdconv -t "Chippyash Monad" contract.html docs/Test-Contract.md 5 | rm contract.html 6 | 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chippyash/monad", 3 | "description": "Functional programming Monad support", 4 | "homepage": "http://zf4.biz/packages?utm_source=packagist&utm_medium=web&utm_campaign=blinks&utm_content=monad", 5 | "license": "BSD-3-Clause", 6 | "keywords": [ 7 | "monad", 8 | "identity", 9 | "option", 10 | "match", 11 | "functional", 12 | "try", 13 | "collection" 14 | ], 15 | "authors": [ 16 | { 17 | "name": "Ashley Kitson", 18 | "email": "ashley@zf4.biz" 19 | } 20 | ], 21 | "require": { 22 | "php": ">=8.0" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "~9.5", 26 | "mikey179/vfsstream": "1.6.*" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Monad\\": "src/chippyash/Monad" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/Monad Classes.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | title Monad classes 3 | 4 | interface Monadic { 5 | value():mixed 6 | flatten():mixed 7 | bind(Closure:function, array:args = []):monadic 8 | {static} create(mixed:value):monadic 9 | } 10 | 11 | abstract class Monad { 12 | #mixed:value 13 | __invoke():mixed 14 | } 15 | 16 | class Identity { 17 | __construct(mixed:value) 18 | } 19 | 20 | abstract class Option { 21 | {static} create(mixed:value, mixed:noneValue=null):Some|None 22 | getOrElse(mixed:elseValue):mixed 23 | } 24 | 25 | class None { 26 | __construct(mixed:value = null) 27 | value():RuntimeException 28 | bind(Closure:function, array:args = []):None 29 | {static} create(mixed:value):None 30 | } 31 | 32 | class Some { 33 | __construct(mixed:value) 34 | bind(Closure:function, array:args = [], mixed:noneValue=null):Some|None 35 | } 36 | 37 | class FMatch { 38 | #mixed:value 39 | #bool:isMatched 40 | __construct(mixed:value, bool:isMatched) 41 | {static} on(mixed:value):Match 42 | __call(string:method, array:args=[]):Match 43 | any(Closure:function = null, array:$args = []):Match 44 | } 45 | 46 | abstract class FTry { 47 | {static} with(mixed:value):Success|Failure 48 | getOrElse(mixed:elseValue):mixed 49 | {abstract} isSuccess():bool 50 | } 51 | 52 | class Success { 53 | __construct(mixed:value) 54 | bind(Closure:function, array:args = []):Success|Failure 55 | isSuccess():bool(true) 56 | } 57 | 58 | class Failure { 59 | __construct(Exception:value) 60 | create(mixed:value = null):Failure 61 | bind(Closure:function, array:args = []):Failure 62 | pass():!!Exception 63 | isSuccess():bool(false) 64 | } 65 | 66 | class Collection <> { 67 | __construct(array:value = [], string:type = null) 68 | diff(Collection:other, Closure:function = null):Collection 69 | intersect(Collection:other, Closure:function = null):Collection 70 | vUnion(Collection:other):Collection 71 | kUnion(Collection:other):Collection 72 | head():Collection 73 | tail():Collection 74 | each():Collection 75 | } 76 | 77 | Monadic <-- Monad 78 | Monadic <-- FMatch 79 | Monadic <-- Collection 80 | Monad <-- Identity 81 | Monad <-- Option 82 | Option <-- None 83 | Option <-- Some 84 | Monad <-- FTry 85 | FTry <-- Success 86 | FTry <-- Failure 87 | 88 | @enduml -------------------------------------------------------------------------------- /docs/Test-Contract.md: -------------------------------------------------------------------------------- 1 | # Chippyash Monad 2 | 3 | ## Monad\test\Failure 4 | 5 | * ✓ Can construct if value is exception 6 | * ✓ Create creates a failure if value is exception 7 | * ✓ Create creates a failure if value is not an exception 8 | * ✓ Bind returns failure with same value 9 | * ✓ Calling pass will throw an exception 10 | * ✓ Calling is success will return false 11 | 12 | ## Monad\test\Success 13 | 14 | * ✓ You can construct a success if you have a value for it 15 | * ✓ You cannot construct a success with an exception 16 | * ✓ Binding a success with something that does not throw an exception will return success 17 | * ✓ Binding a success with something that throws an exception will return a failure 18 | * ✓ You can get a value from a success 19 | * ✓ Calling is success will return true 20 | 21 | ## Monad\Test\Collection 22 | 23 | * ✓ You can construct a collection with a non empty array 24 | * ✓ You cannot construct a collection with null values 25 | * ✓ You cannot construct a collection with an empty array and no type specified 26 | * ✓ You can construct an empty collection if you pass a type 27 | * ✓ When constructing a collection you must have same type values 28 | * ✓ Constructing a collection with dissimilar types will cause an exception 29 | * ✓ You can create a collection 30 | * ✓ The value of a collection is the collection 31 | * ✓ You can bind a function to the entire collection and return a collection 32 | * ✓ You can bind a function to each member of the collection and return a collection 33 | * ✓ You can count the items in the collection 34 | * ✓ You can get an iterator for a collection 35 | * ✓ You cannot unset a collection member 36 | * ✓ You cannot set a collection member 37 | * ✓ You can get a collection member as an array offset 38 | * ✓ You can test if a collection member exists as an array offset 39 | * ✓ You can create a collection of collections 40 | * ✓ Flattening a collection of collections will return a collection 41 | * ✓ You can get the difference of values between two collections 42 | * ✓ You can get the difference of keys between two collections 43 | * ✓ You can chain v diff methods to act on arbitrary numbers of collections 44 | * ✓ You can chain k diff methods to act on arbitrary numbers of collections 45 | * ✓ You can supply an optional comparator function to v diff method 46 | * ✓ You can supply an optional comparator function to k diff method 47 | * ✓ You can get the intersection of two collections by value 48 | * ✓ You can get the intersection of two collections by key 49 | * ✓ You can chain value intersect methods to act on arbitrary numbers of collections 50 | * ✓ You can chain key intersect methods to act on arbitrary numbers of collections 51 | * ✓ You can supply an optional comparator function to the value intersect method 52 | * ✓ You can supply an optional comparator function to the key intersect method 53 | * ✓ You can get the union of values of two collections 54 | * ✓ You can chain the union of values of two collections 55 | * ✓ You can get the union of keys of two collections 56 | * ✓ You can chain the union of keys of two collections 57 | * ✓ Performing a value union with dissimilar collections will throw an exception 58 | * ✓ Performing a key union with dissimilar collections will throw an exception 59 | * ✓ The head of a collection is its first member 60 | * ✓ The tail of a collection is all but its first member 61 | * ✓ You can filter a collection with a closure 62 | * ✓ You can reduce a collection to a single value with a closure 63 | * ✓ You can reference a collection as though it was an array 64 | * ✓ Value method proxies to collection get array copy method 65 | * ✓ You can flip a collection 66 | * ✓ Appending to a collection returns a new collection 67 | 68 | ## Monad\Test\FTry 69 | 70 | * ✓ Creating an f try with a non exception will return a success 71 | * ✓ Creating an f try with an exception will return a failure 72 | * ✓ The with method proxies to create 73 | * ✓ Get or else will return f try value if option is a success 74 | * ✓ Get or else will return else value if f try is a failure 75 | 76 | ## Monad\Test\Identity 77 | 78 | * ✓ You can create an identity statically 79 | * ✓ Creating an identity with an identity parameter will return the parameter 80 | * ✓ Creating an identity with a non identity parameter will return an identity containing the parameter as value 81 | * ✓ You can bind a function on an identity 82 | * ✓ Bind can take optional additional parameters 83 | * ✓ You can chain bind methods together 84 | * ✓ Binding on an identity with a closure value will evaluate the value 85 | * ✓ You can flatten an identity value to its base type 86 | 87 | ## Monad\Test\Map 88 | 89 | * ✓ You cannot create an empty map 90 | * ✓ Maps require string keys 91 | * ✓ You can construct an empty map if you pass a type 92 | * ✓ Appending to a map returns a new map 93 | * ✓ Appending to a map with unhashed values throws an exception 94 | * ✓ Vunion method is not supported for maps 95 | * ✓ Vintersect method is not supported for maps 96 | * ✓ Vdiff method is not supported for maps 97 | 98 | ## Monad\Test\FMatch 99 | 100 | * ✓ Construction requires a value to match against 101 | * ✓ You can construct via static on factory method 102 | * ✓ You can match on native php types 103 | * ✓ Matching will return set by callable parameter if matched 104 | * ✓ Matching will return set by non callable parameter if matched 105 | * ✓ Failing to match will return new match object with same value as original 106 | * ✓ You can match on a class name 107 | * ✓ Failing to match on class name will return new match object with same value as original 108 | * ✓ You can chain match tests 109 | * ✓ You can chain match tests and bind a function on successful match 110 | * ✓ Binding a match will return a match 111 | * ✓ Match on any method will match anything 112 | * ✓ Match on any method can accept optional function and arguments 113 | * ✓ You can nest matches 114 | * ✓ You can test for equality 115 | 116 | ## Monad\Test\Monad 117 | 118 | * ✓ You can return a value when monad created with simple value 119 | * ✓ You can return a value when monad created with monadic value 120 | * ✓ You can use a closure for value 121 | * ✓ Flatten will return base type 122 | * ✓ You can bind a closure on a monad to create a new monad of the same type 123 | * ✓ Bind can take optional additional parameters 124 | * ✓ Magic invoke proxies to bind method if passed a closure 125 | * ✓ Magic invoke proxies to value method if passed no parameters 126 | * ✓ Calling magic invoke will throw exception if no method is executable 127 | * ✓ You cannot create an abstract monad statically 128 | 129 | ## Monad\Test\MutableCollection 130 | 131 | * ✓ You can unset a mutable collection member 132 | * ✓ You can set a mutable collection member 133 | * ✓ Calling one of the underlaying collection methods returns a mutable collection 134 | 135 | ## Monad\Test\None 136 | 137 | * ✓ You can construct a none 138 | * ✓ You can construct a none with a parameter and it will still be none 139 | * ✓ Create will return a none 140 | * ✓ Binding a none returns a none 141 | * ✓ Calling get on a none throws a runtime exception 142 | 143 | ## Monad\Test\Some 144 | 145 | * ✓ You can construct a some if you have a value for it 146 | * ✓ You cannot construct a some with no value 147 | * ✓ You can get a value from a some 148 | * ✓ Binding on a some may return a some or a none 149 | * ✓ Binding on a some takes a third nonetest value 150 | 151 | ## Monad\Test\Option 152 | 153 | * ✓ You cannot construct an option directly 154 | * ✓ Creating with a value returns a some 155 | * ✓ Creating with no value or null returns a none 156 | * ✓ You can replace none test by calling create with additional parameter 157 | * ✓ Get or else will return option value if option is a some 158 | * ✓ Get or else will return else value if option is a none 159 | 160 | ## Monad\Test\Set 161 | 162 | * ✓ Creating an empty set with no type hint will throw an exception 163 | * ✓ Passing in unique values at construction will create a set 164 | * ✓ Passing in non unique values at construction will create a set with unique values 165 | * ✓ You can create sets of objects 166 | * ✓ You can create sets of resources 167 | * ✓ Value intersection will produce a set 168 | * ✓ Value union will produce a set 169 | * ✓ Diff will produce a set 170 | * ✓ You can bind a function to the entire set and return a set 171 | * ✓ Kintersect method is not supported for sets 172 | * ✓ Kunion method is not supported for sets 173 | * ✓ Kdiff method is not supported for sets 174 | 175 | 176 | Generated by [chippyash/testdox-converter](https://github.com/chippyash/Testdox-Converter) -------------------------------------------------------------------------------- /docs/monad-classes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chippyash/Monad/5954e8f4403150377b393925a73979821456afb2/docs/monad-classes.png -------------------------------------------------------------------------------- /phpunit.travis.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 10 | 11 | 12 | 13 | ./src 14 | 15 | 16 | ./docs 17 | ./examples 18 | ./test 19 | ./vendor 20 | ./src/chippyash/Monad/Interfaces 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ./test/src/ 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/chippyash/Monad/CallFunctionAble.php: -------------------------------------------------------------------------------- 1 | bind($function, $args); 30 | } 31 | 32 | $val = ($value instanceof \Closure ? $value() : $value); 33 | array_unshift($args, $val); 34 | 35 | return call_user_func_array($function, $args); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/chippyash/Monad/Collection.php: -------------------------------------------------------------------------------- 1 | string(function () use ($type) { 45 | return $this->setType($type); 46 | }) 47 | ->null(function () use ($value) { 48 | return $this->setTypeFromValue($value); 49 | 50 | }); 51 | 52 | /** @noinspection PhpUndefinedMethodInspection */ 53 | parent::__construct( 54 | FMatch::on($setType->value()) 55 | ->Monad_FTry_Success(function () use ($value) { 56 | return $this->setValue($value); 57 | }) 58 | ->Monad_FTry_Failure(function () use ($setType) { 59 | return $setType->value(); 60 | }) 61 | ->value() 62 | ->pass() 63 | ->value() 64 | ); 65 | } 66 | 67 | /** 68 | * Monadic Interface 69 | * 70 | * @param array $value 71 | * 72 | * @return Collection 73 | */ 74 | public static function create($value): Collection 75 | { 76 | return new static($value); 77 | } 78 | 79 | /** 80 | * Bind monad with function. Function is in form f($value){} 81 | * You can pass additional parameters in the $args array in which case your 82 | * function should be in the form f($value, $arg1, ..., $argN) 83 | * 84 | * @param \Closure $function 85 | * @param array $args additional arguments to pass to function 86 | * 87 | * @return Collection 88 | */ 89 | public function bind(\Closure $function, array $args = []) 90 | { 91 | $res = $this->callFunction($function, $this, $args); 92 | 93 | return ($res instanceof Collection ? $res : new static(is_array($res)? $res :[$res])); 94 | } 95 | 96 | /** 97 | * For each item in the collection apply the function and return a new collection 98 | * 99 | * @param callable|\Closure $function 100 | * @param array $args 101 | * 102 | * @return Collection 103 | */ 104 | public function each(\Closure $function, array $args = []): Collection 105 | { 106 | $content = $this->getArrayCopy(); 107 | 108 | return new static( 109 | \array_combine( 110 | \array_keys($content), 111 | \array_map( 112 | function ($value) use ($function, $args) { 113 | return $this->callFunction($function, $value, $args); 114 | }, 115 | \array_values($content) 116 | ) 117 | ) 118 | ); 119 | } 120 | 121 | /** 122 | * Reduce the collection using closure to a single value 123 | * 124 | * @see array_reduce 125 | * 126 | * @param \Closure $function 127 | * @param mixed $initial optional initial value 128 | * 129 | * @return mixed 130 | */ 131 | public function reduce(\Closure $function, $initial = null) 132 | { 133 | return \array_reduce($this->getArrayCopy(), $function, $initial); 134 | } 135 | 136 | /** 137 | * Filter collection using closure to return another Collection 138 | * 139 | * @see array_filter 140 | * 141 | * @param \Closure $function 142 | * 143 | * @return Collection 144 | */ 145 | public function filter(\Closure $function): Collection 146 | { 147 | return new static(\array_filter($this->getArrayCopy(), $function)); 148 | } 149 | 150 | /** 151 | * Flip keys and values. 152 | * Returns new Collection 153 | * 154 | * @return Collection 155 | */ 156 | public function flip(): Collection 157 | { 158 | return new static(\array_flip($this->getArrayCopy())); 159 | } 160 | 161 | /** 162 | * Monadic Interface 163 | * Return this collection as value 164 | * 165 | * @return Collection 166 | */ 167 | public function value(): Collection 168 | { 169 | return $this; 170 | } 171 | 172 | /** 173 | * Return value of Monad as a base type. 174 | * 175 | * If value === \Closure, will evaluate the function and return it's value 176 | * If value === \Monadic, will recurse 177 | * 178 | * @return Collection 179 | */ 180 | public function flatten(): Collection 181 | { 182 | $ret = []; 183 | foreach ($this as $key => $value) { 184 | $ret[$key] = FMatch::on($value) 185 | ->Closure(function ($v) { 186 | return $v(); 187 | }) 188 | ->Monad_Monadic(function ($v) { 189 | return $v->flatten(); 190 | }) 191 | ->any() 192 | ->flatten(); 193 | } 194 | 195 | return new static($ret); 196 | } 197 | 198 | /** 199 | * Return collection as an array 200 | * @return array 201 | */ 202 | public function toArray(): array 203 | { 204 | return $this->getArrayCopy(); 205 | } 206 | 207 | /** 208 | * ArrayAccess Interface 209 | * 210 | * @param mixed $offset 211 | * @param mixed $value 212 | * 213 | * @throw \BadMethodCallException 214 | */ 215 | public function offsetSet($offset, $value) 216 | { 217 | throw new \BadMethodCallException('Cannot set on an immutable Collection'); 218 | } 219 | 220 | /** 221 | * ArrayAccess Interface 222 | * 223 | * @param mixed $offset 224 | * 225 | * @throw \BadMethodCallException 226 | */ 227 | public function offsetUnset($offset) 228 | { 229 | throw new \BadMethodCallException('Cannot unset an immutable Collection value'); 230 | } 231 | 232 | /** 233 | * @deprecated Use vDiff 234 | * 235 | * @param Collection $other 236 | * @param \Closure $function optional function to compare values 237 | * 238 | * @return Collection 239 | */ 240 | public function diff(Collection $other, \Closure $function = null): Collection 241 | { 242 | return $this->vDiff($other, $function); 243 | } 244 | 245 | /** 246 | * Compares this collection against another collection using its values for 247 | * comparison and returns a new Collection with the values in this collection 248 | * that are not present in the other collection. 249 | * 250 | * Note that keys are preserved 251 | * 252 | * If the optional comparison function is supplied it must have signature 253 | * function(mixed $a, mixed $b){}. The comparison function must return an integer 254 | * less than, equal to, or greater than zero if the first argument is considered 255 | * to be respectively less than, equal to, or greater than the second. 256 | * 257 | * @param Collection $other 258 | * @param \Closure $function optional function to compare values 259 | * 260 | * @return Collection 261 | */ 262 | public function vDiff(Collection $other, \Closure $function = null) 263 | { 264 | if (is_null($function)) { 265 | return new static(\array_diff($this->getArrayCopy(), $other->getArrayCopy()), $this->type); 266 | } 267 | 268 | return new static(\array_udiff($this->getArrayCopy(), $other->getArrayCopy(), $function), $this->type); 269 | } 270 | 271 | /** 272 | * Compares this collection against another collection using its keys for 273 | * comparison and returns a new Collection with the values in this collection 274 | * that are not present in the other collection. 275 | * 276 | * Note that keys are preserved 277 | * 278 | * If the optional comparison function is supplied it must have signature 279 | * function(mixed $a, mixed $b){}. The comparison function must return an integer 280 | * less than, equal to, or greater than zero if the first argument is considered 281 | * to be respectively less than, equal to, or greater than the second. 282 | * 283 | * @param Collection $other 284 | * @param \Closure $function optional function to compare values 285 | * 286 | * @return Collection 287 | */ 288 | public function kDiff(Collection $other, \Closure $function = null) 289 | { 290 | if (is_null($function)) { 291 | return new static(\array_diff_key($this->getArrayCopy(), $other->getArrayCopy()), $this->type); 292 | } 293 | 294 | return new static(\array_diff_ukey($this->getArrayCopy(), $other->getArrayCopy(), $function), $this->type); 295 | } 296 | 297 | 298 | /** 299 | * @deprecated - use vIntersect 300 | * 301 | * @param Collection $other 302 | * @param \Closure $function 303 | * @return Collection 304 | */ 305 | public function intersect(Collection $other, \Closure $function = null) 306 | { 307 | return $this->vIntersect($other, $function); 308 | } 309 | 310 | /** 311 | * Returns a Collection containing all the values of this Collection that are present 312 | * in the other Collection. Note that keys are preserved 313 | * 314 | * If the optional comparison function is supplied it must have signature 315 | * function(mixed $a, mixed $b){}. The comparison function must return an integer 316 | * less than, equal to, or greater than zero if the first argument is considered 317 | * to be respectively less than, equal to, or greater than the second. 318 | * 319 | * @param Collection $other 320 | * @param callable|\Closure $function Optional function to compare values 321 | * 322 | * @return Collection 323 | */ 324 | public function vIntersect(Collection $other, \Closure $function = null) 325 | { 326 | if (is_null($function)) { 327 | return new static(\array_intersect($this->getArrayCopy(), $other->getArrayCopy()), $this->type); 328 | } 329 | 330 | return new static(\array_uintersect($this->getArrayCopy(), $other->getArrayCopy(), $function), $this->type); 331 | } 332 | 333 | /** 334 | * Returns a Collection containing all the values of this Collection that are present 335 | * in the other Collection. Keys are used for comparison 336 | * 337 | * If the optional comparison function is supplied it must have signature 338 | * function(mixed $a, mixed $b){}. The comparison function must return an integer 339 | * less than, equal to, or greater than zero if the first argument is considered 340 | * to be respectively less than, equal to, or greater than the second. 341 | * 342 | * @param Collection $other 343 | * @param \Closure $function Optional function to compare values 344 | * 345 | * @return Collection 346 | */ 347 | public function kIntersect(Collection $other, \Closure $function = null) 348 | { 349 | return new static( 350 | FMatch::on(Option::create($function)) 351 | ->Monad_Option_Some(function () use ($other, $function) { 352 | return \array_intersect_ukey($this->getArrayCopy(), $other->getArrayCopy(), $function); 353 | }) 354 | ->Monad_Option_None(function () use ($other) { 355 | return \array_intersect_key($this->getArrayCopy(), $other->getArrayCopy()); 356 | }) 357 | ->value(), 358 | $this->type 359 | ); 360 | } 361 | 362 | /** 363 | * Return a Collection that is the union of the values of this Collection 364 | * and the other Collection. Note that keys may be discarded and new ones set 365 | * 366 | * @param Collection $other 367 | * @param int $sortOrder arrayUnique sort order. one of SORT_... 368 | * 369 | * @return Collection 370 | */ 371 | public function vUnion(Collection $other, $sortOrder = SORT_REGULAR) 372 | { 373 | return new static( 374 | \array_unique( 375 | \array_merge($this->getArrayCopy(), $other->getArrayCopy()), 376 | $sortOrder 377 | ) 378 | , $this->type 379 | ); 380 | } 381 | 382 | /** 383 | * Return a Collection that is the union of the values of this Collection 384 | * and the other Collection using the keys for comparison 385 | * 386 | * @param Collection $other 387 | * 388 | * @return Collection 389 | */ 390 | public function kUnion(Collection $other) 391 | { 392 | return new static($this->getArrayCopy() + $other->getArrayCopy(), $this->type); 393 | } 394 | 395 | /** 396 | * Return a Collection with the first element of this Collection as its only 397 | * member 398 | * 399 | * @return Collection 400 | */ 401 | public function head(): Collection 402 | { 403 | return new static(array_slice($this->getArrayCopy(), 0, 1)); 404 | } 405 | 406 | /** 407 | * Return a Collection with all but the first member of this Collection 408 | * 409 | * @return Collection 410 | */ 411 | public function tail(): Collection 412 | { 413 | return new static(array_slice($this->getArrayCopy(), 1)); 414 | } 415 | 416 | /** 417 | * Append value and return a new collection 418 | * NB this uses vUnion 419 | * 420 | * Value will be forced into an array if not already one 421 | * 422 | * @param mixed $value 423 | * 424 | * @return Collection 425 | */ 426 | public function append($value) 427 | { 428 | $nValue = (is_array($value) ? $value : [$value]); 429 | return $this->vUnion(new static($nValue)); 430 | } 431 | 432 | /** 433 | * @param string $type 434 | * 435 | * @return FTry 436 | */ 437 | protected function setType($type): FTry 438 | { 439 | return FMatch::on($type) 440 | ->string(function ($type) { 441 | $this->type = $type; 442 | return FTry::with($type); 443 | }) 444 | ->any(function () { 445 | return FTry::with(function () { 446 | return new \RuntimeException('Type must be specified by string'); 447 | }); 448 | }) 449 | ->value(); 450 | } 451 | 452 | /** 453 | * @param array $value 454 | * 455 | * @return FTry 456 | */ 457 | protected function setTypeFromValue(array $value): FTry 458 | { 459 | //required to be defined as a var so it can be called in next statement 460 | $basicTest = function () use ($value) { 461 | if (count($value) > 0) { 462 | return array_values($value)[0]; 463 | } 464 | 465 | return null; 466 | }; 467 | 468 | //@var Option 469 | //firstValue is used twice below 470 | $firstValue = Option::create($basicTest()); 471 | 472 | //@var Option 473 | //NB - this separate declaration is not needed, but is provided only to 474 | // allow some separation between what can become a complex match pattern 475 | $type = FMatch::on($firstValue) 476 | ->Monad_Option_Some( 477 | function ($option) { 478 | return Option::create(gettype($option->value())); 479 | } 480 | ) 481 | ->Monad_Option_None(function () { 482 | return new None(); 483 | }) 484 | ->value(); 485 | 486 | //@var Option 487 | //MatchLegalType requires to be defined separately as it is used twice 488 | //in the next statement 489 | $matchLegalType = FTry::with( 490 | FMatch::on($type) 491 | ->Monad_Option_None() 492 | ->Monad_Option_Some( 493 | function ($v) use ($firstValue) { 494 | FMatch::on($v->value()) 495 | ->test('object', function ($v) use ($firstValue) { 496 | $this->setType(get_class($firstValue->value())); 497 | return new Some($v); 498 | }) 499 | ->test('string', function ($v) { 500 | $this->setType($v); 501 | return new Some($v); 502 | }) 503 | ->test('integer', function ($v) { 504 | $this->setType($v); 505 | return new Some($v); 506 | }) 507 | ->test('double', function ($v) { 508 | $this->setType($v); 509 | return new Some($v); 510 | }) 511 | ->test('boolean', function ($v) { 512 | $this->setType($v); 513 | return new Some($v); 514 | }) 515 | ->test('resource', function ($v) { 516 | $this->setType($v); 517 | return new Some($v); 518 | }) 519 | ->any(function () { 520 | return new None(); 521 | 522 | }); 523 | } 524 | ) 525 | ->any(function () { 526 | return new None(); 527 | }) 528 | ); 529 | 530 | return FTry::with(function () use ($matchLegalType) { 531 | return $matchLegalType->value(); 532 | }); 533 | } 534 | 535 | /** 536 | * @param array $values 537 | * 538 | * @return FTry 539 | */ 540 | protected function setValue(array $values): FTry 541 | { 542 | foreach ($values as $key => $value) { 543 | if (($this->type !== gettype($value)) && (!$value instanceof $this->type)) { 544 | return new Failure(new \RuntimeException("Value {$key} is not a {$this->type}")); 545 | } 546 | } 547 | 548 | return new Success($values); 549 | } 550 | 551 | /** 552 | * Call function on value 553 | * 554 | * @param \Closure $function 555 | * @param mixed $value 556 | * @param array $args additional arguments to pass to function 557 | * 558 | * @return mixed 559 | */ 560 | protected function callFunction(\Closure $function, $value, array $args = []) 561 | { 562 | if ($value instanceof Monadic && !$value instanceof Collection) { 563 | return $value->bind($function, $args); 564 | } 565 | 566 | $val = ($value instanceof \Closure ? $value() : $value); 567 | array_unshift($args, $val); 568 | 569 | return call_user_func_array($function, $args); 570 | } 571 | } 572 | -------------------------------------------------------------------------------- /src/chippyash/Monad/FMatch.php: -------------------------------------------------------------------------------- 1 | value = $value; 30 | $this->isMatched = $isMatched; 31 | } 32 | 33 | /** 34 | * Syntactic proxy for create() 35 | * @param mixed $value 36 | * 37 | * @return FMatch 38 | *@see create() 39 | * 40 | */ 41 | public static function on($value): FMatch 42 | { 43 | return static::create($value); 44 | } 45 | 46 | /** 47 | * Magic unknown method that proxies native type and class type matching 48 | * 49 | * @param string $method 50 | * @param array $args If args[0] set, then use as concrete value or function to 51 | * bind onto current value 52 | * 53 | * @return FMatch 54 | */ 55 | public function __call($method, $args): FMatch 56 | { 57 | if ($this->isMatched) { 58 | return new static($this->value, $this->isMatched); 59 | } 60 | 61 | if ($this->matchOnNative($method) || $this->matchOnClassName($method)) { 62 | if (isset($args[0])) { 63 | if (is_callable($args[0]) && !$args[0] instanceof Monadic) { 64 | return new static($args[0]($this->value), true); 65 | } 66 | 67 | return new static($args[0], true); 68 | } 69 | 70 | return new static($this->value, true); 71 | } 72 | 73 | return new static($this->value); 74 | } 75 | 76 | /** 77 | * Match anything. Usually called last in Match chain 78 | * 79 | * @param callable|\Closure $function 80 | * @param array $args Optional additional arguments to function 81 | * @return FMatch 82 | */ 83 | public function any(\Closure $function = null, array $args = []): FMatch 84 | { 85 | if ($this->isMatched) { 86 | return new static($this->value, $this->isMatched); 87 | } 88 | 89 | if (is_null($function)) { 90 | return new static($this->value, true); 91 | } 92 | 93 | return new static($this->callFunction($function, $this->value, $args), true); 94 | } 95 | 96 | /** 97 | * Test current value for exact equality to the test value 98 | * 99 | * @param mixed $test Value to test against 100 | * 101 | * @param \Closure $function Function that is used if test is true 102 | * @param array $args Optional additional arguments to function 103 | * 104 | * @return FMatch 105 | */ 106 | public function test($test, \Closure $function = null, array $args = []): FMatch 107 | { 108 | if ($this->isMatched) { 109 | return new static($this->value, $this->isMatched); 110 | } 111 | if ($this->value === $test) { 112 | if (is_null($function)) { 113 | return new static($this->value, true); 114 | } 115 | 116 | return new static($this->callFunction($function, $this->value, $args), true); 117 | } 118 | 119 | return new static($this->value()); 120 | } 121 | 122 | 123 | /** 124 | * Return value of Monad 125 | * Does not manipulate the value in any way 126 | * 127 | * @return mixed 128 | */ 129 | public function value() 130 | { 131 | return $this->value; 132 | } 133 | 134 | /** 135 | * Bind match value with function. 136 | * 137 | * Function is in form f($value) {} 138 | * 139 | * You can pass additional parameters in the $args array in which case your 140 | * function should be in the form f($value, $arg1, ..., $argN) {} 141 | * 142 | * @param \Closure $function 143 | * @param array $args additional arguments to pass to function 144 | * 145 | * @return FMatch 146 | */ 147 | public function bind(\Closure $function, array $args = []): FMatch 148 | { 149 | return new static($this->callFunction($function, $this->value, $args), $this->isMatched); 150 | } 151 | 152 | /** 153 | * Static factory creator for the Monad 154 | * 155 | * @param mixed $value 156 | * 157 | * @return FMatch 158 | */ 159 | public static function create($value): FMatch 160 | { 161 | return new static($value); 162 | } 163 | 164 | /** 165 | * @param $name 166 | * @return bool 167 | */ 168 | protected function matchOnNative($name): bool 169 | { 170 | switch(strtolower($name)) { 171 | case 'string': 172 | return is_string($this->value); 173 | case 'integer': 174 | case 'int': 175 | case 'long': 176 | return is_int($this->value); 177 | case 'float': 178 | case 'double': 179 | case 'real': 180 | return is_double($this->value); 181 | case 'null': 182 | return is_null($this->value); 183 | case 'array': 184 | return is_array($this->value); 185 | case 'boolean': 186 | case 'bool': 187 | return is_bool($this->value); 188 | case 'callable': 189 | case 'function': 190 | case 'closure': 191 | return is_callable($this->value); 192 | case 'file': 193 | return is_file($this->value); 194 | case 'dir': 195 | case 'directory': 196 | return is_dir($this->value); 197 | case 'object': 198 | return is_object($this->value); 199 | case 'scalar': 200 | return is_scalar($this->value); 201 | case 'numeric': 202 | return is_numeric($this->value); 203 | case 'resource': 204 | return is_resource($this->value); 205 | default: 206 | return false; 207 | } 208 | } 209 | 210 | /** 211 | * @param $name 212 | * @return bool 213 | */ 214 | protected function matchOnClassName($name): bool 215 | { 216 | $className = str_replace('_', '\\', $name); 217 | 218 | if (!class_exists($className)) { 219 | return false; 220 | } 221 | 222 | if ($this->value instanceof $className) { 223 | return true; 224 | } 225 | 226 | return false; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/chippyash/Monad/FTry.php: -------------------------------------------------------------------------------- 1 | flatten()); 41 | } 42 | 43 | return new Success($value); 44 | 45 | } catch (\Exception $e) { 46 | return new Failure($e); 47 | } 48 | } 49 | 50 | /** 51 | * Proxy to create() 52 | * 53 | * @param mixed $value 54 | * @return Failure|Success 55 | */ 56 | public static function with($value): FTry 57 | { 58 | return static::create($value); 59 | } 60 | 61 | /** 62 | * Return FTry value if Success else the elseValue 63 | * 64 | * @param mixed $elseValue 65 | * 66 | * @return mixed 67 | */ 68 | public function getOrElse($elseValue) 69 | { 70 | if ($this instanceof Success) { 71 | return $this->value(); 72 | } 73 | 74 | return $elseValue; 75 | } 76 | 77 | /** 78 | * Is this option a Success? 79 | * 80 | * @return bool 81 | */ 82 | abstract public function isSuccess(): bool; 83 | } 84 | -------------------------------------------------------------------------------- /src/chippyash/Monad/FTry/Failure.php: -------------------------------------------------------------------------------- 1 | value = $value; 23 | } 24 | 25 | /** 26 | * Always return another instance of Failure 27 | * 28 | * @param \Exception $value Ignored 29 | * 30 | * @return Failure 31 | */ 32 | public static function create($value = null): Failure 33 | { 34 | if ($value instanceof \Exception) { 35 | return new static($value); 36 | } 37 | 38 | return new static(new \RuntimeException('Creating Failure with no exception')); 39 | } 40 | 41 | /** 42 | * Always return another instance of failure 43 | * 44 | * @param \Closure $function Ignored 45 | * @param array $args Ignored 46 | * 47 | * @return Failure 48 | */ 49 | public function bind(\Closure $function, array $args = []): Failure 50 | { 51 | return new static($this->value); 52 | } 53 | 54 | /** 55 | * Throw this failure as a php exception 56 | * We use 'pass' as `throw` is a PHP reserved word and I'm a Rugby player 57 | * 58 | * @throw \Exception 59 | */ 60 | public function pass() 61 | { 62 | throw $this->value; 63 | } 64 | 65 | /** 66 | * Is this option a Success? 67 | * 68 | * @return bool 69 | */ 70 | public function isSuccess(): bool 71 | { 72 | return false; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/chippyash/Monad/FTry/Success.php: -------------------------------------------------------------------------------- 1 | value = $value; 28 | } 29 | 30 | /** 31 | * Return Success or Failure as a result of bind 32 | * 33 | * @param \Closure $function Ignored 34 | * @param array $args Ignored 35 | * 36 | * @return Success|Failure 37 | */ 38 | public function bind(\Closure $function, array $args = []): FTry 39 | { 40 | try { 41 | return FTry::create($this->callFunction($function, $this->value, $args)); 42 | } catch (\Exception $e) { 43 | return new Failure($e); 44 | } 45 | } 46 | 47 | /** 48 | * Is this option a Success? 49 | * 50 | * @return bool 51 | */ 52 | public function isSuccess(): bool 53 | { 54 | return true; 55 | } 56 | 57 | /** 58 | * Do nothing 59 | * 60 | * @return $this 61 | */ 62 | public function pass() 63 | { 64 | return $this; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/chippyash/Monad/FlattenAble.php: -------------------------------------------------------------------------------- 1 | value(); 28 | if ($val instanceof \Closure) { 29 | return $val(); 30 | } 31 | if ($val instanceof Monadic) { 32 | return $val->flatten(); 33 | } 34 | 35 | return $val; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/chippyash/Monad/Identity.php: -------------------------------------------------------------------------------- 1 | value = $value; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/chippyash/Monad/Map.php: -------------------------------------------------------------------------------- 1 | checkHash($value)) { 39 | throw new \InvalidArgumentException('value is not a hashed array'); 40 | } 41 | } 42 | parent::__construct($value, $type); 43 | } 44 | 45 | /** 46 | * Append value and return a new collection 47 | * 48 | * The appending is based on the hashed keys 49 | * 50 | * Overrides ancestor 51 | * 52 | * @param array $value 53 | * 54 | * @return Collection 55 | * @throws \InvalidArgumentException 56 | */ 57 | public function append($value): Map 58 | { 59 | if (!is_array($value)) { 60 | throw new \InvalidArgumentException('Appended value must be array'); 61 | } 62 | return $this->kUnion(new static($value)); 63 | } 64 | 65 | /** 66 | * Value intersection is meaningless for a Map 67 | * 68 | * @inheritdoc 69 | * @throws \BadMethodCallException 70 | */ 71 | final public function vIntersect(Collection $other, \Closure $function = null) 72 | { 73 | throw new \BadMethodCallException(sprintf(self::ERR_TPL_BADM, __METHOD__)); 74 | } 75 | 76 | /** 77 | * Value union is meaningless for a Map 78 | * 79 | * @inheritdoc 80 | * @throws \BadMethodCallException 81 | */ 82 | final public function vUnion(Collection $other, $sortOrder = SORT_REGULAR) 83 | { 84 | throw new \BadMethodCallException(sprintf(self::ERR_TPL_BADM, __METHOD__)); 85 | } 86 | 87 | /** 88 | * Value difference is meaningless for a Map 89 | * 90 | * @inheritdoc 91 | * @throws \BadMethodCallException 92 | */ 93 | final public function vDiff(Collection $other, \Closure $function = null) 94 | { 95 | throw new \BadMethodCallException(sprintf(self::ERR_TPL_BADM, __METHOD__)); 96 | } 97 | 98 | /** 99 | * @param array $value 100 | * 101 | * @return bool 102 | */ 103 | protected function checkHash(array $value): bool 104 | { 105 | return array_reduce( 106 | array_keys($value), 107 | function ($carry, $val) { 108 | if (!is_string($val)) { 109 | return false; 110 | } 111 | return $carry; 112 | }, 113 | true 114 | ); 115 | } 116 | } -------------------------------------------------------------------------------- /src/chippyash/Monad/Monad.php: -------------------------------------------------------------------------------- 1 | callFunction($function, $this->value, $args)); 37 | } 38 | 39 | /** 40 | * Static factory creator for the Monad 41 | * 42 | * @param $value 43 | * 44 | * @return Monadic 45 | */ 46 | public static function create($value) 47 | { 48 | if ($value instanceof Monadic) { 49 | return $value; 50 | } 51 | 52 | return new static($value); 53 | } 54 | 55 | /** 56 | * Some syntactic sugar 57 | * 58 | * Proxy to bind() e.g. $ret = $foo(function($val){return $val * 2;}); 59 | * Proxy to value() e.g. $val = $foo(); 60 | * 61 | * @see bind() 62 | * @see value() 63 | * 64 | * @return mixed|Monadic 65 | * @throw BadMethodCallException 66 | */ 67 | public function __invoke() 68 | { 69 | if (func_num_args() == 0) { 70 | return $this->value(); 71 | } 72 | if (func_get_arg(0) instanceof \Closure) { 73 | return call_user_func_array(array($this, 'bind'), func_get_args()); 74 | } 75 | 76 | throw new \BadMethodCallException('Invoke could not match value() or bind()'); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/chippyash/Monad/Monadic.php: -------------------------------------------------------------------------------- 1 | value(); 49 | } 50 | 51 | return $elseValue; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/chippyash/Monad/Option/None.php: -------------------------------------------------------------------------------- 1 | value = $value; 27 | } 28 | 29 | /** 30 | * Return Some or None as a result of bind 31 | * 32 | * @param \Closure $function 33 | * @param array $args 34 | * @param mixed $noneValue Optional value to test for None 35 | * 36 | * @return Some|None 37 | */ 38 | public function bind(\Closure $function, array $args = [], $noneValue = null): Option 39 | { 40 | return Option::create($this->callFunction($function, $this->value, $args), $noneValue); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/chippyash/Monad/ReturnValueAble.php: -------------------------------------------------------------------------------- 1 | value; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/chippyash/Monad/Set.php: -------------------------------------------------------------------------------- 1 | exchangeArray($this->checkUniqueness($value)); 39 | } 40 | } 41 | 42 | /** 43 | * Returns a Set containing all the values of this Set that are present 44 | * in the other Set. 45 | * 46 | * If the optional comparison function is supplied it must have signature 47 | * function(mixed $a, mixed $b){}. The comparison function must return an integer 48 | * less than, equal to, or greater than zero if the first argument is considered 49 | * to be respectively less than, equal to, or greater than the second. 50 | * 51 | * If the comparison function is not supplied, a built in one will be used 52 | * 53 | * @param Set $other 54 | * @param callable|\Closure $function Optional function to compare values 55 | * 56 | * @return Set 57 | */ 58 | public function vIntersect(Collection $other, \Closure $function = null): Set 59 | { 60 | $function = (is_null($function) ? $this->equalityFunction() : $function); 61 | 62 | return parent::vIntersect($other, $function); 63 | } 64 | 65 | /** 66 | * Compares this collection against another collection using its values for 67 | * comparison and returns a new Collection with the values in this collection 68 | * that are not present in the other collection. 69 | * 70 | * Note that keys are preserved 71 | * 72 | * If the optional comparison function is supplied it must have signature 73 | * function(mixed $a, mixed $b){}. The comparison function must return an integer 74 | * less than, equal to, or greater than zero if the first argument is considered 75 | * to be respectively less than, equal to, or greater than the second. 76 | * 77 | * If the comparison function is not supplied, a built in one will be used 78 | * 79 | * @param Set $other 80 | * @param \Closure $function optional function to compare values 81 | * 82 | * @return Set 83 | */ 84 | public function vDiff(Collection $other, \Closure $function = null): Set 85 | { 86 | $function = (is_null($function) ? $this->equalityFunction() : $function); 87 | 88 | return parent::vDiff( 89 | $other, $function 90 | ); 91 | } 92 | 93 | /** 94 | * Bind monad with function. Function is in form f($value){} 95 | * You can pass additional parameters in the $args array in which case your 96 | * function should be in the form f($value, $arg1, ..., $argN) 97 | * 98 | * @param \Closure $function 99 | * @param array $args additional arguments to pass to function 100 | * 101 | * @return Set 102 | */ 103 | public function bind(\Closure $function, array $args = []): Set 104 | { 105 | $res = $this->callFunction($function, $this, $args); 106 | 107 | return ($res instanceof Set ? $res : new static(is_array($res)? $res :[$res])); 108 | } 109 | 110 | /** 111 | * Key intersection is meaningless for a set 112 | * 113 | * @inheritdoc 114 | * @throws \BadMethodCallException 115 | */ 116 | final public function kIntersect(Collection $other, \Closure $function = null) 117 | { 118 | throw new \BadMethodCallException(sprintf(self::ERR_TPL_BADM, __METHOD__)); 119 | } 120 | 121 | /** 122 | * Key union is meaningless for a set 123 | * 124 | * @inheritdoc 125 | * @throws \BadMethodCallException 126 | */ 127 | final public function kUnion(Collection $other) 128 | { 129 | throw new \BadMethodCallException(sprintf(self::ERR_TPL_BADM, __METHOD__)); 130 | } 131 | 132 | /** 133 | * Key difference is meaningless for a set 134 | * 135 | * @inheritdoc 136 | * @throws \BadMethodCallException 137 | */ 138 | final public function kDiff(Collection $other, \Closure $function = null) 139 | { 140 | throw new \BadMethodCallException(sprintf(self::ERR_TPL_BADM, __METHOD__)); 141 | } 142 | 143 | 144 | /** 145 | * Make sure that values are unique 146 | * 147 | * @param array $values Values to check 148 | * 149 | * @return array 150 | */ 151 | protected function checkUniqueness(array $values): array 152 | { 153 | try { 154 | //see if we can turn a value into a string 155 | $toTest = end($values); 156 | reset($values); 157 | (string) $toTest; //this will throw an exception if it fails 158 | 159 | //do the simple 160 | return array_values(array_unique($values)); 161 | } catch (\Throwable $e) { 162 | //slower but effective 163 | return array_values( 164 | array_map( 165 | function ($key) use ($values) { 166 | return $values[$key]; 167 | }, 168 | array_keys( 169 | array_unique( 170 | array_map( 171 | function ($item) { 172 | return serialize($item); 173 | }, 174 | $values 175 | ) 176 | ) 177 | ) 178 | ) 179 | ); 180 | } 181 | } 182 | 183 | /** 184 | * Provide equality check function 185 | * 186 | * @return \Closure 187 | */ 188 | private function equalityFunction(): \Closure 189 | { 190 | return function ($a, $b) { 191 | if (is_object($a)) { 192 | $a = \serialize($a); 193 | $b = \serialize($b); 194 | } 195 | 196 | return ($a === $b ? 0 : ($a < $b ? -1 : 1)); 197 | }; 198 | } 199 | } -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | .phpunit.result.cache -------------------------------------------------------------------------------- /test/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 31 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /test/src/chippyash/FTry/FailureTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Failure::class, new Failure(new \Exception())); 21 | } 22 | 23 | public function testCreateCreatesAFailureIfValueIsException() 24 | { 25 | $this->assertInstanceOf(Failure::class, Failure::create(new \Exception())); 26 | } 27 | 28 | public function testCreateCreatesAFailureIfValueIsNotAnException() 29 | { 30 | $this->assertInstanceOf(Failure::class, Failure::create('foo')); 31 | } 32 | 33 | public function testBindReturnsFailureWithSameValue() 34 | { 35 | $exc = new \Exception(); 36 | $fail = Failure::create($exc); 37 | $this->assertInstanceOf(Failure::class, $fail); 38 | $this->assertInstanceOf(Failure::class, $fail->bind(function(){})); 39 | $this->assertEquals($exc, $fail->bind(function(){})->value()); 40 | } 41 | 42 | public function testCallingPassWillThrowAnException() 43 | { 44 | $this->expectException(\RuntimeException::class); 45 | Failure::create('foo')->pass(); 46 | } 47 | 48 | public function testCallingIsSuccessWillReturnFalse() 49 | { 50 | $this->assertFalse(Failure::create(new \Exception())->isSuccess()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/src/chippyash/FTry/SuccessTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Success::class, new Success('foo')); 22 | } 23 | 24 | public function testYouCannotConstructASuccessWithAnException() 25 | { 26 | $this->expectException(\RuntimeException::class); 27 | new Success(new \Exception()); 28 | } 29 | 30 | public function testBindingASuccessWithSomethingThatDoesNotThrowAnExceptionWillReturnSuccess() 31 | { 32 | $sut = new Success('foo'); 33 | $this->assertInstanceOf(Success::class, $sut->bind(function(){return true;})); 34 | } 35 | 36 | public function testBindingASuccessWithSomethingThatReturnsASuccessWillFlattenTheValue() 37 | { 38 | $sut = new Success('foo'); 39 | $binded = $sut->bind(function () { 40 | return new Success('bar'); 41 | }); 42 | $this->assertInstanceOf(Success::class, $binded); 43 | $this->assertEquals('bar', $binded->value()); 44 | } 45 | 46 | public function testBindingASuccessWithSomethingThatThrowsAnExceptionWillReturnAFailure() 47 | { 48 | $sut = new Success('foo'); 49 | $this->assertInstanceOf(Failure::class, $sut->bind(function(){throw new \Exception();})); 50 | } 51 | 52 | public function testYouCanGetAValueFromASuccess() 53 | { 54 | $sut = new Success('foo'); 55 | $this->assertEquals('foo', $sut->value()); 56 | } 57 | 58 | public function testCallingIsSuccessWillReturnTrue() 59 | { 60 | $this->assertTrue(Success::create('foo')->isSuccess()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/src/chippyash/Monad/CollectionTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Collection::class, new Collection(['foo'])); 21 | } 22 | 23 | public function testYouCannotConstructACollectionWithNullValues() 24 | { 25 | $this->expectException(\RuntimeException::class); 26 | $this->assertInstanceOf(Collection::class, new Collection([null])); 27 | } 28 | 29 | public function testYouCannotConstructACollectionWithAnEmptyArrayAndNoTypeSpecified() 30 | { 31 | $this->expectException(\RuntimeException::class); 32 | new Collection([]); 33 | } 34 | 35 | public function testYouCanConstructAnEmptyCollectionIfYouPassAType() 36 | { 37 | $this->assertInstanceOf(Collection::class, new Collection([], 'string')); 38 | $this->assertInstanceOf(Collection::class, new Collection([], Monad::class)); 39 | } 40 | 41 | public function testWhenConstructingACollectionYouMustHaveSameTypeValues() 42 | { 43 | $this->assertInstanceOf(Collection::class, new Collection(['foo','bar','baz'], 'string')); 44 | } 45 | 46 | public function testConstructingACollectionWithDissimilarTypesWillCauseAnException() 47 | { 48 | $this->expectException(\RuntimeException::class); 49 | new Collection(['foo',new \StdClass(),'baz'], 'string'); 50 | } 51 | 52 | public function testYouCanCreateACollection() 53 | { 54 | $this->assertInstanceOf(Collection::class, Collection::create(['foo'])); 55 | } 56 | 57 | public function testTheValueOfACollectionIsTheCollection() 58 | { 59 | $collection = Collection::create(['foo']); 60 | $this->assertEquals($collection, $collection->value()); 61 | } 62 | 63 | public function testYouCanBindAFunctionToTheEntireCollectionAndReturnACollection() 64 | { 65 | $sut = Collection::create([2,3,4,5,6]); 66 | //function returns a single value - converted to a collection 67 | $f = function($c){ 68 | return $c[0]; 69 | }; 70 | $this->assertEquals([2], $sut->bind($f)->getArrayCopy()); 71 | //function returns a single value - converted to collection 72 | $f2 = function($c){ 73 | return 'foo'; 74 | }; 75 | $this->assertEquals(['foo'], $sut->bind($f2)->getArrayCopy()); 76 | //function returns a collection 77 | $f3 = function($c) { 78 | return new Collection(array_flip($c->toArray())); 79 | }; 80 | $this->assertEquals([2=>0, 3=>1, 4=>2, 5=>3, 6=>4], $sut->bind($f3)->getArrayCopy()); 81 | //function returns and array - converted to a collection 82 | $f4 = function($c){ 83 | return $c->toArray(); 84 | }; 85 | $this->assertEquals([2,3,4,5,6], $sut->bind($f4)->getArrayCopy()); 86 | } 87 | 88 | public function testYouCanBindAFunctionToEachMemberOfTheCollectionAndReturnACollection() 89 | { 90 | $sut = Collection::create([2,3,4,5,6]); 91 | $res = $sut->each(function($v){return $v * 2;}); 92 | $this->assertInstanceOf(Collection::class, $res); 93 | $this->assertEquals([4,6,8,10,12], $res->getArrayCopy()); 94 | } 95 | 96 | public function testYouCanCountTheItemsInTheCollection() 97 | { 98 | $this->assertEquals(4, Collection::create([1,2,3,4])->count()); 99 | } 100 | 101 | public function testYouCanGetAnIteratorForACollection() 102 | { 103 | $this->assertInstanceOf('ArrayIterator', Collection::create([1,2,3,4])->getIterator()); 104 | } 105 | 106 | public function testYouCannotUnsetACollectionMember() 107 | { 108 | $this->expectException(\BadMethodCallException::class); 109 | Collection::create([1,2,3])->offsetUnset(2); 110 | } 111 | 112 | public function testYouCannotSetACollectionMember() 113 | { 114 | $this->expectException(\BadMethodCallException::class); 115 | Collection::create([1,2,3])->offsetSet(2, 6); 116 | } 117 | 118 | public function testYouCanGetACollectionMemberAsAnArrayOffset() 119 | { 120 | $sut = Collection::create([1,2,3]); 121 | $this->assertEquals(2, $sut->offsetGet(1)); 122 | $this->assertEquals(2, $sut[1]); 123 | } 124 | 125 | public function testYouCanTestIfACollectionMemberExistsAsAnArrayOffset() 126 | { 127 | $sut = Collection::create([1,2,3]); 128 | $this->assertTrue(isset($sut[1])); 129 | $this->assertFalse(isset($sut[99])); 130 | } 131 | 132 | public function testYouCanCreateACollectionOfCollections() 133 | { 134 | $s1 = Collection::create([1,2,3]); 135 | $s2 = Collection::create([5,6,7]); 136 | $s3 = Collection::create([$s1, $s2]); 137 | $this->assertInstanceOf(Collection::class, $s3); 138 | } 139 | 140 | public function testFlatteningACollectionOfCollectionsWillReturnACollection() 141 | { 142 | $s1 = Collection::create([1,2,3]); 143 | $s2 = Collection::create([5,6,7]); 144 | $flattened = Collection::create([$s1, $s2])->flatten(); 145 | $this->assertInstanceOf(Collection::class, $flattened); 146 | foreach ($flattened as $value) { 147 | $this->assertInstanceOf(Collection::class, $value); 148 | } 149 | } 150 | 151 | public function testYouCanGetTheDifferenceOfValuesBetweenTwoCollections() 152 | { 153 | $s1 = Collection::create([1, 2, 3, 6, 7]); 154 | $s2 = Collection::create([6,7]); 155 | $this->assertEquals([1,2,3], $s1->vDiff($s2)->flatten()->toArray()); 156 | } 157 | 158 | public function testYouCanGetTheDifferenceOfKeysBetweenTwoCollections() 159 | { 160 | $s1 = Collection::create([1 => 0, 2 => 0, 3 => 0, 6 => 0, 7 => 0]); 161 | $s2 = Collection::create([6 => 0,7 => 0]); 162 | $this->assertEquals([1 => 0,2 => 0,3 => 0], $s1->kDiff($s2)->flatten()->toArray()); 163 | } 164 | 165 | public function testYouCanChainVDiffMethodsToActOnArbitraryNumbersOfCollections() 166 | { 167 | $s1 = Collection::create([1, 2, 3, 6, 7]); 168 | $s2 = Collection::create([6,7]); 169 | $s3 = Collection::create([1]); 170 | $s4 = Collection::create([9]); 171 | 172 | $this->assertEquals([2,3], array_values($s1->vDiff($s2)->vDiff($s3)->vDiff($s4)->flatten()->toArray())); 173 | } 174 | 175 | public function testYouCanChainKDiffMethodsToActOnArbitraryNumbersOfCollections() 176 | { 177 | $s1 = Collection::create([1 => 0, 2 => 0, 3 => 0, 6 => 0, 7 => 0]); 178 | $s2 = Collection::create([6 => 0, 7 => 0]); 179 | $s3 = Collection::create([1 => 0]); 180 | $s4 = Collection::create([9 => 0]); 181 | 182 | $this->assertEquals([2 => 0, 3 => 0], $s1->kDiff($s2)->kDiff($s3)->kDiff($s4)->flatten()->toArray()); 183 | } 184 | 185 | public function testYouCanSupplyAnOptionalComparatorFunctionToVDiffMethod() 186 | { 187 | $s1 = Collection::create([1, 2, 3, 6, 7]); 188 | $s2 = Collection::create([6,7]); 189 | $f = function($a, $b){ 190 | return ($a<$b ? -1 : ($a>$b ? 1 : 0)); 191 | }; 192 | $this->assertEquals([1,2,3], $s1->vDiff($s2, $f)->flatten()->toArray()); 193 | } 194 | 195 | public function testYouCanSupplyAnOptionalComparatorFunctionToKDiffMethod() 196 | { 197 | $s1 = Collection::create([1 => 0, 2 => 0, 3 => 0, 6 => 0, 7 => 0]); 198 | $s2 = Collection::create([6 => 0,7 => 0]); 199 | $f = function($a, $b){ 200 | return ($a<$b ? -1 : ($a>$b ? 1 : 0)); 201 | }; 202 | $this->assertEquals([1 => 0,2 => 0,3 => 0], $s1->kDiff($s2, $f)->flatten()->toArray()); 203 | } 204 | 205 | public function testYouCanGetTheIntersectionOfTwoCollectionsByValue() 206 | { 207 | $s1 = Collection::create([1, 2, 3, 6, 7]); 208 | $s2 = Collection::create([6,7]); 209 | $this->assertEquals([6,7], array_values($s1->vIntersect($s2)->flatten()->toArray())); 210 | } 211 | 212 | public function testYouCanGetTheIntersectionOfTwoCollectionsByKey() 213 | { 214 | $s1 = Collection::create([1, 2, 3, 6, 7]); 215 | $s2 = Collection::create([6,7]); 216 | $this->assertEquals([1,2], array_values($s1->kIntersect($s2)->flatten()->toArray())); 217 | } 218 | 219 | public function testYouCanChainValueIntersectMethodsToActOnArbitraryNumbersOfCollections() 220 | { 221 | $s1 = Collection::create([1, 2, 3, 6, 7]); 222 | $s2 = Collection::create([6,7]); 223 | $s3 = Collection::create([7]); 224 | 225 | $this->assertEquals([7], array_values($s1->vIntersect($s2)->vIntersect($s3)->flatten()->toArray())); 226 | } 227 | 228 | public function testYouCanChainKeyIntersectMethodsToActOnArbitraryNumbersOfCollections() 229 | { 230 | $s1 = Collection::create([1, 2, 3, 6, 7]); 231 | $s2 = Collection::create([6,7]); 232 | $s3 = Collection::create([7]); 233 | 234 | $this->assertEquals([0=>1], array_values($s1->kIntersect($s2)->kIntersect($s3)->flatten()->toArray())); 235 | } 236 | 237 | public function testYouCanSupplyAnOptionalComparatorFunctionToTheValueIntersectMethod() 238 | { 239 | $s1 = Collection::create([1, 2, 3, 6, 7]); 240 | $s2 = Collection::create([6,7]); 241 | $f = function($a, $b){ 242 | return ($a<$b ? -1 : ($a>$b ? 1 : 0)); 243 | }; 244 | $this->assertEquals([6, 7], array_values($s1->vIntersect($s2, $f)->flatten()->toArray())); 245 | } 246 | 247 | public function testYouCanSupplyAnOptionalComparatorFunctionToTheKeyIntersectMethod() 248 | { 249 | $s1 = Collection::create([1, 2, 3, 6, 7]); 250 | $s2 = Collection::create([6,7]); 251 | $f = function($a, $b){ 252 | return ($a<$b ? -1 : ($a>$b ? 1 : 0)); 253 | }; 254 | $this->assertEquals([0=>1, 1=>2], array_values($s1->kIntersect($s2, $f)->flatten()->toArray())); 255 | } 256 | 257 | public function testYouCanGetTheUnionOfValuesOfTwoCollections() 258 | { 259 | $s1 = Collection::create([1, 2, 3, 6, 7]); 260 | $s2 = Collection::create([3, 6, 7, 8]); 261 | $this->assertEquals([1,2,3,6,7,8], array_values($s1->vUnion($s2)->flatten()->toArray())); 262 | } 263 | 264 | public function testYouCanChainTheUnionOfValuesOfTwoCollections() 265 | { 266 | $s1 = Collection::create([1, 2, 3, 6, 7]); 267 | $s2 = Collection::create([3, 6, 7, 8]); 268 | $s3 = Collection::create([7, 8, 9, 10]); 269 | $this->assertEquals([1,2,3,6,7,8,9,10], array_values($s1->vUnion($s2)->vUnion($s3)->flatten()->toArray())); 270 | } 271 | 272 | public function testYouCanGetTheUnionOfKeysOfTwoCollections() 273 | { 274 | $s1 = Collection::create([1, 2, 3, 6, 7]); 275 | $s2 = Collection::create([0, 0, 3, 6, 7, 8]); 276 | $this->assertEquals([0=>1,1=>2,2=>3,3=>6,4=>7, 5=>8], $s1->kUnion($s2)->flatten()->toArray()); 277 | } 278 | 279 | public function testYouCanChainTheUnionOfKeysOfTwoCollections() 280 | { 281 | $s1 = Collection::create([1, 2, 3, 6, 7]); 282 | $s2 = Collection::create([0, 0, 3, 6, 7, 8]); 283 | $s3 = Collection::create([0, 0, 0, 7, 8, 9, 10]); 284 | $this->assertEquals( 285 | [0=>1, 1=>2, 2=>3, 3=>6,4=>7, 5=>8, 6=>10], 286 | $s1->kUnion($s2)->kUnion($s3)->flatten()->toArray() 287 | ); 288 | } 289 | 290 | public function testPerformingAValueUnionWithDissimilarCollectionsWillThrowAnException() 291 | { 292 | $this->expectException(\RuntimeException::class); 293 | (new Collection([],'string'))->vUnion(new Collection([1])); 294 | } 295 | 296 | public function testPerformingAKeyUnionWithDissimilarCollectionsWillThrowAnException() 297 | { 298 | $this->expectException(\RuntimeException::class); 299 | (new Collection([],'string'))->kUnion(new Collection([1])); 300 | } 301 | 302 | public function testTheHeadOfACollectionIsItsFirstMember() 303 | { 304 | $s1 = Collection::create([1, 2, 3, 6, 7]); 305 | $this->assertEquals([1], $s1->head()->flatten()->toArray()); 306 | } 307 | 308 | public function testTheTailOfACollectionIsAllButItsFirstMember() 309 | { 310 | $s1 = Collection::create([1, 2, 3, 6, 7]); 311 | $this->assertEquals([2, 3, 6, 7], $s1->tail()->flatten()->toArray()); 312 | } 313 | 314 | public function testYouCanFilterACollectionWithAClosure() 315 | { 316 | $f = function($v){return $v>3;}; 317 | $s1 = Collection::create([1, 2, 3, 6, 7]); 318 | $this->assertEquals([3=>6, 4=>7], $s1->filter($f)->toArray()); 319 | } 320 | 321 | public function testYouCanReduceACollectionToASingleValueWithAClosure() 322 | { 323 | $f = function($v, $carry){return $carry + $v;}; 324 | $s1 = Collection::create([1, 2, 3, 6, 7]); 325 | $this->assertEquals(29, $s1->reduce($f, 10)); 326 | } 327 | 328 | public function testYouCanReferenceACollectionAsThoughItWasAnArray() 329 | { 330 | $s1 = Collection::create([1, 2, 3, 6, 7]); 331 | $this->assertEquals(2, $s1[1]); 332 | } 333 | 334 | public function testValueMethodProxiesToCollectionGetArrayCopyMethod() 335 | { 336 | $s1 = Collection::create([1, 2, 3, 6, 7]); 337 | $this->assertEquals($s1->toArray(), $s1->getArrayCopy()); 338 | } 339 | 340 | public function testYouCanFlipACollection() 341 | { 342 | $s1 = Collection::create([1, 2, 3, 6, 7])->flip()->toArray(); 343 | $this->assertEquals([1=>0, 2=>1, 3=>2, 6=>3, 7=>4], $s1); 344 | } 345 | 346 | public function testAppendingToACollectionReturnsANewCollection() 347 | { 348 | $s1 = Collection::create([1, 2, 3, 6, 7]); 349 | $s2 = $s1->append(8); 350 | $this->assertEquals([1,2,3,6,7,8], $s2->toArray()); 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /test/src/chippyash/Monad/FMatchTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(FMatch::class, new FMatch('foo')); 26 | } 27 | 28 | public function testYouCanConstructViaStaticOnFactoryMethod() 29 | { 30 | $this->assertInstanceOf(FMatch::class, FMatch::on('foo')); 31 | } 32 | 33 | public function testYouCanFMatchOnNativePhpTypes() 34 | { 35 | $this->assertEquals('foo', FMatch::on('foo')->string()->value()); 36 | $this->assertEquals(1, FMatch::on(1)->int()->value()); 37 | $this->assertEquals(1, FMatch::on(1)->integer()->value()); 38 | $this->assertEquals(1.0, FMatch::on(1.0)->float()->value()); 39 | $this->assertEquals(1.0, FMatch::on(1.0)->double()->value()); 40 | $this->assertNull(FMatch::on(null)->null()->value()); 41 | $this->assertEquals([], FMatch::on([])->array()->value()); 42 | $this->assertTrue(FMatch::on(true)->bool()->value()); 43 | $this->assertTrue(FMatch::on(true)->boolean()->value()); 44 | $this->assertInstanceOf(\Closure::class, FMatch::on(function(){})->callable()->value()); 45 | $this->assertInstanceOf(\Closure::class, FMatch::on(function(){})->function()->value()); 46 | $this->assertInstanceOf(\Closure::class, FMatch::on(function(){})->closure()->value()); 47 | $this->assertTrue(is_object(FMatch::on(new \stdClass())->object()->value())); 48 | $this->assertEquals(123, FMatch::on(123)->scalar()->value()); 49 | $this->assertEquals(123, FMatch::on('123')->numeric()->value()); 50 | $this->assertEquals(123, FMatch::on(123)->numeric()->value()); 51 | 52 | $fileRoot = vfsStream::setup(); 53 | $this->assertEquals('vfs://root', FMatch::on($fileRoot->url())->dir()->value()); 54 | $this->assertEquals('vfs://root', FMatch::on($fileRoot->url())->directory()->value()); 55 | 56 | $fileRoot->addChild(new vfsStreamFile('foo')); 57 | $this->assertEquals('vfs://root/foo', FMatch::on($fileRoot->url() . '/foo')->file()->value()); 58 | 59 | $fh = fopen($fileRoot->url() . '/foo', 'r'); 60 | $this->assertTrue(is_resource(FMatch::on($fh)->resource()->value())); 61 | fclose($fh); 62 | } 63 | 64 | public function testFMatchingWillReturnSetByCallableParameterIfFMatched() 65 | { 66 | $this->assertEquals('foobarfoo', FMatch::on('foobar')->string(function($val){return $val . 'foo';})->value()); 67 | } 68 | 69 | public function testFMatchingWillReturnSetByNonCallableParameterIfFMatched() 70 | { 71 | $this->assertEquals('foobarfoo', FMatch::on('foobar')->string('foobarfoo')->value()); 72 | } 73 | 74 | public function testFailingToFMatchWillReturnNewFMatchObjectWithSameValueAsOriginal() 75 | { 76 | $this->assertEquals(true, FMatch::on(true)->string()->value()); 77 | } 78 | 79 | public function testYouCanFMatchOnAClassName() 80 | { 81 | $this->assertInstanceOf('StdClass', FMatch::on(new \StdClass)->StdClass()->value()); 82 | $this->assertInstanceOf('Monad\FMatch', FMatch::on(new FMatch('foo'))->Monad_FMatch()->value()); 83 | } 84 | 85 | public function testFailingToFMatchOnClassNameWillReturnNewFMatchObjectWithSameValueAsOriginal() 86 | { 87 | $val = new Identity('foo'); 88 | $test = FMatch::on($val)->StdClass(); 89 | $this->assertInstanceOf(FMatch::class, $test); 90 | $this->assertInstanceOf(Identity::class, $test->value()); 91 | $this->assertEquals('foo', $test->flatten()); 92 | } 93 | 94 | public function testYouCanChainFMatchTests() 95 | { 96 | $test = FMatch::on(true) 97 | ->string() 98 | ->int() 99 | ->bool() 100 | ->value(); 101 | 102 | $this->assertTrue($test); 103 | } 104 | 105 | public function testYouCanChainFMatchTestsAndBindAFunctionOnSuccessfulFMatch() 106 | { 107 | $test = FMatch::on(true) 108 | ->string() 109 | ->int() 110 | ->bool(function(){return 'foo';}) 111 | ->value(); 112 | 113 | $this->assertEquals('foo', $test); 114 | } 115 | 116 | public function testBindingAFMatchWillReturnAFMatch() 117 | { 118 | $test = FMatch::on('foo')->bind(function($v){return $v . 'bar';}); 119 | $this->assertInstanceOf(FMatch::class, $test); 120 | $this->assertEquals('foobar', $test->value()); 121 | } 122 | 123 | /** 124 | * @dataProvider anyFMatchData 125 | * @param $value 126 | */ 127 | public function testFMatchOnAnyMethodWillFMatchAnything($value) 128 | { 129 | $this->assertEquals( 130 | $value, 131 | FMatch::on($value) 132 | ->any() 133 | ->value() 134 | ); 135 | } 136 | 137 | /** 138 | * @dataProvider anyFMatchData 139 | * @param $value 140 | */ 141 | public function testFMatchOnAnyMethodCanAcceptOptionalFunctionAndArguments($value) 142 | { 143 | $this->assertEquals( 144 | 'bar', 145 | FMatch::on($value) 146 | ->any( 147 | function($v, $z){return $z;}, 148 | ['bar'] 149 | ) 150 | ->value() 151 | ); 152 | } 153 | 154 | public function anyFMatchData() 155 | { 156 | date_default_timezone_set('UTC'); 157 | return [ 158 | [2], 159 | ['foo'], 160 | [1.13], 161 | [new \DateTime()], 162 | [new \StdClass()], 163 | [true], 164 | [false] 165 | ]; 166 | } 167 | 168 | public function testYouCanNestFMatches() 169 | { 170 | $this->assertEquals('foo', $this->nestedFMatcher('foo')->value()); 171 | $this->assertEquals('bar', $this->nestedFMatcher(Option::create('bar'))->value()); 172 | try { 173 | $this->nestedFMatcher(Option::create()); 174 | $this->fail('Expected an Exception but got none'); 175 | } catch (\Exception $e) { 176 | $this->assertTrue(true); 177 | } 178 | $this->assertEquals('foobar', $this->nestedFMatcher(Identity::create('foo'))->value()); 179 | //expecting match on any() as integer won't be matched 180 | $this->assertEquals('any', $this->nestedFMatcher(2)->value()); 181 | } 182 | 183 | protected function nestedFMatcher($initialValue) 184 | { 185 | return FMatch::on($initialValue) 186 | ->string('foo') 187 | ->Monad_Option( 188 | function ($v) { 189 | return FMatch::on($v) 190 | ->Monad_Option_Some(function ($v) { 191 | return $v->value(); 192 | }) 193 | ->Monad_Option_None(function () { 194 | throw new \Exception(); 195 | }) 196 | ->value(); 197 | } 198 | ) 199 | ->Monad_Identity( 200 | function ($v) { 201 | return $v->value() . 'bar'; 202 | } 203 | ) 204 | ->any( 205 | function() { 206 | return 'any'; 207 | } 208 | ); 209 | } 210 | 211 | public function testYouCanTestForEquality() 212 | { 213 | $test = FMatch::on('foo') 214 | ->test('foo') 215 | ->value(); 216 | $this->assertEquals('foo', $test); 217 | 218 | $test = FMatch::on('foo') 219 | ->test('bar') 220 | ->value(); 221 | $this->assertEquals('foo', $test); 222 | 223 | $test = FMatch::on('foo') 224 | ->test('foo', function($v){return new Some($v);}) 225 | ->flatten(); 226 | $this->assertEquals('foo', $test); 227 | 228 | $test = FMatch::on('bar') 229 | ->test('foo', function($v){return new Some($v);}) 230 | ->any(function(){return new None();}) 231 | ->value(); 232 | $this->assertInstanceOf('Monad\Option\None', $test); 233 | 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /test/src/chippyash/Monad/FTryTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Success::class, FTry::create('foo')); 24 | $this->assertEquals('foo', FTry::create('foo')->value()); 25 | 26 | $this->assertInstanceOf(Success::class, FTry::create(function(){return true;})); 27 | $this->assertInstanceOf(\Closure::class, FTry::create(function(){return true;})->value()); 28 | 29 | $this->assertTrue(FTry::create(function(){return true;})->flatten()); 30 | $this->assertInstanceOf(Success::class, FTry::create(new Identity('foo'))); 31 | 32 | $this->assertEquals('foo', FTry::create(new Identity('foo'))->flatten()); 33 | } 34 | 35 | public function testCreatingAnFTryWithAnExceptionWillReturnAFailure() 36 | { 37 | $this->assertInstanceOf(Failure::class, FTry::create(new \Exception())); 38 | $this->assertInstanceOf(\Exception::class, FTry::create(new \Exception())->value()); 39 | 40 | $this->assertInstanceOf(Failure::class, FTry::create(function(){throw new \Exception();})); 41 | $this->assertInstanceOf(\Exception::class, FTry::create(function(){throw new \Exception();})->value()); 42 | 43 | $this->assertInstanceOf(Failure::class, FTry::create(new Identity(function(){throw new \Exception();}))); 44 | $this->assertInstanceOf(\Exception::class, FTry::create(new Identity(function(){throw new \Exception();}))->value()); 45 | } 46 | 47 | public function testTheWithMethodProxiesToCreate() 48 | { 49 | $this->assertInstanceOf(Success::class, FTry::with('foo')); 50 | $this->assertInstanceOf(Success::class, FTry::with(function(){return true;})); 51 | $this->assertInstanceOf(Success::class, FTry::with(new Identity('foo'))); 52 | $this->assertInstanceOf(Failure::class, FTry::with(new \Exception())); 53 | $this->assertInstanceOf(Failure::class, FTry::with(function(){throw new \Exception();})); 54 | $this->assertInstanceOf(Failure::class, FTry::with(new Identity(function(){throw new \Exception();}))); 55 | } 56 | 57 | public function testGetOrElseWillReturnFTryValueIfOptionIsASuccess() 58 | { 59 | $sut = FTry::create(true); 60 | $this->assertTrue($sut->getOrElse(false)); 61 | } 62 | 63 | public function testGetOrElseWillReturnElseValueIfFTryIsAFailure() 64 | { 65 | $sut = FTry::create(new \Exception()); 66 | $this->assertFalse($sut->getOrElse(false)); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /test/src/chippyash/Monad/IdentityTest.php: -------------------------------------------------------------------------------- 1 | sut = new Identity('foo'); 26 | } 27 | 28 | public function testYouCanCreateAnIdentityStatically() 29 | { 30 | $this->assertInstanceOf(Identity::class, Identity::create('bar')); 31 | } 32 | 33 | public function testCreatingAnIdentityWithAnIdentityParameterWillReturnTheParameter() 34 | { 35 | $this->assertEquals($this->sut, Identity::create($this->sut)); 36 | } 37 | 38 | public function testCreatingAnIdentityWithANonIdentityParameterWillReturnAnIdentityContainingTheParameterAsValue() 39 | { 40 | $sut = Identity::create('foo'); 41 | $this->assertInstanceOf(Identity::class, $sut); 42 | $this->assertEquals('foo', $sut->value()); 43 | 44 | $sut1 = Identity::create(function($a){return $a;}); 45 | $this->assertInstanceOf(Identity::class, $sut1); 46 | $this->assertInstanceOf(\Closure::class, $sut1->value()); 47 | } 48 | 49 | public function testYouCanBindAFunctionOnAnIdentity() 50 | { 51 | $func = function ($value) { 52 | return $value . 'bar'; 53 | }; 54 | $this->assertEquals('foobar', $this->sut->bind($func)->value()); 55 | 56 | $identity = new Identity($this->sut); 57 | $this->assertEquals('foobar', $identity->bind($func)->value()); 58 | } 59 | 60 | public function testBindCanTakeOptionalAdditionalParameters() 61 | { 62 | $func = function ($value, $fudge) { 63 | return $value . $fudge; 64 | }; 65 | $this->assertEquals('foobar', $this->sut->bind($func, ['bar'])->value()); 66 | 67 | $identity = new Identity($this->sut); 68 | $this->assertEquals('foobar', $identity->bind($func, ['bar'])->value()); 69 | } 70 | 71 | public function testYouCanChainBindMethodsTogether() 72 | { 73 | $sut = new Identity(10); 74 | $val = $sut 75 | ->bind(function($v){return $v * 10;}) 76 | ->bind(function($v, $n){return $v - $n;}, [2]) 77 | ->value(); 78 | $this->assertEquals(98, $val); 79 | } 80 | 81 | 82 | public function testBindingOnAnIdentityWithAClosureValueWillEvaluateTheValue() 83 | { 84 | $sut = new Identity(function(){return 'foo';}); 85 | $func = function ($value) { 86 | return $value . 'bar'; 87 | }; 88 | $this->assertEquals('foobar', $sut->bind($func)->value()); 89 | } 90 | 91 | public function testYouCanFlattenAnIdentityValueToItsBaseType() 92 | { 93 | $sut = new Identity(function(){return 'foo';}); 94 | $func = function ($value) { 95 | return $value . 'bar'; 96 | }; 97 | $this->assertEquals('foobar', $sut->bind($func)->flatten()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test/src/chippyash/Monad/MapTest.php: -------------------------------------------------------------------------------- 1 | expectException(\RuntimeException::class); 19 | new Map(); 20 | } 21 | 22 | public function testMapsRequireStringKeys() 23 | { 24 | $this->expectException(\InvalidArgumentException::class); 25 | $this->expectExceptionMessage('value is not a hashed array'); 26 | new Map(['a','b']); 27 | } 28 | 29 | public function testYouCanConstructAnEmptyMapIfYouPassAType() 30 | { 31 | $this->assertInstanceOf(Map::class, new Map([], 'string')); 32 | $this->assertInstanceOf(Map::class, new Map([], 'Monad\Identity')); 33 | } 34 | 35 | public function testAppendingToAMapReturnsANewMap() 36 | { 37 | $orig = new Map([], 'string'); 38 | $new = $orig->append(['foo' => 'bar']); 39 | $this->assertEquals(['foo' => 'bar'], $new->toArray()); 40 | } 41 | 42 | public function testAppendingToAMapWithUnhashedValuesThrowsAnException() 43 | { 44 | $this->expectException(\InvalidArgumentException::class); 45 | $this->expectExceptionMessage('value is not a hashed array'); 46 | $orig = new Map([], 'string'); 47 | $orig->append(['bar']); 48 | } 49 | 50 | public function testVunionMethodIsNotSupportedForMaps() 51 | { 52 | $this->expectException(\BadMethodCallException::class); 53 | $this->expectExceptionMessage('vUnion is not a supported method for Maps'); 54 | $setA = Map::create(['a' =>0, 'b' => 0]); 55 | $setB = Map::create(['c' =>0, 'd' => 0]); 56 | $setA->vUnion($setB); 57 | } 58 | 59 | public function testVintersectMethodIsNotSupportedForMaps() 60 | { 61 | $this->expectException(\BadMethodCallException::class); 62 | $this->expectExceptionMessage('vIntersect is not a supported method for Maps'); 63 | $setA = Map::create(['a' =>0, 'b' => 0]); 64 | $setB = Map::create(['c' =>0, 'd' => 0]); 65 | $setA->vIntersect($setB); 66 | } 67 | 68 | public function testVdiffMethodIsNotSupportedForMaps() 69 | { 70 | $this->expectException(\BadMethodCallException::class); 71 | $this->expectExceptionMessage('vDiff is not a supported method for Maps'); 72 | $setA = Map::create(['a' =>0, 'b' => 0]); 73 | $setB = Map::create(['c' =>0, 'd' => 0]); 74 | $setA->vDiff($setB); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/src/chippyash/Monad/MonadTest.php: -------------------------------------------------------------------------------- 1 | sut = $this->getMockForAbstractClass(Monad::class); 27 | } 28 | 29 | public function testYouCanReturnAValueWhenMonadCreatedWithSimpleValue() 30 | { 31 | $this->setSutValue('foo'); 32 | $this->assertEquals('foo', $this->sut->value()); 33 | } 34 | 35 | public function testYouCanReturnAValueWhenMonadCreatedWithMonadicValue() 36 | { 37 | $bound = $this->createMonadWithValue('foo'); 38 | $this->setSutValue($bound); 39 | $this->assertInstanceOf(Monadic::class, $this->sut->value()); 40 | } 41 | 42 | public function testYouCanUseAClosureForValue() 43 | { 44 | $this->setSutValue(function(){return 'foo';}); 45 | $this->assertInstanceOf(\Closure::class, $this->sut->value()); 46 | } 47 | 48 | public function testFlattenWillReturnBaseType() 49 | { 50 | $this->setSutValue('foo'); 51 | $this->assertEquals('foo', $this->sut->flatten()); 52 | $this->setSutValue(function(){return 'foo';}); 53 | $this->assertEquals('foo', $this->sut->flatten()); 54 | $this->setSutValue($this->createMonadWithValue('foo')); 55 | $this->assertEquals('foo', $this->sut->flatten()); 56 | } 57 | 58 | public function testYouCanBindAClosureOnAMonadToCreateANewMonadOfTheSameType() 59 | { 60 | $this->setSutValue('foo'); 61 | $fn = function($value){return $value;}; 62 | $this->assertEquals(get_class($this->sut), get_class($this->sut->bind($fn))); 63 | 64 | $this->setSutValue($this->createMonadWithValue('foo')); 65 | $this->assertEquals(get_class($this->sut), get_class($this->sut->bind($fn))); 66 | 67 | $this->setSutValue(function(){return 'foo';}); 68 | $this->assertEquals(get_class($this->sut), get_class($this->sut->bind($fn))); 69 | } 70 | 71 | public function testBindCanTakeOptionalAdditionalParameters() 72 | { 73 | $fn = function ($value, $fudge) { 74 | return $value . $fudge; 75 | }; 76 | $this->assertEquals(get_class($this->sut), get_class($this->sut->bind($fn, ['bar']))); 77 | } 78 | 79 | public function testMagicInvokeProxiesToBindMethodIfPassedAClosure() 80 | { 81 | $this->setSutValue('foo'); 82 | $fn = function ($value) { 83 | return $value . 'bar'; 84 | }; 85 | $sut = $this->sut; 86 | $this->assertEquals(get_class($sut), get_class($sut($fn))); 87 | } 88 | 89 | public function testMagicInvokeProxiesToValueMethodIfPassedNoParameters() 90 | { 91 | $this->setSutValue('foo'); 92 | $sut = $this->sut; 93 | $this->assertEquals('foo', $sut()); 94 | } 95 | 96 | public function testCallingMagicInvokeWillThrowExceptionIfNoMethodIsExecutable() 97 | { 98 | $this->expectException(\BadMethodCallException::class); 99 | $sut = $this->sut; 100 | $sut('foo'); 101 | } 102 | 103 | public function testYouCannotCreateAnAbstractMonadStatically() 104 | { 105 | $refl = new \ReflectionClass(Monad::class); 106 | $this->assertNull($refl->getConstructor()); 107 | } 108 | 109 | /** 110 | * Set value on the Monad SUT - Abstract Monad does not have a constructor 111 | * @param $value 112 | */ 113 | private function setSutValue($value) 114 | { 115 | $refl = new \ReflectionProperty($this->sut, 'value'); 116 | $refl->setAccessible(true); 117 | $refl->setValue($this->sut, $value); 118 | } 119 | 120 | /** 121 | * Create a Mock Monad with a value 122 | * 123 | * @param mixed $value 124 | * @return Monad Mock Monad 125 | */ 126 | private function createMonadWithValue($value) 127 | { 128 | $monad = $this->getMockForAbstractClass(Monad::class); 129 | $refl = new \ReflectionProperty($monad, 'value'); 130 | $refl->setAccessible(true); 131 | $refl->setValue($monad, $value); 132 | 133 | return $monad; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /test/src/chippyash/Monad/MutableCollectionTest.php: -------------------------------------------------------------------------------- 1 | sut = new MutableCollection(['foo', 'bar', 'baz']); 26 | } 27 | 28 | public function testYouCanUnsetAMutableCollectionMember() 29 | { 30 | unset($this->sut[2]); 31 | $this->assertEquals(['foo', 'bar'], $this->sut->toArray()); 32 | } 33 | 34 | public function testYouCanSetAMutableCollectionMember() 35 | { 36 | $this->sut[2] = 'bop'; 37 | $this->assertEquals(['foo', 'bar', 'bop'], $this->sut->toArray()); 38 | } 39 | 40 | /** 41 | * Simple test to ensure that we get back a MutableCollection as a result 42 | * of calling an underlaying Collection method as they all use `new static(...)` 43 | */ 44 | public function testCallingOneOfTheUnderlayingCollectionMethodsReturnsAMutableCollection() 45 | { 46 | $this->assertInstanceOf(MutableCollection::class, $this->sut->flip()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/src/chippyash/Monad/Option/NoneTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(None::class, new None()); 20 | } 21 | 22 | public function testYouCanConstructANoneWithAParameterAndItWillStillBeNone() 23 | { 24 | $this->assertInstanceOf(None::class, new None('foo')); 25 | } 26 | 27 | public function testCreateWillReturnANone() 28 | { 29 | $this->assertInstanceOf(None::class, None::create()); 30 | } 31 | 32 | public function testBindingANoneReturnsANone() 33 | { 34 | $none = new None(); 35 | $this->assertInstanceOf(None::class, $none->bind(function(){})); 36 | } 37 | 38 | public function testCallingGetOnANoneThrowsARuntimeException() 39 | { 40 | $this->expectException(\RuntimeException::class); 41 | None::create()->value(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/src/chippyash/Monad/Option/SomeTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Some::class, new Some('foo')); 21 | } 22 | 23 | public function testYouCannotConstructASomeWithNoValue() 24 | { 25 | try { 26 | new Some(); 27 | } catch (\Exception $e) { 28 | //php < 7.1 29 | $this->assertInstanceOf("PHPUnit_Framework_Error_Warning", $e); 30 | } catch (\ArgumentCountError $e) { 31 | //php >= 7.1 32 | $this->assertInstanceOf("ArgumentCountError", $e); 33 | } 34 | } 35 | 36 | public function testYouCanGetAValueFromASome() 37 | { 38 | $this->assertEquals('foo', (new Some('foo'))->value()); 39 | } 40 | 41 | public function testBindingOnASomeMayReturnASomeOrANone() 42 | { 43 | $this->assertInstanceOf(Some::class, (new Some('foo'))->bind(function($value){return $value;})); 44 | $this->assertInstanceOf(None::class, (new Some('foo'))->bind(function($value){return null;})); 45 | } 46 | 47 | public function testBindingOnASomeTakesAThirdNonetestValue() 48 | { 49 | $sut = new Some('foo'); 50 | $this->assertInstanceOf(Some::class, $sut->bind(function($value){return true;}, [], false)); 51 | $this->assertInstanceOf(None::class, $sut->bind(function($value){return false;}, [], false)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/src/chippyash/Monad/OptionTest.php: -------------------------------------------------------------------------------- 1 | assertNull($refl->getConstructor()); 23 | $this->assertTrue($refl->isAbstract()); 24 | } 25 | 26 | public function testCreatingWithAValueReturnsASome() 27 | { 28 | $sut = Option::create('foo'); 29 | $this->assertInstanceOf(Some::class, $sut); 30 | } 31 | 32 | public function testCreatingWithNoValueOrNullReturnsANone() 33 | { 34 | $sut = Option::create(); 35 | $this->assertInstanceOf(None::class, $sut); 36 | $sut = Option::create(null); 37 | $this->assertInstanceOf(None::class, $sut); 38 | } 39 | 40 | public function testYouCanReplaceNoneTestByCallingCreateWithAdditionalParameter() 41 | { 42 | $sut = Option::create(true, false); 43 | $this->assertInstanceOf(Some::class, $sut); 44 | $sut1 = Option::create(false, false); 45 | $this->assertInstanceOf(None::class, $sut1); 46 | } 47 | 48 | public function testGetOrElseWillReturnOptionValueIfOptionIsASome() 49 | { 50 | $sut = Option::create(true); 51 | $this->assertTrue($sut->getOrElse(false)); 52 | } 53 | 54 | public function testGetOrElseWillReturnElseValueIfOptionIsANone() 55 | { 56 | $sut = Option::create(true, true); 57 | $this->assertFalse($sut->getOrElse(false)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/src/chippyash/Monad/SetTest.php: -------------------------------------------------------------------------------- 1 | expectException(\RuntimeException::class); 19 | new Set([]); 20 | } 21 | 22 | public function testPassingInUniqueValuesAtConstructionWillCreateASet() 23 | { 24 | $this->assertInstanceOf(Set::class, new Set(['a', 'b', 'c'])); 25 | } 26 | 27 | public function testPassingInNonUniqueValuesAtConstructionWillCreateASetWithUniqueValues( 28 | ) 29 | { 30 | $sut = new Set(['a', 'b', 'c', 'a', 'b', 'c']); 31 | $this->assertEquals(['a', 'b', 'c'], $sut->toArray()); 32 | } 33 | 34 | public function testYouCanCreateSetsOfObjects() 35 | { 36 | $a = new \stdClass(); 37 | $a->val = 'a'; 38 | $b = new \stdClass(); 39 | $b->val = 'b'; 40 | $c = new \stdClass(); 41 | $c->val = 'c'; 42 | 43 | $this->assertInstanceOf(Set::class, new Set([$a, $b, $c])); 44 | 45 | $d = new \stdClass(); 46 | $d->val = 'a'; 47 | $e = new \stdClass(); 48 | $e->val = 'b'; 49 | 50 | $sut = new Set([$a, $b, $c, $d, $e]); 51 | $this->assertInstanceOf(Set::class, $sut); 52 | $test = array_map( 53 | function ($v) { 54 | return $v->val; 55 | }, 56 | $sut->toArray() 57 | ); 58 | $this->assertEquals(['a', 'b', 'c'], $test); 59 | } 60 | 61 | public function testYouCanCreateSetsOfResources() 62 | { 63 | $a = opendir(__DIR__); 64 | $b = opendir(__DIR__); 65 | $sut = new Set([$a, $b]); 66 | $this->assertInstanceOf(Set::class, $sut); 67 | closedir($a); 68 | closedir($b); 69 | } 70 | 71 | public function testValueIntersectionWillProduceASet() 72 | { 73 | $a = new \stdClass(); 74 | $a->val = 'a'; 75 | $b = new \stdClass(); 76 | $b->val = 'b'; 77 | $c = new \stdClass(); 78 | $c->val = 'c'; 79 | 80 | $setA = new Set([$a, $b]); 81 | $setB = new Set([$a, $c]); 82 | 83 | $test = array_map( 84 | function ($v) { 85 | return $v->val; 86 | }, 87 | $setA->vIntersect($setB)->toArray() 88 | ); 89 | $this->assertEquals(['a'], $test); 90 | } 91 | 92 | public function testValueUnionWillProduceASet() 93 | { 94 | $a = new \stdClass(); 95 | $a->val = 'a'; 96 | $b = new \stdClass(); 97 | $b->val = 'b'; 98 | $c = new \stdClass(); 99 | $c->val = 'c'; 100 | 101 | $setA = new Set([$a, $b]); 102 | $setB = new Set([$a, $c]); 103 | 104 | $test = array_map( 105 | function ($v) { 106 | return $v->val; 107 | }, 108 | $setA->vUnion($setB)->toArray() 109 | ); 110 | $this->assertEquals(['a', 'b', 'c'], $test); 111 | } 112 | 113 | public function testDiffWillProduceASet() 114 | { 115 | $a = new \stdClass(); 116 | $a->val = 'a'; 117 | $b = new \stdClass(); 118 | $b->val = 'b'; 119 | $c = new \stdClass(); 120 | $c->val = 'c'; 121 | 122 | $setA = new Set([$a, $b]); 123 | $setB = new Set([$a, $c]); 124 | 125 | $test = array_map( 126 | function ($v) { 127 | return $v->val; 128 | }, 129 | $setA->diff($setB)->toArray() 130 | ); 131 | 132 | $this->assertEquals(['b'], $test); 133 | } 134 | 135 | public function testYouCanBindAFunctionToTheEntireSetAndReturnASet() 136 | { 137 | $sut = Set::create([2, 3, 4, 5, 6]); 138 | //function returns a single value - converted to a collection 139 | $f = function ($c) { 140 | return $c[0]; 141 | }; 142 | $this->assertEquals([2], $sut->bind($f)->getArrayCopy()); 143 | $this->assertInstanceOf('Monad\Set', $sut->bind($f)); 144 | } 145 | 146 | public function testKintersectMethodIsNotSupportedForSets() 147 | { 148 | $this->expectException(\BadMethodCallException::class); 149 | $this->expectExceptionMessage('kIntersect is not a supported method for Sets'); 150 | $setA = Set::create([2, 3, 4, 5, 6]); 151 | $setB = Set::create([2, 3, 4, 5, 6]); 152 | $setA->kIntersect($setB); 153 | } 154 | 155 | public function testKunionMethodIsNotSupportedForSets() 156 | { 157 | $this->expectException(\BadMethodCallException::class); 158 | $this->expectExceptionMessage('kUnion is not a supported method for Sets'); 159 | $setA = Set::create([2, 3, 4, 5, 6]); 160 | $setB = Set::create([2, 3, 4, 5, 6]); 161 | $setA->kUnion($setB); 162 | } 163 | 164 | public function testKdiffMethodIsNotSupportedForSets() 165 | { 166 | $this->expectException(\BadMethodCallException::class); 167 | $this->expectExceptionMessage('kDiff is not a supported method for Sets'); 168 | $setA = Set::create([2, 3, 4, 5, 6]); 169 | $setB = Set::create([2, 3, 4, 5, 6]); 170 | $setA->kDiff($setB); 171 | } 172 | 173 | } 174 | --------------------------------------------------------------------------------