Letzte Woche: Functions Pipeline
Zur Kata-Beschreibung
Runterscrollen zur aktuellen Kata-Beschreibung
Meine erste Implementierung in PHP war eine Pipe
Klasse mit __invoke
Methode:
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]; } }
Ich habe es möglich gemacht, die Klasse ohne Callables zu instantiieren. In dem Fall war das Verhalten nicht spezifiziert, ich habe mich entschieden die Pipe das erste Argument unverändert zurückgeben zu lassen.
Ich habe getestet dass es mit allen Arten von callables funktioniert:
- Funktionsnamen:
'strtolower'
- “Invokable” Klasse, mittels Mock:
$callable = $this->getMockBuilder(\stdClass::class) ->setMethods(['__invoke']) ->getMock();
- Objektmethode, mit ähnlichem Mock:
[$object, 'method']
. Im Nachhinein wäre das auch ein guter Fall für anonyme Klassen gewesen.
Beim nächsten Mal habe ich es stark vereinfacht: Erstens indem ich mindestens ein callable verlangte und das erste vom Rest separiert habe, was die Funktion (keine Klasse diesmal) viel übersichtlicher machte.
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) ); }; }
Aber auch die Tests waren einfacher. Mit dem callable
Type Hint kann ich annehmen, dass alle Typen von Callables auf die selbe Weise funktionieren, so dass ich mich in den Tests auf einfache Core-Funktionen beschränkte. Schlussfolgerung: Versuche, auch die Tests so einfach wie möglich zu halten!
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')); } }
Wenn ich die alternative Version compose()
implementiert habe, die von rechts nach links funktioniert, waren die Tests ähnlich, aber bei der Implementierung wurde es am Ende immer Rekursion, was für diesen Fall eine viel passenderer Lösung erschien:
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)); }; }
Zum Schluss habe ich mich an der pipe Funktion auch noch einmal in Ruby versucht. Gelernt habe ich dass Blocks (wie in map{|x| x + 1}
) keine Funktionen sind und nicht als Variable/Parameter herumgereicht werden können. Ruby hat stattdessen Procs und Lambdas, aber das ist immer noch unterschiedlich zu echten higher order functions. Aus Testing-Sicht gab es aber nichts neues:
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
Neunte Kata: Print Diamond
Die nächste Kata ist hier wie folgt beschrieben:
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
Wieder einmal ist es schwer, nicht den ganzen Algorithmus auf einmal zu schreiben, anstatt in kleinen Schritten zu arbeiten, Test für Test. Mal sehen wie es läuft!