Full Code of mtdowling/transducers.php for AI

master f3c025a7bc3c cached
11 files
78.5 KB
21.1k tokens
111 symbols
1 requests
Download .txt
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 <mtdowling@gmail.com>

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 <http://clojure.org/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 <http://blog.cognitect.com/blog/2014/8/6/transducers-are-coming>`_
and this `video <https://www.youtube.com/watch?v=6mTbuzafcII>`_.

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 <operator> $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 <http://php.net/manual/en/stream.filters.php>`_.
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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./vendor/autoload.php"
         colors="true">
    <testsuites>
        <testsuite>
            <directory>tests</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist>
            <directory suffix=".php">src</directory>
        </whitelist>
    </filter>
</phpunit>


================================================
FILE: src/transducers.php
================================================
<?php
namespace transducers;

/**
 * Lazily applies the transducer $xf to the $input iterator.
 *
 * @param mixed    $iterable Iterable input to transform.
 * @param callable $xf       Transducer to apply.
 *
 * @return \Iterator Returns an iterator that lazily applies transformations.
 */
function to_iter($iterable, callable $xf)
{
    $items = [];
    $reducer = $xf([
        'init'   => '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 <operator> $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
================================================
<?php
namespace transducers\Tests;

use transducers as t;

class functionsTest extends \PHPUnit_Framework_TestCase
{
    public function testComposesFunctions()
    {
        $a = function ($x) {
            $this->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);
    }
}
Download .txt
gitextract_gybzcee7/

├── .gitattributes
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.rst
├── composer.json
├── phpunit.xml.dist
├── src/
│   └── transducers.php
└── tests/
    └── transducersTest.php
Download .txt
SYMBOL INDEX (111 symbols across 2 files)

FILE: src/transducers.php
  function to_iter (line 12) | function to_iter($iterable, callable $xf)
  function to_array (line 55) | function to_array($coll, callable $xf)
  function to_assoc (line 74) | function to_assoc($coll, callable $xf)
  function to_string (line 88) | function to_string($coll, callable $xf)
  function into (line 111) | function into($target, $coll, callable $xf)
  function xform (line 135) | function xform($coll, callable $xf)
  function transduce (line 170) | function transduce(callable $xf, array $step, $coll, $init = null)
  function to_fn (line 191) | function to_fn(callable $xf, $builder = null)
  function map (line 213) | function map(callable $f)
  function filter (line 233) | function filter(callable $pred)
  function remove (line 255) | function remove(callable $pred)
  function cat (line 267) | function cat(array $xf)
  function mapcat (line 292) | function mapcat(callable $f)
  function flatten (line 303) | function flatten()
  function partition (line 330) | function partition($size)
  function partition_by (line 364) | function partition_by(callable $pred)
  function take (line 405) | function take($n)
  function take_while (line 427) | function take_while(callable $pred)
  function take_nth (line 449) | function take_nth($nth)
  function drop (line 472) | function drop($n)
  function drop_while (line 496) | function drop_while(callable $pred)
  function replace (line 528) | function replace(array $smap)
  function keep (line 550) | function keep(callable $f)
  function keep_indexed (line 573) | function keep_indexed(callable $f)
  function dedupe (line 596) | function dedupe()
  function interpose (line 623) | function interpose($separator)
  function compact (line 647) | function compact()
  function tap (line 672) | function tap(callable $interceptor)
  function split (line 695) | function split(array $chars, $maxBuffer = 10240000)
  function lines (line 734) | function lines($maxBuffer = 10240000)
  function words (line 747) | function words($maxBuffer = 4096)
  function array_reducer (line 770) | function array_reducer()
  function assoc_reducer (line 791) | function assoc_reducer()
  function stream_reducer (line 808) | function stream_reducer()
  function string_reducer (line 828) | function string_reducer($joiner = '')
  function operator_reducer (line 849) | function operator_reducer($operator)
  function create_reducer (line 885) | function create_reducer(callable $step, callable $init = null, callable ...
  function comp (line 911) | function comp()
  function reduce (line 953) | function reduce(callable $fn, $coll, $accum = null)
  function to_traversable (line 980) | function to_traversable($value)
  function is_traversable (line 1008) | function is_traversable($value)
  function ensure_reduced (line 1022) | function ensure_reduced($r)
  function unreduced (line 1034) | function unreduced($r)
  function identity (line 1046) | function identity($value = null)
  function assoc_iter (line 1059) | function assoc_iter($iterable)
  function stream_iter (line 1074) | function stream_iter($stream, $size = 1)
  function type_error (line 1087) | function type_error($name, $coll)
  class Reduced (line 1103) | class Reduced
    method __construct (line 1107) | public function __construct($value)
  function append_stream_filter (line 1126) | function append_stream_filter($stream, callable $xf, $readWrite)
  function prepend_stream_filter (line 1141) | function prepend_stream_filter($stream, callable $xf, $readWrite)
  function register_stream_filter (line 1150) | function register_stream_filter()
  class StreamFilter (line 1158) | class StreamFilter extends \php_user_filter
    method onCreate (line 1164) | public function onCreate()
    method onClose (line 1176) | public function onClose()
    method filter (line 1183) | function filter($in, $out, &$consumed, $closing)

FILE: tests/transducersTest.php
  class functionsTest (line 6) | class functionsTest extends \PHPUnit_Framework_TestCase
    method testComposesFunctions (line 8) | public function testComposesFunctions()
    method testEnsuresReduced (line 24) | public function testEnsuresReduced()
    method testReturnsIdentity (line 32) | public function testReturnsIdentity()
    method testReturnsAppendXform (line 37) | public function testReturnsAppendXform()
    method testReturnsStreamXform (line 45) | public function testReturnsStreamXform()
    method testTransformStreamWithxform (line 57) | public function testTransformStreamWithxform()
    method testSeqAppliesToIterator (line 67) | public function testSeqAppliesToIterator()
    method testSeqAppliesToString (line 76) | public function testSeqAppliesToString()
    method testSeqThrowsWhenUnknownDataType (line 87) | public function testSeqThrowsWhenUnknownDataType()
    method testCompactTrimsFalseyValues (line 92) | public function testCompactTrimsFalseyValues()
    method testTapsIntoReduce (line 99) | public function testTapsIntoReduce()
    method testInterposes (line 109) | public function testInterposes()
    method testRemovesDuplicates (line 116) | public function testRemovesDuplicates()
    method testMaps (line 123) | public function testMaps()
    method testFilters (line 131) | public function testFilters()
    method testRemoves (line 139) | public function testRemoves()
    method testCats (line 147) | public function testCats()
    method testMapCats (line 154) | public function testMapCats()
    method testFlattensIterables (line 162) | public function testFlattensIterables()
    method testFlattenSkipsNonIterables (line 169) | public function testFlattenSkipsNonIterables()
    method testPartitions (line 176) | public function testPartitions()
    method testPartitionsByPredicate (line 184) | public function testPartitionsByPredicate()
    method testTakes (line 195) | public function testTakes()
    method testDrops (line 202) | public function testDrops()
    method testTakesNth (line 209) | public function testTakesNth()
    method testTakesWhile (line 216) | public function testTakesWhile()
    method testDropsWhile (line 224) | public function testDropsWhile()
    method testReplaces (line 232) | public function testReplaces()
    method testKeeps (line 240) | public function testKeeps()
    method testKeepsWithIndex (line 248) | public function testKeepsWithIndex()
    method testToTraversableReturnsArrays (line 261) | public function testToTraversableReturnsArrays()
    method testToTraversableReturnsStreamsIter (line 270) | public function testToTraversableReturnsStreamsIter()
    method testToTraversableReturnsStringAsArray (line 282) | public function testToTraversableReturnsStringAsArray()
    method testToTraversableReturnsIteratorAsIs (line 287) | public function testToTraversableReturnsIteratorAsIs()
    method testToTraversableEnsuresItCanHandleType (line 297) | public function testToTraversableEnsuresItCanHandleType()
    method testConvertsToArray (line 302) | public function testConvertsToArray()
    method testConvertsToArrayThrowsWhenInvalidType (line 322) | public function testConvertsToArrayThrowsWhenInvalidType()
    method testReducedConstructor (line 327) | public function testReducedConstructor()
    method testCreatesReducers (line 333) | public function testCreatesReducers()
    method testChecksIfTraversable (line 344) | public function testChecksIfTraversable()
    method testHasOperatorReducer (line 353) | public function testHasOperatorReducer()
    method testEnsuresOperatorIsValid (line 368) | public function testEnsuresOperatorIsValid()
    method testReducesToString (line 373) | public function testReducesToString()
    method testReducesToAssoc (line 380) | public function testReducesToAssoc()
    method testSplitsWords (line 390) | public function testSplitsWords()
    method testSplitsLines (line 402) | public function testSplitsLines()
    method stream (line 418) | private function stream($str)
    method addsProvider (line 426) | public function addsProvider()
    method testAddsWhenWriting (line 434) | public function testAddsWhenWriting($append)
    method testAddsWhenReading (line 454) | public function testAddsWhenReading()
    method testCanEarlyTerminate (line 469) | public function testCanEarlyTerminate()
    method testCanStepInClosing (line 484) | public function testCanStepInClosing()
    method testEnsuresXfIscallable (line 511) | public function testEnsuresXfIscallable()
    method testToFn (line 518) | public function testToFn()
Condensed preview — 11 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (84K chars).
[
  {
    "path": ".gitattributes",
    "chars": 250,
    "preview": "build/ export-ignore\ntests/ export-ignore\nvendor/ export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n.t"
  },
  {
    "path": ".gitignore",
    "chars": 73,
    "preview": "phpunit.xml\ncomposer.lock\nvendor/\nartifacts/\ndocs/_build\n.idea\n.DS_STORE\n"
  },
  {
    "path": ".travis.yml",
    "chars": 144,
    "preview": "language: php\n\nphp:\n  - 5.5\n  - 5.6\n  - hhvm\n\nbefore_script:\n  - composer install\n\nscript: make test\n\nmatrix:\n  allow_fa"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 499,
    "preview": "# CHANGELOG\n\n## 0.3.0 - 2014-01-12\n\n* Updated `transducers\\comp()` to work for any type of variadic function\n  compositi"
  },
  {
    "path": "LICENSE",
    "chars": 1111,
    "preview": "Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling <mtdowling@gmail.com>\n\nPermission is hereby granted, fr"
  },
  {
    "path": "Makefile",
    "chars": 207,
    "preview": "all: clean test\n\ntest:\n\tvendor/bin/phpunit\n\ncoverage:\n\tvendor/bin/phpunit --coverage-html=artifacts/coverage\n\nview-cover"
  },
  {
    "path": "README.rst",
    "chars": 27061,
    "preview": "===============\ntransducers-php\n===============\n\n.. image:: https://badges.gitter.im/Join Chat.svg\n        :target: http"
  },
  {
    "path": "composer.json",
    "chars": 363,
    "preview": "{\n  \"name\": \"mtdowling/transducers\",\n  \"license\": \"MIT\",\n  \"authors\": [\n    {\n      \"name\": \"Michael Dowling\",\n      \"em"
  },
  {
    "path": "phpunit.xml.dist",
    "chars": 355,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit bootstrap=\"./vendor/autoload.php\"\n         colors=\"true\">\n    <testsuite"
  },
  {
    "path": "src/transducers.php",
    "chars": 34582,
    "preview": "<?php\nnamespace transducers;\n\n/**\n * Lazily applies the transducer $xf to the $input iterator.\n *\n * @param mixed    $it"
  },
  {
    "path": "tests/transducersTest.php",
    "chars": 15735,
    "preview": "<?php\nnamespace transducers\\Tests;\n\nuse transducers as t;\n\nclass functionsTest extends \\PHPUnit_Framework_TestCase\n{\n   "
  }
]

About this extraction

This page contains the full source code of the mtdowling/transducers.php GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 11 files (78.5 KB), approximately 21.1k tokens, and a symbol index with 111 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!