TDD Kata 09 – Print Diamond

Dies ist mein wöchentlicher Kata Post. Lies den ersten um zu erfahren, worum es hier geht.

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’ prints

    A    
   B B   
  C   C
 D     D
E       E
 D     D
  C   C
   B B
    A

For 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!