Seite 1 von 1

Funktionale Programmierung - Beispiel Seiteneffekte

Verfasst: Mo Okt 25, 2021 4:44 pm
von unlimited101
Demnächst zeige ich einigen Personen Vorteile Funktionaler Programmierung. Ich mache dies Anhand der Java 8 Stream API.

Ich wollte zeigen, dass imperative Programmierung Seiteneffekte erzeugt, die man mit funktionaler Programmierung umgehen kann.

Das (fiktive) Szenario ist folgendes: In einer Klinik sind Patientendaten fehlerhaft gespeichert worden. Aus irgendwelchen Gründen ist das Geburtsjahr bei allen Patienten falsch hinterlegt. Das Geburtsjahr ist genau um 1 zu klein.

Ich simuliere das folgendermaßen. Ich habe eine Klasse Patient:

Code: Alles auswählen

public class Patient {
    private String name;
    private int year;
    private boolean corrected = false;

    public Patient(String name, int year) {
        this.name = name;
        this.year = year;
    }

    public Patient(String name, int year, boolean corrected) {
        this.name = name;
        this.year = year;
        this.corrected = corrected;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public boolean isCorrected() {
        return corrected;
    }

    public void setCorrected(boolean corrected) {
        this.corrected = corrected;
    }

}
Und dann ändere ich einmal imperativ das Geburtsjahr:

Code: Alles auswählen

    private void correctPatientsImperative() {
        for (int i = 0; i < App.patients.size(); i++) {
            Patient patient = App.patients.get(i);
            if (!patient.isCorrected()) {
                patient.setYear(patient.getYear() + 1);
                patient.setCorrected(true);
            }
        }
    }
Und einmal funktional:

Code: Alles auswählen

        return List<Patient> correctedPatients = App.patients.stream().filter(patient -> !patient.isCorrected())
                .map(patient -> new Patient(patient.getName(), patient.getYear() + 1, true))
                .collect(Collectors.toList());

Ich arbeite mit einer Liste von Patienten. Da sind etwa 25 Patient-Objekte abgespeichert. Die Idee ist jetzt folgende: Ich erzeuge viele Threads (50 bis 5000) und alle rufen die Korrekturfunktion auf. Bei der imperativen Variante sollten Seiteneffekte auftreten, bei der funktionalen Variante nicht. Ich lasse mir die Liste vorher und nachher ausgeben, und lasse zusätzlich eine "Check"-Funktion drüber laufen, die prüft, ob die korrigierten Geburtsjahre zu den Patienten passen.

Ist diese Idee nachvollziehbar? Kann man das vom Grundansatz her so machen? Oder habe ich irgendwo einen Denkfehler?

Re: Funktionale Programmierung - Beispiel Seiteneffekte

Verfasst: Mo Okt 25, 2021 8:53 pm
von Xin
Hmm... ich würde sagen... wenn ich das richtig verstehe, wirst Du in der funktionalen Geschichte keine Seiteneffekte haben, aber 50 bis 5000 gleiche, aber nicht identische Ergebnisse, kombiniert mit einem unveränderten Stream. Das lässt sich auch imperativ über Const-Correctness genauso machen. Da alles konstant ist, muss Du die Liste mit veränderten Elementen neu anlegen.
Und in der rein imperativen Geschichte mit modifizierbaren Objekten wirst Du halt RacingConditions haben... mangels Const-Correctness. Die fakest Du in der funktionalen Lösung ja auch nur dadurch, dass Du die Original-Objekte nicht modifizierst.

Ich bin nicht sicher, dass das die Vorteile der funktionalen Programmierung wirklich optimal repräsentiert.

Der Vorteil der funktionalen Programmierung ist, dass alle Elemente unverändert bleiben müssen, also immer konstant sind. Daher sind Racing-Conditions ausgeschlossen. Das ist der gewünschte Vorteil. Man kann aber auch imperativ sich entscheiden, Objekte nicht zu verändern und das geht über Const-Correctness - was wiederum Java nicht durchsetzen kann.

Dafür müssen aber neue veränderte Elemente erzeugt werden. Ob das überhaupt parallel geht, hängt erstmal von der Beschaffenheit der Daten ab. Ist ein Objekt vom Vorgänger abhängig, stellt sich beispielsweise die Frage, ob ich ein Kind überhaupt bearbeiten kann, bevor ich nicht das modifizierte Elternteil habe. Schon habe ich ein Problem mit der Machbarkeit von Parallelität. Und an der Stelle muss ich u.U. die gleichen Entscheidungen treffen, die in der modifizierenden Variante die Racing-Conditions rausnehmen würden.

Es reicht nicht, eine Funktion 50 bis 5000 mal aufzurufen, damit sie 50 bis 5000 mal das gleiche macht. Parallelität soll ja etwas beschleunigen. Eine konstruierte Racing-Condition ist kein Nachteil beim verwenden modifizierbarer Objekte. In dem Fall hast Du halt die Locks vergessen, was eher ein Programmierfehler ist.

Der Vorteil der imperativen Programmierung ist, dass man Elemente verändern kann, also nicht neu erzeugen muss. Ob das bei Parallelisierung zu RacingConditions führt, hängt erstmal von der Beschaffenheit der Daten ab.
Denn funktionale Programmierung wird teuer, wenn man die Welt der Primitive verlässt - also Objekte, die grundsätzlich kopiert werden, weil sie so primitiv sind, dass sie gar nicht ohne überkopieren überarbeitet werden können. Bei Datenstrukturen sieht das anders aus, da kann ich Teile überschreiben und lasse andere unberührt. Wenn ich das nicht darf, muss ich die komplette Datenstruktur neu anlegen.

Um das mal zu verdeutlichen: Man nehme eine 24Bit Grafik für einen 4K-Monitor. Also einen Block von 3840*2160*3 Byte, also rund 25MB.
Und jetzt setze ich einen Pixel. Das Ergebnis ist eine neue 25MB große Grafik mit einem veränderten Pixel und 8,3 Millionen Pixel-Kopien. Und jetzt malen wir mal eine Linie aus 10 Punkten, die wird Punkt für Punkt gesetzt und jeder Punkt ist vom Ergebnis der vorherigen Punkt-Operation abhängig, sonst wird keine Linie draus. Es werden also 250'000'000 Bytes kopiert, um davon 30 Byte zu modifizieren.
Wir würden moderne High-End-Rechner brauchen, um Jump'n'Run Spiele in C64-Grafik hinzubekommen. Falls das überhaupt reicht. ;-)

