Repository: mtdowling/transducers.php Branch: master Commit: f3c025a7bc3c Files: 11 Total size: 78.5 KB Directory structure: gitextract_gybzcee7/ ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.rst ├── composer.json ├── phpunit.xml.dist ├── src/ │ └── transducers.php └── tests/ └── transducersTest.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ build/ export-ignore tests/ export-ignore vendor/ export-ignore .gitattributes export-ignore .gitignore export-ignore .travis.yml export-ignore CHANGELOG.md export-ignore Makefile export-ignore phpunit.xml.dist export-ignore README.rst export-ignore ================================================ FILE: .gitignore ================================================ phpunit.xml composer.lock vendor/ artifacts/ docs/_build .idea .DS_STORE ================================================ FILE: .travis.yml ================================================ language: php php: - 5.5 - 5.6 - hhvm before_script: - composer install script: make test matrix: allow_failures: - php: hhvm ================================================ FILE: CHANGELOG.md ================================================ # CHANGELOG ## 0.3.0 - 2014-01-12 * Updated `transducers\comp()` to work for any type of variadic function composition. ## 0.2.0 - 2014-12-07 * Renamed `transducers\seq()` to `transducers\xform()`. * Renamed `transducers\vec()` to `transducers\to_traversable()`. * Renamed `transducers\is_iterable()` to `transducers\is_traversable()`. * Added `transducers\to_fn()` so that transducers can be used with existing reduce functions like `array_reduce`. ## 0.1.0 - 2014-12-03 Initial release. ================================================ FILE: LICENSE ================================================ Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ all: clean test test: vendor/bin/phpunit coverage: vendor/bin/phpunit --coverage-html=artifacts/coverage view-coverage: open artifacts/coverage/index.html clean: rm -rf artifacts/* .PHONY: coverage ================================================ FILE: README.rst ================================================ =============== transducers-php =============== .. image:: https://badges.gitter.im/Join Chat.svg :target: https://gitter.im/mtdowling/transducers.php?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge `Transducers `_ are composable algorithmic transformations. They are independent from the context of their input and output sources and specify only the essence of the transformation in terms of an individual element. Because transducers are decoupled from input or output sources, they can be used in many different processes - collections, streams, channels, observables, etc. Transducers compose directly, without awareness of input or creation of intermediate aggregates. For more information about Clojure transducers and transducer semantics see the introductory `blog post `_ and this `video `_. You can transduce anything that you can iterate over in a foreach-loop (e.g., arrays, ``\Iterator``, ``Traversable``, ``Generator``, etc.). Transducers can be applied **eagerly** using ``transduce()``, ``into()``, ``to_array()``, ``to_assoc()``, ``to_string()``; and **lazily** using ``to_iter()``, ``xform()``, or by applying a transducer stream filter. :: composer.phar require mtdowling/transducers Defining Transformations With Transducers ----------------------------------------- Transducers compose with ordinary function composition. A transducer performs its operation before deciding whether and how many times to call the transducer it wraps. You can easily compose transducers to create transducer pipelines. The recommended way to compose transducers is with the ``transducers\comp()`` function: .. code-block:: php use Transducers as t; $xf = t\comp( t\drop(2), t\map(function ($x) { return $x + 1; }), t\filter(function ($x) { return $x % 2; }), t\take(3) ); The above composed transducer is a function that creates a pipeline for transforming data: it skips the first two elements of a collection, adds 1 to each value, filters out even numbers, then takes 3 elements from the collection. This new transformation function can be used with various transducer application functions, including ``xform()``. .. code-block:: php $data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; $result = t\xform($data, $xf); // Contains: [5, 7, 9] Transducers ----------- Transducers are functions that return a function that accept a reducing function array ``$xf`` and return a new reducing function array that wraps the provided ``$xf``. Here's how to create a transducer that adds ``$n`` to each value: .. code-block:: php $inc = function ($n = 1) { // Return a function that accepts a reducing function array $xf. return function (array $xf) use ($n) { // Return a new reducing function array that wraps $xf. return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($xf, $n) { return $xf['step']($result, $input + $n); } ]; } }; $result = t\xform([1, 2, 3], $inc(1)); // Contains: 2, 3, 4 .. _reducing-link: Reducing Function Array ----------------------- Reducing function arrays are PHP associative arrays that contain a 'init', 'step', and 'result' key that maps to a function. +--------+-------------------------+------------------------------------------+ | key | arguments | Description | +========+=========================+==========================================+ | init | none | Invoked to initialize a transducer. This | | | | function should call the 'init' function | | | | on the nested reducing function array | | | | ``$xf``, which will eventually call out | | | | to the transducing process. This function| | | | is only called when an initial value is | | | | not provided while transducing. | +--------+-------------------------+------------------------------------------+ | step | ``$result``, ``$input`` | This is a standard reduction function | | | | but it is expected to call the | | | | ``$xf['step']`` function 0 or more | | | | times as appropriate in the transducer. | | | | For example, ``filter`` will choose | | | | (based on the predicate) whether to call | | | | ``$xf`` or not. ``map`` will always call | | | | it exactly once. ``cat`` may call it | | | | many times depending on the inputs. | +--------+-------------------------+------------------------------------------+ | result | ``$result`` | Some processes will not end, but for | | | | those that do (like transduce), the | | | | 'result' function is used to produce | | | | a final value and/or flush state. This | | | | function must call the ``$xf['result']`` | | | | function exactly once. | +--------+-------------------------+------------------------------------------+ Using Transducers ----------------- Transducers can be used in any number of ways. This library provides several methods that can be used to apply transducers. transduce() ~~~~~~~~~~~ ``function transduce(callable $xf, array $step, $coll, $init = null)`` Transform and reduce $coll by applying $xf($step)['step'] to each value. - ``callable $xf``: Transducer function to apply. - ``array $step``: Transformer array that has 'init', 'result', and 'step' keys that map to a callable. - ``$coll``: Data to transform. Can be an array, iterator, or PHP stream resource. - ``$init``: Optional first initialization value of the reduction. If this value is not provided, the ``$step['init']()`` function will be called to provide a default value. .. code-block:: php use Transducers as t; $data = [[1, 2], [3, 4]]; $xf = t\comp( t\flatten(), t\filter(function ($value) { return $value % 2; }), ); $result = t\transduce($xf, t\array_reducer(), $data); // Contains: [1, 3] When using this function, you can use any of the built-in reducing function arrays as the ``$step`` argument: - ``transducers\array_reducer()``: Creates a reducing function array that appends values to an array. .. code-block:: php $data = [[1, 2], [3, 4]]; $result = t\transduce(t\flatten(), t\array_reducer(), $data); // Results contains [1, 2, 3, 4] - ``transducers\stream_reducer()``: Creates a reducing function array that writes values to a stream resource. If no ``$init`` value is provided when transducing then a PHP temp stream will be used. .. code-block:: php $data = [[1, 2], [3, 4]]; $result = t\transduce(t\flatten(), t\stream_reducer(), $data); fseek($result, 0); echo stream_get_contents($result); // Outputs: 1234 - ``transducers\string_reducer()``: Creates a reducing function array that concatenates each value to a string. .. code-block:: php $xf = t\flatten(); // use an optional joiner on the string reducer. $reducer = t\string_reducer('|'); $data = [[1, 2], [3, 4]]; $result = t\transduce($xf, $reducer, $data); // Result is '1|2|3|4' - ``transducers\assoc_reducer()``: Creates a reducing function array that adds key value pairs to an associative array. Each value must be an array that contains the array key in the first element and the array value in the second element. - ``transducers\create_reducer()``: Convenience function that can be used to quickly create reducing function arrays. The first and only required argument is a step function that takes the accumulated result and the new value and returns a single result. The next, optional, argument is the init function that takes no arguments an returns an initialized result. The next, optional, argument is the result function which takes a single result argument and is expected to return a final result. .. code-block:: php $result = t\transduce( t\flatten(), t\create_reducer(function ($r, $x) { return $r + $x; }), [[1, 2], [3, 4]] ); // Result is equal to 10 - ``transducers\operator_reducer()``: Creates a reducing function array that uses the provided infix operator to reduce the collection (i.e., $result $input). Supports: '.', '+', '-', '*', and '/' operators. .. code-block:: php $result = t\transduce( t\flatten() t\operator_reducer('+'), [[1, 2], [[3], 4]] ); // Result is equal to 10 xform() ~~~~~~~ ``function xform($coll, callable $xf)`` Returns the same data type passed in as ``$coll`` with ``$xf`` applied. ``xform()`` using the following logic when returning values: - ``array``: Returns an array using the provided array. - ``associative array``: Turn the provided array into an indexed array, meaning that each value passed to the ``step`` reduce function is an array where the first element is the key and the second element is the value. When completed, ``xform()`` returns an associative array. - ``\Iterator``: Returns an iterator in which ``$xf`` is applied lazily. - ``resource``: Reads single bytes from the provided value and returns a new fopen resource that contains the bytes from the input resource after applying ``$xf``. - ``string``: Passes each character from the string through to each step function and returns a string. .. code-block:: php // Give an array and get back an array $result = t\xform([1, false, 3], t\compact()); assert($result === [1, 3]); // Give an iterator and get back an iterator $result = t\xform(new ArrayIterator([1, false, 3]), t\compact()); assert($result instanceof \Iterator); // Give a stream and get back a stream. $stream = fopen('php://temp', 'w+'); fwrite($stream, '012304'); rewind($stream); $result = t\xform($stream, t\compact()); assert($result == '1234'); // Give a string and get back a string $result = t\xform('abc', t\map(function ($v) { return strtoupper($v); })); assert($result === 'abc'); // Give an associative array and get back an associative array. $data = ['a' => 1, 'b' => 2]; $result = t\xform('abc', t\map(function ($v) { return [strtoupper($v[0]), $v[1]]; })); assert($result === ['A' => 1, 'B' => 2]); into() ~~~~~~ ``function into($target, $coll, callable $xf)`` Transduces items from ``$coll`` into the given ``$target``, in essence "pouring" transformed data from one source into another data type. This function does not attempt to discern between arrays and associative arrays. Any array or ArrayAccess object provided will be treated as an indexed array. When a string is provided, each value will be concatenated to the end of the string with no separator. When an fopen resource is provided, data will be written to the end of the stream with no separator between writes. .. code-block:: php use Transducers as t; // Compose a transducer function. $transducer = t\comp( // Remove a single level of nesting. 'transducers\cat', // Filter out even values. t\filter(function ($value) { return $value % 2; }), // Multiply each value by 2 t\map(function ($value) { return $value * 2; }), // Immediately stop when the value is >= 15. t\take_while(function($value) { return $value < 15; }) ); $data = [[1, 2, 3], [4, 5], [6], [], [7], [8, 9, 10, 11]]; // Eagerly pour the transformed data, [2, 6, 10, 14], into an array. $result = t\into([], $data, $transducer); to_iter() ~~~~~~~~~ ``function to_iter($coll, callable $xf)`` Creates an iterator that **lazily** applies the transducer ``$xf`` to the ``$input`` iterator. Use this function when dealing with large amounts of data or when you want operations to occur only as needed. .. code-block:: php // Generator that yields incrementing numbers. $forever = function () { $i = 0; while (true) { yield $i++; } }; // Create a transducer that multiplies each value by two and takes // ony 100 values. $xf = t\comp( t\map(function ($value) { return $value * 2; }), t\take(100) ); foreach (t\to_iter($forever(), $xf) as $value) { echo $value; } to_array() ~~~~~~~~~~ ``function to_array($iterable, callable $xf)`` Converts a value to an array and applies a transducer function. ``$iterable`` is passed through ``to_traversable()`` in order to convert the input value into an array. .. code-block:: php .. code-block:: php $result = t\to_array( 'abc', t\map(function ($v) { return strtoupper($v); }) ); // Contains: ['A', 'B', 'C'] to_assoc() ~~~~~~~~~~ ``function to_assoc($iterable, callable $xf)`` Creates an associative array using the provided input while applying ``$xf`` to each value. Values are converted to arrays that contain the array key in the first element and the array value in the second. .. code-block:: php $result = t\to_assoc( ['a' => 1, 'b' => 2], t\map(function ($v) { return [$v[0], $v[1] + 1]; }) ); assert($result == ['a' => 2, 'b' => 3]); to_string() ~~~~~~~~~~~ ``function to_string($iterable, callable $xf)`` Converts a value to a string and applies a transducer function to each character. ``$iterable`` is passed through ``to_traversable()`` in order to convert the input value into an array. .. code-block:: php echo t\to_string( ['a', 'b', 'c'], t\map(function ($v) { return strtoupper($v); }) ); // Outputs: ABC to_fn() ~~~~~~~ ``function to_fn(callable $xf, callable|array $builder = null)`` Convert a transducer into a function that can be used with existing reduce implementations (e.g., array_reduce). .. code-block:: php $xf = t\map(function ($x) { return $x + 1; }); $fn = t\to_fn($xf); // $builder is optional $result = array_reduce([1, 2, 3], $fn); assert($result == [2, 3, 4]); $fn = t\to_fn($xf, t\string_reducer()); $result = array_reduce([1, 2, 3], $fn); assert($result == '234'); Stream Filter ~~~~~~~~~~~~~ You can apply transducers to PHP streams using a `stream filter `_. This library registers a ``transducers`` stream filter that can be appended or prepended to a PHP stream using the ``transducers\append_stream_filter()`` or ``transducers\prepend_stream_filter()`` functions. .. code-block:: php use transducers as t; $f = fopen('php://temp', 'w+'); fwrite($f, 'testing. Can you hear me?'); rewind($f); $xf = t\comp( // Split by words t\words(), // Uppercase/lowercase every other word. t\keep_indexed(function ($i, $v) { return $i % 2 ? strtoupper($v) : strtolower($v); }), // Combine words back together into a string separated by ' '. t\interpose(' ') ); // Apply a transducer stream filter. $filter = t\append_stream_filter($f, $xf, STREAM_FILTER_READ); echo stream_get_contents($f); // Be sure to remove the filter to flush out any buffers. stream_filter_remove($filter); echo stream_get_contents($f); fclose($f); // Echoes: "testing. CAN you HEAR me?" Available Transducers --------------------- map() ~~~~~ ``function map(callable $f)`` Applies a map function ``$f`` to each value in a collection. .. code-block:: php $data = ['a', 'b', 'c']; $xf = t\map(function ($value) { return strtoupper($value); }); assert(t\xform($data, $xf) == ['A', 'B', 'C']); filter() ~~~~~~~~ ``function filter(callable $pred)`` Filters values that do not satisfy the predicate function ``$pred``. .. code-block:: php $data = [1, 2, 3, 4]; $odd = function ($value) { return $value % 2; }; $result = t\xform($data, t\filter($odd)); assert($result == [1, 3]); remove() ~~~~~~~~ ``function remove(callable $pred)`` Removes anything from a sequence that satisfied ``$pred``. .. code-block:: php $data = [1, 2, 3, 4]; $odd = function ($value) { return $value % 2; }; $result = t\xform($data, t\remove($odd)); assert($result == [2, 4]); cat() ~~~~~ ``function cat()`` Transducer that concatenates items from nested lists. Note that ``cat()`` is used differently than other transducers: you use cat using the string value of the function name (i.e., ``'transducers\cat'``); .. code-block:: php $xf = 'transducers\cat'; $data = [[1, 2], [3], [], [4, 5]]; $result = t\xform($data, $xf); assert($result == [1, 2, 3, 4, 5]); mapcat() ~~~~~~~~ ``function mapcat(callable $f)`` Applies a map function to a collection and concats them into one less level of nesting. .. code-block:: php $data = [[1, 2], [3], [], [4, 5]]; $xf = t\mapcat(function ($value) { return array_sum($value); }); $result = t\xform($data, $xf); assert($result == [3, 3, 0, 9]); flatten() ~~~~~~~~~ ``function flatten()`` Takes any nested combination of sequential things and returns their contents as a single, flat sequence. .. code-block:: php $data = [[1, 2], 3, [4, new ArrayObject([5, 6])]]; $xf = t\flatten(); $result = t\to_array($data, $xf); assert($result == [1, 2, 3, 4, 5, 6]); partition() ~~~~~~~~~~~ ``function partition($size)`` Partitions the source into arrays of size ``$size``. When the reducing function array completes, the array will be stepped with any remaining items. .. code-block:: php $data = [1, 2, 3, 4, 5]; $result = t\xform($data, t\partition(2)); assert($result == [[1, 2], [3, 4], [5]]); partition_by() ~~~~~~~~~~~~~~ ``function partition_by(callable $pred)`` Split inputs into lists by starting a new list each time the predicate passed in evaluates to a different condition (true/false) than what holds for the present list. .. code-block:: php $data = [['a', 1], ['a', 2], [2, 3], ['c', 4]]; $xf = t\partition_by(function ($v) { return is_string($v[0]); }); $result = t\into([], $data, $xf); assert($result == [ [['a', 1], ['a', 2]], [[2, 3]], [['c', 4]] ]); take() ~~~~~~ ``function take($n);`` Takes ``$n`` number of values from a collection. .. code-block:: php $data = [1, 2, 3, 4, 5]; $result = t\xform($data, t\take(2)); assert($result == [1, 2]); take_while() ~~~~~~~~~~~~ ``function take_while(callable $pred)`` Takes from a collection while the predicate function ``$pred`` returns true. .. code-block:: php $data = [1, 2, 3, 4, 5]; $xf = t\take_while(function ($value) { return $value < 4; }); $result = t\xform($data, $xf); assert($result == [1, 2, 3]); take_nth() ~~~~~~~~~~ ``function take_nth($nth)`` Takes every nth item from a sequence of values. .. code-block:: php $data = [1, 2, 3, 4, 5, 6]; $result = t\xform($data, t\take_nth(2)); assert($result == [1, 3, 5]); drop() ~~~~~~ ``function drop($n)`` Drops ``$n`` items from the beginning of the input sequence. .. code-block:: php $data = [1, 2, 3, 4, 5]; $result = t\xform($data, t\drop(2)); assert($result == [3, 4, 5]); drop_while() ~~~~~~~~~~~~ ``function drop_while(callable $pred)`` Drops values from a sequence so long as the predicate function ``$pred`` returns true. .. code-block:: php $data = [1, 2, 3, 4, 5]; $xf = t\drop_while(function ($value) { return $value < 3; }); $result = t\xform($data, $xf); assert($result == [3, 4, 5]); replace() ~~~~~~~~~ ``function replace(array $smap)`` Given a map of replacement pairs and a collection, returns a sequence where any elements equal to a key in ``$smap`` are replaced with the corresponding ``$smap`` value. .. code-block:: php $data = ['hi', 'there', 'guy', '!']; $xf = t\replace(['hi' => 'You', '!' => '?']); $result = t\xform($data, $xf); assert($result == ['You', 'there', 'guy', '?']); keep() ~~~~~~ ``function keep(callable $f)`` Keeps ``$f`` items for which ``$f`` does not return null. .. code-block:: php $result = t\xform( [0, false, null, true], t\keep(function ($value) { return $value; }) ); assert($result == [0, false, true]); keep_indexed() ~~~~~~~~~~~~~~ ``function keep_indexed(callable $f)`` Returns a sequence of the non-null results of ``$f($index, $input)``. .. code-block:: php $result = t\xform( [0, false, null, true], t\keep_indexed(function ($index, $input) { echo $index . ':' . json_encode($input) . ', '; return $input; }) ); assert($result == [0, false, true]); // Will echo: 0:0, 1:false, 2:null, 3:true, dedupe() ~~~~~~~~ ``function dedupe()`` Removes duplicates that occur in order (keeping the first in a sequence of duplicate values). .. code-block:: php $result = t\xform( ['a', 'b', 'b', 'c', 'c', 'c', 'b'], t\dedupe() ); assert($result == ['a', 'b', 'c', 'b']); interpose() ~~~~~~~~~~~ ``function interpose($separator)`` Adds a separator between each item in the sequence. .. code-block:: php $result = t\xform(['a', 'b', 'c'], t\interpose('-')); assert($result == ['a', '-', 'b', '-', 'c']); tap() ~~~~~ ``function tap(callable $interceptor)`` Invokes interceptor with each result and item, and then steps through unchanged. The primary purpose of this method is to "tap into" a method chain, in order to perform operations on intermediate results within the chain. Executes interceptor with current result and item. .. code-block:: php // echo each value as it passes through the tap function. $tap = t\tap(function ($r, $x) { echo $x . ', '; }); t\xform( ['a', 'b', 'c'], t\comp( $tap, t\map(function ($v) { return strtoupper($v); }), $tap ) ); // Prints: a, A, b, B, c, C, compact() ~~~~~~~~~ ``function compact()`` Trim out all falsey values. .. code-block:: php $result = t\xform(['a', true, false, 'b', 0], t\compact()); assert($result == ['a', true, 'b']); words() ~~~~~~~ ``function words($maxBuffer = 4096)`` Splits the input by words. You can provide an optional max buffer length that will ensure the buffer size used to find words is never exceeded. The default max buffer length is 4096. To use an unbounded buffer, provide ``INF``. .. code-block:: php $xf = t\words(); $data = ['Hi. This is a test.']; $result = t\xform($data, $xf); assert($result == ['Hi.', 'This', 'is', 'a', 'test.']); $data = ['Hi. ', 'This is', ' a test.']; $result = t\xform($data, $xf); assert($result == ['Hi.', 'This', 'is', 'a', 'test.']); lines() ~~~~~~~ ``function lines($maxBuffer = 10240000)`` Splits the input by lines. You can provide an optional max buffer length that will ensure the buffer size used to find lines is never exceeded. The default max buffer length is 10MB. To use an unbounded buffer, provide ``INF``. .. code-block:: php $xf = t\lines(); $data = ["Hi.\nThis is a test."]; $result = t\xform($data, $xf); assert($result == ['Hi.', 'This is a test.']); $data = ["Hi.\n", 'This is', ' a test.', "\nHear me?"]; $result = t\xform($data, $xf); assert($result == ['Hi.', 'This is a test.', 'Hear me?']); Utility Functions ----------------- identity() ~~~~~~~~~~ ``function indentity($value)`` Returns the provided value. This is useful for writing reducing function arrays that do not need to modify an 'init' or 'result' function. In these cases, you can simply use the string ``'transducers\identity'`` as the 'init' or 'result' function to continue to proxy to further reducers. assoc_iter() ~~~~~~~~~~~~ ``function assoc_iter($iterable)`` Converts an iterable into an indexed array iterator where each value yielded is an array containing the key followed by the value. .. code-block:: php $data = ['a' => 1, 'b' => 2]; assert(t\assoc_iter($data) == [['a', 1], ['b', 2]]; This can be combined with the ``assoc_reducer()`` to generate associative arrays. .. code-block:: php $result = t\transduce( t\map(function ($v) { return [$v[0], $v[1] + 1]; }), t\assoc(), t\assoc_iter(['a' => 1, 'b' => 2]) ); assert($result == ['a' => 2, 'b' => 3]); You should really just use the ``t\to_assoc()`` function if you know you're reducing an associative array. .. code-block:: php $result = t\to_assoc( ['a' => 1, 'b' => 2], t\map(function ($v) { return [$v[0], $v[1] + 1]; }) ); assert($result == ['a' => 2, 'b' => 3]); stream_iter() ~~~~~~~~~~~~~ ``function stream_iter($stream, $size = 1)`` Creates an iterator that reads from a stream using the given ``$size`` argument. .. code-block:: php $s = fopen('php://temp', 'w+'); fwrite($s, 'foo'); rewind($s); // outputs: foo foreach (t\stream_iter($s) as $char) { echo $char; } rewind($s); // outputs: fo-o foreach (t\stream_iter($s, 2) as $char) { echo $char . '-'; } to_traversable() ~~~~~~~~~~~~~~~~ ``function to_traversable($value)`` Converts an input value into something this is traversable (e.g., an array or ``\Iterator``). This function accepts arrays, ``\Traversable``, PHP streams, and strings. Arrays pass through unchanged. Associative arrays are returned as iterators that yield arrays where each value is an array that contains the key of the array in the first element and the value of the array in the second element. Iterators are returned as-is. Strings are split by character using ``str_split()``. PHP streams are converted into iterators that yield a single byte at a time. is_traversable() ~~~~~~~~~~~~~~~~ ``function is_traversable($coll)`` Returns true if the provided $coll is something that can be traversed in a foreach loop. This function treats arrays, instances of ``\Traversable``, and ``stdClass`` as iterable. reduce() ~~~~~~~~ ``function reduce(callable $fn, $coll, $accum = null)`` Reduces the given iterable using the provided reduce function $fn. The reduction is short-circuited if $fn returns an instance of Reduced. ================================================ FILE: composer.json ================================================ { "name": "mtdowling/transducers", "license": "MIT", "authors": [ { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" } ], "require": { "php": ">=5.5.0" }, "require-dev": { "phpunit/phpunit": "~4.0" }, "autoload": { "files": ["src/transducers.php"] } } ================================================ FILE: phpunit.xml.dist ================================================ tests src ================================================ FILE: src/transducers.php ================================================ 'Transducers\identity', 'result' => 'Transducers\identity', 'step' => function ($result, $input) use (&$items) { $items[] = $input; return $result; } ]); $result = $reducer['init'](); foreach ($iterable as $input) { $result = $reducer['step']($result, $input); // Yield each queued value from the step function. while ($items) { yield array_shift($items); } // Break early if a Reduced is found. if ($result instanceof Reduced) { break; } } // Allow reducers to step on the final result. $reducer['result']($result); while ($items) { yield array_shift($items); } } /** * Converts a value to an array using a transducer function. * * @param mixed $coll Value to convert. * @param callable $xf Transducer to apply. * * @return array * @throws \InvalidArgumentException */ function to_array($coll, callable $xf) { return transduce($xf, array_reducer(), to_traversable($coll), []); } /** * Converts a value to an associative array using a transducer function. * * Do not provide an indexed array (i.e., [[0, 1], [1, 1], [2, 2]]) as this * function will do that for you. Note that values yielded through each * transducer will be an array where element 0 is the associative array key and * element 1 is the associative array value. * * @param mixed $coll Value to convert. * @param callable $xf Transducer to apply. * * @return array Returns an associative array. * @throws \InvalidArgumentException */ function to_assoc($coll, callable $xf) { return transduce($xf, assoc_reducer(), assoc_iter($coll), []); } /** * Reduces a value to a string by concatenating each step value to a string. * * @param mixed $coll Value to convert. * @param callable $xf Transducer to apply. * * @return string * @throws \InvalidArgumentException */ function to_string($coll, callable $xf) { return transduce($xf, string_reducer(), to_traversable($coll), ''); } /** * Transduces items from $coll into the given $target, in essence "pouring" * transformed data from one source into another data type. * * This function does not attempt to discern between arrays and associative * arrays. Any array or ArrayAccess object provided will be treated as an * indexed array. When a string is provided, each value will be concatenated to * the end of the string with no separator. When an fopen resource is provided, * data will be written to the end of the stream with no separator between * writes. * * @param array|\ArrayAccess|resource|string $target Where items are appended. * @param mixed $coll Sequence of data * @param callable $xf Transducer function. * * @return mixed * @throws \InvalidArgumentException */ function into($target, $coll, callable $xf) { if (is_array($target) || $target instanceof \ArrayAccess) { return transduce($xf, array_reducer(), $coll, $target); } elseif (is_resource($target)) { return transduce($xf, stream_reducer(), $coll, $target); } elseif (is_string($target)) { return transduce($xf, string_reducer(), $coll, $target); } throw type_error('into', $coll); } /** * Returns the same data type passed in as $coll with $xf applied. * * This function will turn associative arrays into a stream of arrays that * contain the array key in the first element and values in the second element. * * @param array|\Iterator|resource $coll Data to transform. * @param callable $xf Transducer to apply. * @return mixed * @throws \InvalidArgumentException */ function xform($coll, callable $xf) { if (is_array($coll)) { reset($coll); return key($coll) === 0 ? transduce($xf, array_reducer(), $coll, []) : transduce($xf, assoc_reducer(), assoc_iter($coll), []); } elseif ($coll instanceof \Iterator) { return to_iter($coll, $xf); } elseif (is_resource($coll)) { register_stream_filter(); stream_filter_append($coll, 'transducer', STREAM_FILTER_READ, $xf); return $coll; } elseif (is_string($coll)) { return transduce($xf, string_reducer(), str_split($coll)); } throw type_error('xform', $coll); } /** * Transform and reduce $coll by applying $xf($step)['step'] to each value. * * Returns the result of applying the transformed $xf to 'init' and the first * item in the $coll, then applying $xf to that result and the second item, * etc. If $coll contains no items, returns init and $f is not called. * * @param callable $xf Transducer function. * @param array $step Transformation array that contains an 'init', 'result', * and 'step' keys mapping to functions. * @param mixed $coll The iterable collection to transform. * @param mixed $init The first initialization value of the reduction. * * @return mixed */ function transduce(callable $xf, array $step, $coll, $init = null) { if ($init === null) { $init = $step['init'](); } $reducer = $xf($step); return $reducer['result'](reduce($reducer['step'], $coll, $init)); } /** * Convert a transducer into a function that can be used with existing reduce * implementations (e.g., array_reduce). * * @param callable $xf Transducer * @param callable|array $builder Reducing function array or a step function * that takes an accumulator value and the next * input and returns a new accumulator value. If * none is provided, an array_reducer is used. * @return mixed */ function to_fn(callable $xf, $builder = null) { if (!$builder) { $builder = array_reducer(); } elseif (is_callable($builder)) { $builder = create_reducer($builder); } return $xf($builder)['step']; } //----------------------------------------------------------------------------- // Transducers //----------------------------------------------------------------------------- /** * Applies a map function $f to each value in a collection. * * @param callable $f Map function to apply. * * @return callable */ function map(callable $f) { return function (array $xf) use ($f) { return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($xf, $f) { return $xf['step']($result, $f($input)); } ]; }; } /** * Filters values that do not satisfy the predicate function $pred. * * @param callable $pred Function that accepts a value and returns true/false * * @return callable */ function filter(callable $pred) { return function (array $xf) use ($pred) { return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($pred, $xf) { return $pred($input) ? $xf['step']($result, $input) : $result; }, ]; }; } /** * Removes anything from a sequence that satisfied $pred * * @param callable $pred Function that accepts a value and returns true/false * * @return callable */ function remove(callable $pred) { return filter(function ($x) use ($pred) { return !($pred($x)); }); } /** * Concatenates items from nested lists. * * @param array $xf Reducing function array. * * @return callable */ function cat(array $xf) { return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($xf) { if (!is_traversable($input)) { return $xf['step']($result, $input); } foreach ($input as $value) { $result = $xf['step']($result, $value); } return $result; } ]; } /** * Applies a map function to a collection and cats them into one less level of * nesting. * * @param callable $f Map function * * @return callable */ function mapcat(callable $f) { return comp(map($f), 'transducers\cat'); } /** * Takes any nested combination of sequential things and returns their contents * as a single, flat sequence. * * @return callable */ function flatten() { return function (array $xf) { return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($xf) { if (!is_traversable($input)) { return $xf['step']($result, $input); } $it = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($input)); foreach ($it as $value) { $result = $xf['step']($result, $value); } return $result; } ]; }; } /** * Partitions the input sequence into partitions of the specified size. * * @param int $size Size to make each partition (except possibly the last chunk) * * @return callable */ function partition($size) { return function (array $xf) use ($size) { $buffer = []; return [ 'init' => $xf['init'], 'result' => function ($result) use (&$buffer, $xf) { if ($buffer) { $result = unreduced($xf['step']($result, $buffer)); } return $xf['result']($result); }, 'step' => function ($result, $input) use ($xf, &$buffer, $size) { $buffer[] = $input; if (count($buffer) == $size) { $result = $xf['step']($result, $buffer); $buffer = []; return $result; } return $result; } ]; }; } /** * Split inputs into lists by starting a new list each time the predicate * passed in evaluates to a different condition (true/false) than what holds * for the present list. * * @param callable $pred Function that returns a new value to partition by. * * @return callable */ function partition_by(callable $pred) { return function (array $xf) use ($pred) { $ctx = []; return [ 'init' => $xf['init'], 'result' => function ($result) use (&$ctx, $xf) { // Add any pending elements. if (!empty($ctx['buffer'])) { $result = unreduced($xf['step']($result, $ctx['buffer'])); } return $xf['result']($result); }, 'step' => function ($result, $input) use ($xf, &$ctx, $pred) { $test = $pred($input); if (!$ctx) { $ctx['last'] = $test; $ctx['buffer'] = [$input]; } elseif ($ctx['last'] !== $test) { $ctx['last'] = $test; if (!empty($ctx['buffer'])) { $buffer = $ctx['buffer']; $ctx['buffer'] = [$input]; return $xf['step']($result, $buffer); } } else { $ctx['buffer'][] = $input; } return $result; } ]; }; } /** * Takes $n number of values from a collection. * * @param int $n Number of value to take * * @return callable */ function take($n) { return function (array $xf) use ($n) { $remaining = $n; return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($r, $input) use (&$remaining, $xf) { $r = $xf['step']($r, $input); return --$remaining > 0 ? $r : ensure_reduced($r); } ]; }; } /** * Takes from a collection while the predicate function $pred returns true. * * @param callable $pred Function that accepts a value and returns true/false * * @return callable */ function take_while(callable $pred) { return function (array $xf) use ($pred) { return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($pred, $xf) { return $pred($input) ? $xf['step']($result, $input) : ensure_reduced($result); } ]; }; } /** * Takes every nth item from a sequence of values. * * @param int $nth The nth value to take * * @return callable */ function take_nth($nth) { return function (array $xf) use ($nth) { $i = 0; return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($xf, &$i, $nth) { return $i++ % $nth ? $result : $xf['step']($result, $input); } ]; }; } /** * Drops $n items from the beginning of the input sequence. * * @param int $n Number of items to drop * * @return callable */ function drop($n) { return function (array $xf) use ($n) { $remaining = $n; return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($xf, &$remaining) { return $remaining-- > 0 ? $result : $xf['step']($result, $input); } ]; }; } /** * Drops values from a sequence so long as the predicate function $pred * returns true. * * @param callable $pred Predicate that accepts a value and returns true/false * * @return callable */ function drop_while(callable $pred) { return function (array $xf) use ($pred) { $trigger = false; return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($xf, $pred, &$trigger) { if ($trigger) { // No longer dropping. return $xf['step']($result, $input); } elseif (!$pred($input)) { // Predicate failed so stop dropping. $trigger = true; return $xf['step']($result, $input); } // Currently dropping return $result; } ]; }; } /** * Given a map of replacement pairs and a collection, returns a sequence where * any elements equal to a key in $smap are replaced with the corresponding * $smap value. * * @param array $smap Search term mapping to a replacement value. * * @return callable */ function replace(array $smap) { return function (array $xf) use ($smap) { return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($xf, $smap) { return isset($smap[$input]) ? $xf['step']($result, $smap[$input]) : $xf['step']($result, $input); } ]; }; } /** * Keeps $f items for which $f does not return null. * * @param callable $f Function that accepts a value and returns null|mixed. * * @return callable */ function keep(callable $f) { return function (array $xf) use ($f) { return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($xf, $f) { $value = $f($input); return $value !== null ? $xf['step']($result, $value) : $result; } ]; }; } /** * Returns a sequence of the non-null results of $f($index, $input). * * @param callable $f Function that accepts an index and an item and returns * a value. Anything other than null is kept. * @return callable */ function keep_indexed(callable $f) { return function (array $xf) use ($f) { $idx = 0; return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($xf, $f, &$idx) { $value = $f($idx++, $input); return $value !== null ? $xf['step']($result, $value) : $result; } ]; }; } /** * Removes duplicates that occur in order (keeping the first in a sequence of * duplicate values). * * @return callable */ function dedupe() { return function (array $xf) { $outer = []; return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($xf, &$outer) { if (!array_key_exists('prev', $outer) || $outer['prev'] !== $input ) { $outer['prev'] = $input; return $xf['step']($result, $input); } return $result; } ]; }; } /** * Adds a separator between each item in the sequence. * * @param mixed $separator Separator to interpose * * @return callable */ function interpose($separator) { return function (array $xf) use ($separator) { $triggered = 0; return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($xf, $separator, &$triggered) { if (!$triggered) { $triggered = true; return $xf['step']($result, $input); } return $xf['step']($xf['step']($result, $separator), $input); } ]; }; } /** * Trim out all falsey values. * * @return callable */ function compact() { return function (array $xf) { return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($xf) { return $input ? $xf['step']($result, $input) : $result; } ]; }; } /** * Invokes interceptor with each result and item, and then steps through * unchanged. * * The primary purpose of this method is to "tap into" a method chain, in order * to perform operations on intermediate results within the chain. Executes * interceptor with current result and item. * * @param callable $interceptor * * @return callable */ function tap(callable $interceptor) { return function (array $xf) use ($interceptor) { return [ 'init' => $xf['init'], 'result' => $xf['result'], 'step' => function ($result, $input) use ($xf, $interceptor) { $interceptor($result, $input); return $xf['step']($result, $input); } ]; }; } /** * Splits the input each time a character is matched. Will only buffer up to * $maxBuffer before flushing. * * @param array $chars Characters to split on. * @param int $maxBuffer Maximum buffer size. Defaults to 10MB. * * @return callable */ function split(array $chars, $maxBuffer = 10240000) { $chars = array_fill_keys($chars, true); return function (array $xf) use ($chars, $maxBuffer) { $buffer = ''; return [ 'init' => $xf['init'], 'result' => function ($result) use (&$buffer, $xf) { if (strlen($buffer)) { $result = unreduced($xf['step']($result, $buffer)); } return $xf['result']($result); }, 'step' => function ($result, $input) use ($xf, $chars, $maxBuffer, &$buffer) { $input = (string) $input; for ($i = 0, $t = strlen($input); $i < $t; $i++) { $c = $input[$i]; if (!isset($chars[$c])) { $buffer .= $c; } if (isset($chars[$c]) || strlen($buffer) >= $maxBuffer) { $data = $buffer; $buffer = ''; $result = $xf['step']($result, $data); } } return $result; } ]; }; } /** * Splits the input by lines, and does not buffer more than $maxBuffer. * * @param int $maxBuffer Maximum buffer size. Defaults to 10MB. * * @return callable */ function lines($maxBuffer = 10240000) { return split([PHP_EOL], $maxBuffer); } /** * Splits inputs by words and does not buffer more than $maxBuffer before * flushing. * * @param int $maxBuffer Maximum buffer size. Defaults to 4096. * * @return callable */ function words($maxBuffer = 4096) { static $boundary; if (!$boundary) { $boundary = [' ', "\f", "\n", "\r", "\t", "\v", json_decode('\u00A0'), json_decode('\u2028'), json_decode('\u2029') ]; } return split($boundary, $maxBuffer); } //----------------------------------------------------------------------------- // Reducers //----------------------------------------------------------------------------- /** * Creates a reducing function array that appends values to an array or object * that implements {@see ArrayAccess}. * * @return array Returns a reducing function array. */ function array_reducer() { return [ 'init' => function () { return []; }, 'result' => 'Transducers\identity', 'step' => function ($result, $input) { $result[] = $input; return $result; } ]; } /** * Creates a hash map reducing function array that merges values into an * associative array. * * This reducer assumes that the provided value is an array where the key is * in the first index and the value is in the second index. * * @return array Returns a reducing function array. */ function assoc_reducer() { return [ 'init' => function () { return []; }, 'result' => 'Transducers\identity', 'step' => function ($result, $input) { $result[$input[0]] = $input[1]; return $result; } ]; } /** * Creates a stream reducing function array for PHP stream resources. * * @return array Returns a reducing function array. */ function stream_reducer() { return [ 'init' => function () { return fopen('php://temp', 'w+'); }, 'result' => 'Transducers\identity', 'step' => function ($result, $input) { fwrite($result, $input); return $result; } ]; } /** * Creates a string reducing function array that concatenates values into a * string. * * @param string $joiner Optional string to concatenate between each value. * * @return array Returns a reducing function array. */ function string_reducer($joiner = '') { return [ 'init' => function () { return ''; }, 'result' => 'Transducers\identity', 'step' => function ($r, $x) use ($joiner) { return $r . $joiner . $x; } ]; } /** * Creates a reducing function array that uses the provided infix operator to * reduce the collection (i.e., $result $input). * * Supports: '.', '+', '-', '*', and '/' operators. * * @param string $operator Infix operator to use. * * @return array Returns a reducing function array. */ function operator_reducer($operator) { static $reducers; if (!$reducers) { $reducers = [ '.' => function ($r, $x) { return $r . $x; }, '+' => function ($r, $x) { return $r + $x; }, '-' => function ($r, $x) { return $r - $x; }, '*' => function ($r, $x) { return $r * $x; }, '/' => function ($r, $x) { return $r / $x; } ]; } if (!isset($reducers[$operator])) { throw new \InvalidArgumentException("A reducer is not defined for {$operator}"); } return [ 'init' => 'Transducers\identity', 'result' => 'Transducers\identity', 'step' => $reducers[$operator] ]; } /** * Convenience function for creating a reducing function array. * * @param callable $step Step function that accepts $accum, $input and * returns a new reduced value. * @param callable $init Optional init function invoked with no argument to * initialize the reducing function. * @param callable $result Optional result function invoked with a single * argument that is expected to return a result. * * @return array Returns a reducing function array. */ function create_reducer(callable $step, callable $init = null, callable $result = null) { return [ 'init' => $init ?: function () {}, 'result' => $result ?: 'Transducers\identity', 'step' => $step ]; } //----------------------------------------------------------------------------- // Utility functions //----------------------------------------------------------------------------- /** * Composes the provided variadic function arguments into a single function. * * comp($f, $g) // returns $f($g(x)) * * Passing a single function will return the passed function. Passing no * functions will return an identity function. Passing two or more functions * will return a function that accepts variadic arguments for the last * function, and the result of this function is passed to the second to last * function and so on. * * @return callable */ function comp() { $fns = func_get_args(); if (!$fns) { // Passing no values will return an identity function. return 'transducers\\identity'; } elseif (!isset($fns[1])) { // Passing a single function will return the function passed. return $fns[0]; } /** @var callable $fn */ $fn = array_pop($fns); $total = count($fns); return function ($a = null, $b = null) use ($fn, $fns, $total) { $passed = func_num_args(); if ($passed === 1) { $value = $fn($a); } elseif ($passed === 2) { $value = $fn($a, $b); } elseif ($passed === 0) { $value = $fn(); } else { $value = call_user_func_array($fn, func_get_args()); } $i = $total; while (--$i > -1) { $value = $fns[$i]($value); } return $value; }; } /** * Reduces the given iterable using the provided reduce function $fn. The * reduction is short-circuited if $fn returns an instance of Reduced. * * @param callable $fn Reduce function. * @param mixed $coll Iterable data to transform. * @param mixed $accum Initial accumulated value. * @return mixed Returns the reduced value */ function reduce(callable $fn, $coll, $accum = null) { foreach ($coll as $input) { $accum = $fn($accum, $input); if ($accum instanceof Reduced) { return $accum->value; } } return $accum; } /** * Converts a value into a sequence of data that can be foreach'ed * * When provided an indexed array, the array is returned as-is. When provided * an associative array, an iterator is returned where each value is an array * containing the [key, value]. When a stream is provided, an iterator is * returned that yields bytes from the stream. When an iterator is provided, * it is returned as-is. To force an iterator to be an indexed iterator, you * must use the assoc_iter() function. * * @param array|\Iterator|resource $value Data to convert to a sequence. * * @return array|\Iterator * @throws \InvalidArgumentException */ function to_traversable($value) { switch (gettype($value)) { case 'array': reset($value); return key($value) === 0 ? $value : assoc_iter($value); case 'object': if ($value instanceof \Traversable || $value instanceof \stdClass) { return $value; } break; case 'string': return str_split($value); case 'resource': return stream_iter($value); } throw type_error('to_traversable', $value); } /** * Returns true if the provided $coll is something that can be iterated in a * foreach loop. * * This function treats arrays, instances of \Traversable, and stdClass as * iterable. * * @param mixed $value * * @return bool */ function is_traversable($value) { return is_array($value) || $value instanceof \Traversable || $value instanceof \stdClass; } /** * Returns the provided Reduced or wraps the value in a Reduced. * * @param mixed|Reduced $r Value to ensure is reduced. * * @return Reduced */ function ensure_reduced($r) { return $r instanceof Reduced ? $r : new Reduced($r); } /** * Unwraps a reduced variable if necessary. * * @param mixed|Reduced $r Value to unwrap if needed. * * @return mixed */ function unreduced($r) { return $r instanceof Reduced ? $r->value : $r; } /** * Returns the provided value. * * @param mixed $value Value to return * * @return mixed */ function identity($value = null) { return $value; } /** * Converts an iterable into an indexed array iterator where each value yielded * is an array containing the key followed by the value. * * @param mixed $iterable Value to convert to an indexed iterator * * @return \Iterator */ function assoc_iter($iterable) { foreach ($iterable as $key => $value) { yield [$key, $value]; } } /** * Creates an iterator that reads from a stream. * * @param resource $stream fopen() resource. * @param int $size Number of bytes to read for each read. Defaults to 1. * * @return \Iterator */ function stream_iter($stream, $size = 1) { while (!feof($stream)) { yield fread($stream, $size); } } /** * @param string $name Name of the function that was called. * @param mixed $coll Data that was provided. * * @return \InvalidArgumentException */ function type_error($name, $coll) { if (is_object($coll)) { $desc = get_class($coll); } else { ob_start(); var_dump($coll); $desc = ob_get_clean(); } return new \InvalidArgumentException("Do not know how to $name $desc"); } //----------------------------------------------------------------------------- // Utility classes //----------------------------------------------------------------------------- class Reduced { public $value; public function __construct($value) { $this->value = $value; } } //----------------------------------------------------------------------------- // Streams //----------------------------------------------------------------------------- /** * Appends a transducer filter to an open stream. * * @param resource $stream Stream to add a filter to. * @param callable $xf Transducer function. * @param int $readWrite Constants available on PHP's stream_filter_append * * @return resource Returns the appended stream filter resource. */ function append_stream_filter($stream, callable $xf, $readWrite) { register_stream_filter(); return stream_filter_append($stream, 'transducer', $readWrite, $xf); } /** * Prepends a transducer filter to an open stream. * * @param resource $stream Stream to add a filter to. * @param callable $xf Transducer function. * @param int $readWrite Constants available on PHP's stream_filter_prepend * * @return resource Returns the appended stream filter resource. */ function prepend_stream_filter($stream, callable $xf, $readWrite) { register_stream_filter(); return stream_filter_prepend($stream, 'transducer', $readWrite, $xf); } /** * Registers the 'transducer' stream filter. */ function register_stream_filter() { stream_filter_register('transducer', 'transducers\StreamFilter'); } /** * Implements transducer functionality in PHP stream filters. */ class StreamFilter extends \php_user_filter { private $xf; private $buffer; private $bufferHandle; public function onCreate() { if (!is_callable($this->params)) { trigger_error('Filter params arg must be a transducer function'); return false; } $reducer = create_reducer(function($r, $x) { $this->buffer .= $x; }); $this->xf = call_user_func($this->params, $reducer); return true; } public function onClose() { if (is_resource($this->bufferHandle)) { fclose($this->bufferHandle); } } function filter($in, $out, &$consumed, $closing) { $result = ''; while ($bucket = stream_bucket_make_writeable($in)) { // Stream each byte through the step function. for ($i = 0, $t = strlen($bucket->data); $i < $t; $i++) { $consumed++; $result = $this->xf['step']($result, $bucket->data[$i]); if ($result instanceof Reduced) { break; } } // A transducer may choose to not use the provided input. if (strlen($this->buffer)) { $bucket->data = $this->buffer; $this->buffer = ''; stream_bucket_append($out, $bucket); } } // When closing, we allow the $xf['result'] function to add more data. if ($closing) { $this->xf['result'](''); if (strlen($this->buffer)) { // The buffer is only needed when the result fn calls the step. $this->bufferHandle = fopen('php://memory', 'w+'); $bucket = stream_bucket_new($this->bufferHandle, $this->buffer); stream_bucket_append($out, $bucket); } } return PSFS_PASS_ON; } } ================================================ FILE: tests/transducersTest.php ================================================ assertEquals(3, $x); return $x + 1; }; $b = function ($x) { $this->assertEquals(1, $x); return $x + 2; }; $c = t\comp($a, $b); $this->assertEquals(4, $c(1)); } public function testEnsuresReduced() { $r = t\ensure_reduced(1); $this->assertEquals(1, $r->value); $r = t\ensure_reduced($r); $this->assertEquals(1, $r->value); } public function testReturnsIdentity() { $this->assertEquals(1, t\identity(1)); } public function testReturnsAppendXform() { $xf = t\array_reducer(); $this->assertEquals([], $xf['init']()); $this->assertSame([10, 1], $xf['step']([10], 1)); $this->assertSame([10], $xf['result']([10])); } public function testReturnsStreamXform() { $xf = t\stream_reducer(); $res = $xf['init'](); $this->assertInternalType('resource', $res); $this->assertSame($res, $xf['step']($res, 'a')); fseek($res, 0); $this->assertEquals('a', stream_get_contents($res)); $this->assertSame($res, $xf['result']($res)); fclose($res); } public function testTransformStreamWithxform() { $stream = fopen('php://temp', 'w+'); fwrite($stream, '012304'); rewind($stream); $result = t\xform($stream, t\compact()); rewind($result); $this->assertEquals('1234', stream_get_contents($result)); } public function testSeqAppliesToIterator() { $xf = t\compact(); $data = new \ArrayIterator([1, false, 2, null]); $iter = t\xform($data, $xf); $this->assertInstanceOf('Generator', $iter); $this->assertEquals([1, 2], iterator_to_array($iter)); } public function testSeqAppliesToString() { $xf = t\map(function ($v) { return strtoupper($v); }); $data = 'foo'; $this->assertSame('FOO', t\xform($data, $xf)); } /** * @expectedExceptionMessage Do not know how to xform bool(false) * @expectedException \InvalidArgumentException */ public function testSeqThrowsWhenUnknownDataType() { t\xform(false, t\compact()); } public function testCompactTrimsFalseyValues() { $data = [0, false, true, 10, ' ', 'a']; $result = t\into([], $data, t\compact()); $this->assertEquals([true, 10, ' ', 'a'], $result); } public function testTapsIntoReduce() { $data = ['a', 'b', 'c']; $res = []; $result = t\into([], $data, t\tap(function ($r, $x) use (&$res) { $res[] = $x; })); $this->assertSame($res, $result); } public function testInterposes() { $data = ['a', 'b', 'c']; $result = t\into([], $data, t\interpose('-')); $this->assertEquals(['a', '-', 'b', '-', 'c'], $result); } public function testRemovesDuplicates() { $data = ['a', 'b', 'b', 'c', 'c', 'c', 'b']; $result = t\into([], $data, t\dedupe()); $this->assertEquals(['a', 'b', 'c', 'b'], $result); } public function testMaps() { $data = ['a', 'b', 'c']; $xf = t\map(function ($value) { return strtoupper($value); }); $result = t\into([], $data, $xf); $this->assertEquals(['A', 'B', 'C'], $result); } public function testFilters() { $data = [1, 2, 3, 4]; $odd = function ($value) { return $value % 2; }; $result = t\into([], $data, t\filter($odd)); $this->assertEquals([1, 3], $result); } public function testRemoves() { $data = [1, 2, 3, 4]; $odd = function ($value) { return $value % 2; }; $result = t\into([], $data, t\remove($odd)); $this->assertEquals([2, 4], $result); } public function testCats() { $data = [[1, 2], 3, [], [4, 5]]; $result = t\into([], $data, 'transducers\cat'); $this->assertEquals($result, [1, 2, 3, 4, 5]); } public function testMapCats() { $data = [[1, 2], [3], [], [4, 5]]; $xf = t\mapcat(function ($value) { return array_sum($value); }); $result = t\into([], $data, $xf); $this->assertEquals($result, [3, 3, 0, 9]); } public function testFlattensIterables() { $data = [[1, 2], [3, [4, 5, new \ArrayObject([6, 7])]], [], [8, 9]]; $result = t\into([], $data, t\flatten()); $this->assertEquals($result, [1, 2, 3, 4, 5, 6, 7, 8, 9]); } public function testFlattenSkipsNonIterables() { $data = ['abc']; $result = t\into([], $data, t\flatten()); $this->assertEquals($result, ['abc']); } public function testPartitions() { $data = [1, 2, 3, 4, 5]; $xf = t\partition(2); $result = t\into([], $data, $xf); $this->assertEquals($result, [[1, 2], [3, 4], [5]]); } public function testPartitionsByPredicate() { $data = [['a', 1], ['a', 2], ['a', 3], [2, 4], ['c', 5]]; $xf = t\partition_by(function ($v) { return is_string($v[0]); }); $result = t\into([], $data, $xf); $this->assertEquals( $result, [[['a', 1], ['a', 2], ['a', 3]], [[2, 4]], [['c', 5]]] ); } public function testTakes() { $data = [1, 2, 3, 4, 5]; $result = t\xform($data, t\take(2)); $this->assertEquals($result, [1, 2]); } public function testDrops() { $data = [1, 2, 3, 4, 5]; $result = t\xform($data, t\drop(2)); $this->assertEquals($result, [3, 4, 5]); } public function testTakesNth() { $data = [1, 2, 3, 4, 5, 6]; $result = t\xform($data, t\take_nth(2)); $this->assertEquals($result, [1, 3, 5]); } public function testTakesWhile() { $data = [1, 2, 3, 4, 5]; $xf = t\take_while(function ($value) { return $value < 4; }); $result = t\xform($data, $xf); $this->assertEquals($result, [1, 2, 3]); } public function testDropsWhile() { $data = [1, 2, 3, 4, 5]; $xf = t\drop_while(function ($value) { return $value < 3; }); $result = t\xform($data, $xf); $this->assertEquals($result, [3, 4, 5]); } public function testReplaces() { $data = ['hi', 'there', 'guy', '!']; $xf = t\replace(['hi' => 'You', '!' => '?']); $result = t\xform($data, $xf); $this->assertEquals($result, ['You', 'there', 'guy', '?']); } public function testKeeps() { $data = [0, false, null, true]; $xf = t\keep(function ($value) { return $value; }); $result = t\xform($data, $xf); $this->assertEquals([0, false, true], $result); } public function testKeepsWithIndex() { $data = [0, false, null, true]; $calls = []; $xf = t\keep_indexed(function ($idx, $item) use (&$calls) { $calls[] = [$idx, $item]; return $item; }); $result = t\xform($data, $xf); $this->assertEquals([0, false, true], $result); $this->assertEquals([[0, 0], [1, false], [2, null], [3, true]], $calls); } public function testToTraversableReturnsArrays() { $this->assertEquals([1, 2, 3], t\to_traversable([1, 2, 3])); $this->assertEquals( [['a', 1], ['b', 2]], iterator_to_array(t\to_traversable(['a' => 1, 'b' => 2])) ); } public function testToTraversableReturnsStreamsIter() { $s = fopen('php://temp', 'w+'); fwrite($s, 'foo'); rewind($s); $this->assertEquals( ['f', 'o','o'], iterator_to_array(t\to_traversable($s)) ); fclose($s); } public function testToTraversableReturnsStringAsArray() { $this->assertEquals(['f', 'o','o'], t\to_traversable('foo')); } public function testToTraversableReturnsIteratorAsIs() { $i = new \ArrayIterator([1, 2]); $this->assertSame($i, t\to_traversable($i)); } /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage Do not know how to to_traversable bool(false) */ public function testToTraversableEnsuresItCanHandleType() { t\to_traversable(false); } public function testConvertsToArray() { $this->assertEquals( [1, 2], t\to_array([1, 2], t\compact()) ); $this->assertEquals( [1, 2], t\to_array(new \ArrayIterator([1, 2]), t\compact()) ); $this->assertEquals( ['a', 'b'], t\to_array('ab', t\compact()) ); } /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage Do not know how to to_traversable int(1) */ public function testConvertsToArrayThrowsWhenInvalidType() { t\to_array(1, function () {}); } public function testReducedConstructor() { $r = new t\Reduced('foo'); $this->assertEquals('foo', $r->value); } public function testCreatesReducers() { $t = t\create_reducer( function ($r, $x) { return $r . $x; }, function () { return ''; } ); $this->assertEquals('', $t['init']()); $this->assertEquals('ab', $t['step']('a', 'b')); $this->assertEquals('foo', $t['result']('foo')); } public function testChecksIfTraversable() { $this->assertTrue(t\is_traversable([1, 2])); $this->assertTrue(t\is_traversable(new \ArrayObject([1, 2]))); $this->assertTrue(t\is_traversable(new \ArrayIterator([1, 2]))); $this->assertTrue(t\is_traversable(new \stdClass())); $this->assertFalse(t\is_traversable('a')); } public function testHasOperatorReducer() { $xf = t\compact(); $data = [1, 2, 3]; $this->assertEquals(6, t\transduce($xf, t\operator_reducer('+'), $data)); $this->assertEquals(-6, t\transduce($xf, t\operator_reducer('-'), $data)); $this->assertEquals(0, t\transduce($xf, t\operator_reducer('*'), $data)); $this->assertEquals(6, t\transduce($xf, t\operator_reducer('*'), $data, 1)); $this->assertEquals(0.16666666666666666, t\transduce($xf, t\operator_reducer('/'), $data, 1)); $this->assertEquals('123', t\transduce($xf, t\operator_reducer('.'), $data)); } /** * @expectedException \InvalidArgumentException */ public function testEnsuresOperatorIsValid() { t\operator_reducer('!'); } public function testReducesToString() { $xf = t\map(function ($v) { return strtoupper($v); }); $data = ['a', 'b', 'c']; $this->assertEquals('ABC', t\transduce($xf, t\string_reducer(), $data)); } public function testReducesToAssoc() { $xf = t\map(function ($v) { return [strtoupper($v[0]), $v[1]]; }); $data = ['a' => 1, 'b' => 2]; $result = t\transduce($xf, t\assoc_reducer(), t\assoc_iter($data)); $this->assertEquals(['A' => 1, 'B' => 2], $result); } public function testSplitsWords() { $this->assertEquals( ['hi', 'there', 'guy!'], t\xform(["hi\nthere", " guy!"], t\words()) ); $this->assertEquals( ['hi', 'the', 're', 'guy', '!'], t\xform(["hi\nthere", " guy!"],t\words(3)) ); } public function testSplitsLines() { $this->assertEquals( ['hi', 'there guy!'], t\xform(["hi\nthere", " guy!"], t\lines()) ); $this->assertEquals( ['hi', 'there', ' guy!'], t\xform(["hi\nthere", " guy!"], t\lines(5)) ); } //----------------------------------------------------------------------------- // Stream tests //----------------------------------------------------------------------------- private function stream($str) { $fp = fopen('php://memory', 'r+'); fwrite($fp, $str); rewind($fp); return $fp; } public function addsProvider() { return [[true], [false]]; } /** * @dataProvider addsProvider */ public function testAddsWhenWriting($append) { $called = []; $fp = fopen('php://memory', 'r+'); $xf = t\map(function ($v) use (&$called) { $called[] = $v; return strtoupper($v); }); if ($append) { t\append_stream_filter($fp, $xf, STREAM_FILTER_WRITE); } else { t\prepend_stream_filter($fp, $xf, STREAM_FILTER_WRITE); } fwrite($fp, 'foo'); fwrite($fp, 'bar'); $this->assertEquals(['f', 'o', 'o', 'b', 'a', 'r'], $called); rewind($fp); $this->assertEquals('FOOBAR', stream_get_contents($fp)); } public function testAddsWhenReading() { $called = []; $fp = $this->stream('foobar'); $xf = t\map(function ($v) use (&$called) { $called[] = $v; return strtoupper($v); }); $filter = t\append_stream_filter($fp, $xf, STREAM_FILTER_READ); $this->assertInternalType('resource', $filter); $this->assertEquals('stream filter', get_resource_type($filter)); $this->assertEquals('FOOBAR', stream_get_contents($fp)); $this->assertEquals(['f', 'o', 'o', 'b', 'a', 'r'], $called); } public function testCanEarlyTerminate() { $fp = $this->stream('foobar'); $called = []; $xf = t\comp( t\take(3), t\tap(function ($r, $x) use (&$called) { $called[] = $x; }) ); t\append_stream_filter($fp, $xf, STREAM_FILTER_READ); $this->assertEquals('foo', stream_get_contents($fp)); $this->assertEquals(['f', 'o', 'o'], $called); } public function testCanStepInClosing() { $fp = $this->stream('hi there guy'); $xf = t\comp( t\partition_by(function ($v) { return $v !== ' '; }), t\filter(function ($v) { return $v !== [' ']; }), t\keep_indexed(function ($i, $v) { $str = implode('', $v); if ($i % 2) { return strtoupper($str); } else { return strtolower($str); } }), t\interpose(' ') ); $filter = t\append_stream_filter($fp, $xf, STREAM_FILTER_READ); $this->assertEquals('hi THERE', stream_get_contents($fp)); // Note that the last bit requires the filter to be removed! stream_filter_remove($filter); $this->assertEquals(' guy', fread($fp, 100)); } /** * @expectedException \PHPUnit_Framework_Error_Notice * @expectedExceptionMessage Filter params arg must be a transducer function */ public function testEnsuresXfIscallable() { $fp = $this->stream('foo'); t\register_stream_filter(); stream_filter_append($fp, 'transducer', STREAM_FILTER_READ); } public function testToFn() { $xf = t\map(function ($x) { return $x + 1; }); $fn = t\to_fn($xf, t\string_reducer()); $result = array_reduce([1, 2, 3], $fn); $this->assertEquals('234', $result); } }