Last week: Functions Pipeline
To the Kata description
Scroll down to this weeks Kata description
My first implementation in PHP was a Pipe
class with __invoke
method:
class Pipe { /** * @var \callable[] */ private $callables; private function __construct(callable ...$callables) { $this->callables = $callables; } public static function create(callable ...$callables) : Pipe { return new self(...$callables); } public function __invoke() { return array_reduce( $this->callables, function(array $result, callable $next) { return [$next(...$result)]; }, func_get_args() )[0]; } }
I made it possible to be created without any callables. The behavior was not specified but I choose to let the pipe return the first argument in that case.
I tested that it works with all kind of callables:
- Function name:
'strtolower'
- Invokable class, using a mock:
$callable = $this->getMockBuilder(\stdClass::class) ->setMethods(['__invoke']) ->getMock();
- Object method, with a similar mock:
[$object, 'method']
. In hindsight, this would have been a good case for anonymous classes.
The next time I simplified it a lot: First, I always required at least one callable and separated the first one from the rest, which made the function (no class this time) much clearer.
use function array_reduce as reduce; function pipe(callable $f, callable ...$fs) : callable { return function(...$args) use ($f, $fs) { return reduce( $fs, function($result, $next) { return $next($result); }, $f(...$args) ); }; }
But also the tests became much simpler. With the callable
type hint I can assume that all types of callables work the same way, so I only used simple core functions. Learning: try to keep the tests simple as well!
class PipeTest extends \PHPUnit_Framework_TestCase { public function testPipeSingleFunction() { $strlen = pipe('strlen'); $this->assertEquals(5, $strlen('abcde')); } public function testPipeSingleFunctionMultipleArguments() { $explode = pipe('explode'); $this->assertEquals(['a', 'b'], $explode('.', 'a.b')); } public function testPipeMultipleFunctions() { $wordcount = pipe('explode', 'count'); $this->assertEquals(2, $wordcount(' ', 'hello world')); } }
When I implemented the alternative version, compose()
, which works from right to left, the tests turned out to be similar in the end but the implementation always ended up using recursion, which seemed to be a much better fit for this case:
use function array_shift as shift; function compose(callable $f, callable ...$fs) : callable { if (empty($fs)) { return $f; } return function(...$args) use ($f, $fs) { return $f(compose(shift($fs), ...$fs)(...$args)); }; }
Finally, I gave the pipe a try with Ruby once more. I learned that blocks (like in map{|x| x + 1}
) are not functions and cannot be passed around, instead Ruby has procs and lambdas, but it’s still a bit different from real higher order functions. From a testing perspective, it was nothing new:
def pipe f, *fs proc do |*x| fs.reduce(f.(*x)){|res,nxt| nxt.(res)} end end class PipeTest < Minitest::Test def test_single_function plus_one_pipe = pipe( lambda {|x| x + 1} ) assert_equal 2, plus_one_pipe.(1) end def test_multiple_functions plus_one_to_string_pipe = pipe( lambda {|x| x + 1}, lambda {|x| x.to_s * 2} ) assert_equal "22", plus_one_to_string_pipe.(1) end def test_multiple_arguments addition_double_pipe = pipe( lambda {|x, y| x + y}, lambda {|x| x.to_s * 2} ) assert_equal "33", addition_double_pipe.(1,2) end end
Ninth Kata: Print Diamond
The next Kata is described here as:
Given a letter print a diamond starting with ‘A’ with the supplied letter at the widest point.
For example: print-diamond ‘E’ printsA B B C C D D E E D D C C B B AFor example, print-diamond ‘C’ prints
A B B C C B B A
Once more it is hard not to write the whole algorithm at once instead of going in small steps, test by test. Let’s see how it goes!