Und an der Stelle fällt die Grundthese: Ob Seiteneffekte ein Nachteil oder ein Vorteil sind, hängt von der Beschaffenheit der Daten ab.
Jeden Scheiß erstmal zu kopieren ist nur aus akademischer Sicht kostenlos. Und das ist der Grund, warum bis heute vorrangig imperativ programmiert wird: Daten sind in der Regel keine Primitive, diese sollen mit minimalem Aufwand modifiziert werden und meistens ist uns vollkommen egal, was mit dem unveränderten Objekt passiert - im Gegenteil, wir müssen uns jetzt auch nicht fragen, wie wir das unveränderte Objekt jetzt loswerden, weil das unveränderte Objekt mit der Veränderung implizit nicht mehr existiert.

Rechner simulieren Mathematik und Logik. Aber es ist nicht die Logik, die wir aus dem Mathematikunterricht kennen. Computer sind Werkzeuge und faken Mathematik. Die Aufgabe der Informatik ist mit unterschiedlichen Ideen dieses Werkzeug so zu bedienen, dass möglichst wenig auffällt, dass das eigentlich alles nur Illusion ist.

Du vergleichst hier modifizierte Objekte gegen nicht modifizierte Objekte (womit Du nicht modifizierbare Objekte fakest). Ob Du das imperativ oder funktional machst... ich sehe da jetzt keinen Unterschied.

Re: Funktionale Programmierung - Beispiel Seiteneffekte

Verfasst: Di Okt 26, 2021 10:33 am
von unlimited101
Danke für die ausführliche Antwort. Das mit mutable und immutable war auch mein Gedanke.

Aber worin würdest du sonst die Vorteile funktionaler Programmierung sehen? Und sind auch Seiteneffekte möglich, wenn immer nur ein einziger Thread die Daten verarbeitet?

Re: Funktionale Programmierung - Beispiel Seiteneffekte

Verfasst: Di Okt 26, 2021 11:33 am
von Xin
Die Antwort wird schwierig, weil funktionale Programmierung in meinen Augen eine miese Definition ist. Im Prinzip ist funktionale Programmierung nichts anderes als imperative Programmierung - man lässt nur einige Möglichkeiten der imperativen Programmierung weg und nutzt sie nicht.

Seiteneffekte sind nur Möglich, wenn Du sie erlaubst - also Daten mutable sind.
Die übliche Programmierung darf Werte verändern, die meisten imperativen Sprachen gehen per Default von veränderlichen Daten aus.

