Funktionale Programmierung ist unbeliebt, weil sie komisch ist

Ursprünglicher Autor: Malte Skarupke
  • Übersetzung
Ich kenne Leute, die zutiefst verblüfft sind, dass funktionale Programmierung nicht sehr beliebt ist. Zum Beispiel lese ich gerade ein Buch mit dem Titel Out of the Tar Pit, in dem die Autoren nach Argumenten für die funktionale Programmierung sagen:

Trotzdem bleibt die Tatsache bestehen, dass diese Argumente nicht ausreichten, um die funktionale Programmierung zu verbreiten. Daher müssen wir folgern, dass der Hauptnachteil der funktionalen Programmierung die Kehrseite ihres Hauptvorteils ist, nämlich dass Probleme auftreten, wenn (wie so oft) das entworfene System eine Art Zustand beibehalten muss.

Und ich denke, dass der Grund für die mangelnde Popularität viel einfacher ist: Das Programmieren in einem funktionalen Stil geschieht oft „von hinten nach vorne“ und sieht eher aus, als wenn man einem Computer eine Aufgabe erklärt. Wenn ich in einer funktionalen Sprache schreibe, weiß ich oft, was ich sagen möchte, aber am Ende löse ich Rätsel, um es mit der Sprache auszudrücken. Kurz gesagt, funktionale Programmierung ist einfach zu unnatürlich.

Versuchen wir, einen Kuchen zu backen, um die funktionale Programmierung weiter zu diskutieren. Nehmen Sie das Rezept von hier . So etwas werden wir einen Imperativkuchen backen:
  1. Backofen auf 175 ° C vorheizen Öl und Mehl auf ein Backblech streuen. Mehl, Backpulver und Salz in einer kleinen Schüssel vermengen.
  2. In einer großen Schüssel die Butter, den Kristallzucker und den braunen Zucker schlagen, bis die Masse hell und luftig ist. Schlage die Eier nacheinander. Fügen Sie die Bananen hinzu und reiben Sie bis glatt. Alternativ zu der resultierenden cremigen Masse die Basis für den Teig aus Punkt 1 und Kefir hinzufügen. Gehackte Walnüsse hinzufügen. Den Teig in die vorbereitete Pfanne geben.
  3. Im vorgeheizten Backofen 30 Minuten backen. Nehmen Sie die Pfanne aus dem Ofen und legen Sie sie auf ein Handtuch, um den Kuchen abzukühlen.

Ich habe mir mehrere Freiheiten bei der Nummerierung erlaubt (offensichtlich besteht jeder Schritt aus mehreren Schritten), aber lassen Sie uns besser sehen, wie wir einen funktionalen Kuchen backen:
  1. Pie ist ein heißer Kuchen, der auf einem Handtuch abgekühlt ist. Ein heißer Kuchen ist ein zubereiteter Kuchen, der 30 Minuten lang in einem vorgeheizten Ofen gebacken wird.
  2. Ein vorgeheizter Ofen ist ein auf 175 ° C vorgeheizter Ofen.
  3. Eine zubereitete Torte ist ein Teig, der auf einem vorbereiteten Backblech ausgebreitet ist. Dabei handelt es sich um eine cremige Masse, zu der zerkleinerte Walnüsse hinzugefügt werden. Wo die cremige Masse aus Butter, Kristallzucker und braunem Zucker besteht, in einer großen Schüssel leicht und luftig ...

Ah, zum Teufel damit - ich kann es nicht beenden! ( Beachten Sie in der Tat, wenn Sie der Logik folgen, sollten sogar die angegebenen Punkte noch komplizierter sein). Ich weiß nicht, wie ich diese Schritte auf einen funktionalen Stil übertragen kann, ohne den veränderlichen Zustand zu verwenden. Entweder geht die Abfolge der Schritte verloren oder Sie müssen "Bananen hinzufügen" schreiben, aber dann ändert sich der aktuelle Status. Vielleicht wird jemand in den Kommentaren fertig? Ich möchte mir Versionen mit Monaden und ohne Monaden ansehen.
In den Kommentaren zum Originalartikel werden mehrere Optionen vorgeschlagen
Es gab keine Monaden, aber mit und ohne Pipe Forward Operator.

Ohne Pipe Forward Operator:
cake = cooled(removed_from_oven(added_to(30min, poured(greased(floured(pan)), stirred(chopped(walnuts),
alternating_mixed(buttermilk, whisked(flour, baking soda, salt), 
mixed(bananas, beat_mixed(eggs, creamed_until(fluffy, butter, white sugar, brown sugar)))), 
preheated(175C, oven))))))

Pipe Forward Operator verwenden:
cake = bake(cake_mixture, 30min, prepare(pan, (grease, flour)), preheated(175C, oven))
where cake_mixture =
creamed :until_fluffy ‘butter’ ‘white’ ‘sugar’ ‘brown sugar’
|> beat_mixed_with ‘eggs’
|> mixed_with ‘bananas’
|> mixed_with :alternating ‘buttermilk’ ‘dry_goods’
|> mixed_with chopped ‘walnuts’
where dry_goods = whisked ‘flour’ ‘baking soda’ ‘salt’


Imperative Sprachen haben immer noch den großen Vorteil, dass sie einen impliziten Zustand haben. Sowohl Menschen als auch Maschinen arbeiten sehr gut mit einem impliziten zeitgebundenen Zustand. Wenn Sie das Kuchenrezept lesen, wissen Sie, dass nach Abschluss der ersten Anweisung der Ofen vorgeheizt, die Pfanne eingefettet und der Boden für den Teig geknetet wird. Dies muss nicht explizit beschrieben werden. Wir haben Anweisungen und wir wissen, dass der endgültige Zustand durch Befolgen dieser Anweisungen erreicht wird. Ein zwingendes Rezept wird niemanden verwirren. Und wenn ich das Funktionsrezept beenden könnte und es meiner Mutter zeigen würde, wäre sie wahrscheinlich sehr verwirrt von ihm. (Naja, zumindest die Version ohne Monaden. Vielleicht wäre die Version mit Monaden nicht so verwirrend.)

Ich schreibe diesen Beitrag, weil ich kürzlich auf ein ähnliches Problem gestoßen bin. Es ist einfach so passiert, dass C ++ - Templates eine funktionale Sprache sind. Und als die C ++ - Entwickler dies verstanden, begannen sie, anstatt das Problem zu lösen, Vorlagen in jeder Hinsicht in einem funktionalen Stil zu schätzen, was es manchmal sehr mühsam macht, regulären Code durch Vorlagen umzuschreiben. Zum Beispiel ist hier, was ich kürzlich für einen Parser geschrieben habe. (Ich weiß, dass es dumm ist, einen eigenen Parser zu schreiben, aber alte Tools wie Yacc und Bison sind schlecht. Als ich Boost Spirit ausprobierte, hatte ich Probleme, deren Lösung zu lange dauerte, und ich entschied mich schließlich, meinen eigenen Parser zu schreiben. )
ParseResult VParser::parse_impl(ParseState state)
{
    ParseResult a = a_parser.parse(state);
    if (ParseSuccess * success = a.get_success())
        return ParseSuccess{{std::move(success->value)}, success->new_state};
    ParseResult b = b_parser.parse(state);
    if (ParseSuccess * success = b.get_success())
        return ParseSuccess{{std::move(success->value)}, success->new_state};
    ParseResult c = c_parser.parse(state);
    if (ParseSuccess * success = c.get_success())
        return ParseSuccess{{std::move(success->value)}, success->new_state};
    ParseResult d = d_parser.parse(state);
    if (ParseSuccess * success = d.get_success())
        return ParseSuccess{{std::move(success->value)}, success->new_state};
    return select_parse_error(*a.get_error(), *b.get_error(), *c.get_error(), *d.get_error());
}

Diese Funktion analysiert den Eingabeparameter in einen Variantentyp vom Typ V und versucht, den Eingabeparameter als Typ A, B, C oder D zu analysieren.
Im realen Code sind die Namen dieser Typen besser, aber für uns nicht wichtig. In diesem Beispiel gibt es eine offensichtliche Verdoppelung: Wir führen genau dasselbe Codefragment viermal mit vier verschiedenen Parsern aus. C ++ unterstützt im Allgemeinen keine Monaden, aber Sie können dieses Codefragment wiederverwendbar machen, indem Sie eine Schleife schreiben, die alle vier Parser in der folgenden Reihenfolge sortiert:
template
ParseResult parse_variant(ParseState state, Parser &... parsers)
{
    boost::optional error;
    template
    for (Parser & parser : parsers)
    {
        ParseResult result = parser.parse(state);
        if (ParseSuccess * success = result.get_success())
            return ParseSuccess{{std::move(success->value)}, success->new_state};
        else
            error = select_parse_error(error, *result.get_error());
    }
    return *error;
}
ParseResult VParser::parse_impl(ParseState state)
{
    return parse_variant(state, a_parser, b_parser, c_parser, d_parser);
}