Funktionale Programmierung lehnt sich an die Mathematik an, wo es ja auch keine "Variablen" im Sinne von ändert seinen Wert im Lauf der Zeit, sondern nur Variablen im Sinne von Du darfst sie wählen für jeweilige Betrachtung der Gleichung, um daraus den konstanten Wert andere "Variablen" abzulesen. Eine Gleichung wie x = x+1 ist in der Mathematik einfach nur falsch.
Funktionale Programmierung geht grundsätzlich von unveränderlichen Daten aus.

Das erstmal zum Unterschied. Nun können C und C++ eben auch Const-Correctness. Man kann sich den Vorteil also auch in imperativen Sprachen ranholen.
Nun sind C# und Java gerade modern und die können das nicht... beide Sprachen stehen aber auch nicht unbedingt für die höchste Kunst der Programmierung...
Rust ist eine imperative Programmiersprache und geht per Default von Const aus. Hier hat also ein Umdenken statt gefunden. Meine Sprache ist ebenfalls recht const-affin.

Die Welt liebt momentan Objektorientiertes Programmieren. Ich übrigens auch. Aaaaaber: Ein Objekt kann sich an vielen Stellen verändern. Nebenher ein Bug, den ich gerade beruflich suche... modifizierbare Objekte können ohne gute Kapselung irgendwo von anderen Objekten verändert werden. Das ganze ist also recht nervig zu finden. Das kann man verhindern, aber häufig will man das ja eben nicht.
In der funktionalen Programmierung wird ein Objekt eingerichtet und dann existiert es für immer genau so. Das heißt, wenn das Objekt falsch ist, ist das beim Erzeugen passiert. Eine Funktion ist eine Einheit und erzeugt ein Ergebnis. In einem Objekt ist jede Methode abhängig vom Zustand des Objektes und der kann sich ändern. Es ist also komplexer herauszufinden, wie das Objekt entstanden ist, was dann den Fehler provoziert hat.

Nun ist es so, dass funktionale Programme die gleichen Probleme zu lösen haben. Man findet entsprechend in der funktionalen Programmierung Ansätze sich an die imperative Programmierung anzunähern. Ich habe aber gerade vergessen, wie diese "Technik" hieß.

Imperative Sprachen sind in der Regel auf Datensammlungen optimiert, die sie filtern (aussortieren), transformieren (berechnen) oder reduzieren (aus vielen Daten ein neues Datum machen). Imperative Sprachen können das auch, weil das im Prinzip nur for-Schleifen sind.

Der Vorteil der funktionalen Programmierung ist am Ende, dass man weiß, dass gewisse imperative Konstrukte nicht existieren, dass die Sache zustandslos abläuft (keine globalen Variablen, keine Membervariablen...) was die Optimierung sehr vereinfacht.

Es ist aber nunmal kein komplett anderes Konzept. Funktionale Programmierung ist eine Teilmenge der imperativen Programmierung. Und übliche Konstrukte wie transform, filter, reduce sind hier häufig expressiver beschreibbar als in imperativen Sprachen. Das heißt aber nicht, dass eine Const-Methode in C++ perse schlechter wäre als eine Funktion in einer funktionalen Sprache. Man muss nur mehr tippen.

Deswegen würde ich die Frage umformulieren: Wann ist funktionale Programmierung vorteilhafter?

Am Ende ist nämlich immer die Frage, wie sind die Daten beschaffen. Wenn ich auf Modifizierung von Daten verzichten kann, bin ich automatisch in der funktionalen Programmierung - auch in einer imperativen Sprache. Häufig will ich aber nicht verzichten: Wie Du mit Deinem Patientenbeispiel: Du willst das Alter der Patienten ändern. Du bist aber nicht an den alten Objekten mehr interessiert. Entsprechend ist für Dein Beispiel funktionale Programmierung nicht vorteilhaft, denn am Ende hast Du alte Patientendaten, die Du gar nicht brauchst und neue Patientendaten.
Das Problem ist mit einem variablen Datensatz vielleicht sinnvoller behoben... womit wir dann aber aus der funktionalen Programmierung rausfallen müssen.

Re: Funktionale Programmierung - Beispiel Seiteneffekte

Verfasst: Mi Okt 27, 2021 9:19 am
von unlimited101
OK, dann überlege ich mir ein Beispiel, wo die Vorteile rein funktionaler Programmierung überwiegen. Aber wenn du eine Idee bzw. einen Tipp hast, dann gerne.