Dieser Code ist ein wenig suboptimal, da Sie die richtige Fehlermeldung auswählen müssen, aber im Allgemeinen ist dies eine eher triviale Transformation des ursprünglichen Beispiels. Abgesehen davon, dass Sie in C ++ nicht so schreiben können. Sobald Vorlagen ins Spiel kommen, müssen Sie funktionaler denken. Hier ist meine Option:
template
ParseResult parse_variant(ParseState state, Parser & first_parser)
{
    ParseResult result = first_parser.parse(state);
    if (ParseSuccess * success = result.get_success())
        return ParseSuccess{{std::move(success->value)}, success->new_state};
    else
        return *result.get_error();
}
template
ParseResult parse_variant(ParseState state, Parser & first_parser, 
    Parser &... more_parsers)
{
    ParseResult result = first_parser.parse(state);
    if (ParseSuccess * success = result.get_success())
        return ParseSuccess{{std::move(success->value)}, success->new_state};
    else
    {
        ParseResult more_result = parse_variant(state, more_parsers...);
        if (ParseSuccess * more_success = more_result.get_success())
            return std::move(*more_success);
        else
            return select_parse_error(*result.get_error(), *more_result.get_error());
    }
}
ParseResult VParser::parse_impl(ParseState state)
{
    return parse_variant(state, a_parser, b_parser, c_parser, d_parser);
}

Und um ehrlich zu sein, ich bin sehr zufrieden mit dieser Option. Natürlich ist dies schwieriger zu lesen, da die Iteration jetzt in der Rekursion verborgen ist. Wenn Sie jedoch erst meinen Code gesehen haben, bevor ich diese Lösung gefunden habe ... Ich hatte eine Struktur mit dem Feld std :: tuple...>. Wenn Sie jemals mit einem Tupel variabler Länge aus der Standardbibliothek gearbeitet haben (d. H. Std :: tuple mit variabler Größe), sollten Sie wissen, dass dies allein jeden Code in einen Rebus verwandelt.

So oder so lautet meine Botschaft: Ich hatte einen einfachen Imperativcode, der dasselbe mehrmals tat. Um es über Vorlagen neu zu schreiben, können Sie es nicht einfach nehmen und ein sich wiederholendes Fragment in eine Schleife einwickeln. Stattdessen müssen Sie die Logik des Programms vollständig ändern. Und dafür musst du zu viele Rätsel lösen. Außerdem habe ich sie nicht einmal beim ersten Mal gelöst. Beim ersten Versuch entschied ich mich für etwas zu Kompliziertes und ließ eine zwingende Option. Und erst nachdem ich einige Tage später auf das Problem zurückgekommen war, kam ich oben auf eine einfachere Lösung. Das Umschreiben von Code durch Vorlagen sollte nicht so schwierig sein. Wenn das Hauptproblem darin besteht, nicht zu verstehen, was das Programm tun soll, sondern zu verstehen, wie man es dazu bringt, es zu tun.

Und ich habe zu oft das Gefühl in funktionalen Sprachen. Ich weiß, dass C ++ - Vorlagen eine schlechte funktionale Sprache sind, aber selbst in guten funktionalen Sprachen verbringe ich zu viel Zeit damit, herauszufinden, wie man Dinge in der Sprache ausdrückt, anstatt darüber nachzudenken, was ich ausdrücken möchte.

Habe ich bei all dem die Meinung, dass funktionale Sprachen schlecht sind? Gar nicht. Die Vorteile von funktionalen Sprachen liegen auf der Hand. Jeder sollte mindestens eine funktionale Sprache lernen und versuchen, das erworbene Wissen in anderen Sprachen anzuwenden. Aber wenn funktionale Sprachen populär werden wollen, sollten sie weniger mit dem Lösen von Rätseln verbunden sein.

Jetzt auch beliebt: