https://d226lax1qjow5r.cloudfront.net/blog/blogposts/the-functions-that-arent-pure/functional-programing_1200x600.png

Die Funktionen, die nicht rein sind

Zuletzt aktualisiert am June 18, 2021

Lesedauer: 3 Minuten

Täglich werden mehr Entwickler auf das Paradigma der funktionalen Programmierung aufmerksam. Dieses Paradigma verspricht fehlerfreien, effizienten Code, da reine Funktionen leichter zu testen und zu parallelisieren sind.

In der Praxis sind vollwertige funktionale Applications immer noch eine abstrakte Sache. Bestimmte Concepts aus dem funktionalen Programmierparadigma werden jedoch immer häufiger auf nicht-funktionale Sprachen angewendet. Außerdem lassen sich mit diesem "teilfunktionalen" Ansatz viele gängige "Probleme" besser lösen.

Heute wollen wir uns eines dieser funktionalen Konzepte näher ansehen - reine/unreine Funktionen.

HINWEIS: Wir werden Kotlin-basierten Pseudocode verwenden, um unsere Funktionen zu definieren, und die Beispiele so einfach wie möglich halten, damit sie leichter zu verstehen sind.

Reine Funktionen

Die reine Funktion ist eine Funktion, die keine Nebeneffekte hat - mit anderen Worten, diese Funktion sollte keine anderen Werte als die als Parameter übergebenen Werte abrufen und verändern. Auf diese Weise wird jeder Funktionsaufruf mit denselben Argumenten immer dieselbe Ausgabe (Rückgabewert) ergeben. Beginnen wir damit, zunächst eine reine Funktion zu definieren:

fun max(a: Int, b: Int) {
if (a > b) 
    return a
else
    return b	
}

Die Funktion max Funktion benötigt zwei Argumente, zwei Zahlen, und gibt die größte von ihnen zurück. Beachten Sie, dass diese Funktion nicht auf Werte außerhalb des Funktionsumfangs zugreift oder diese verändert, sie ist also eine reine Funktion. Wir können sicher sein, dass der Aufruf dieser Funktion mit den Argumenten 2 und 7 IMMER zurückgeben wird 7.

Um dieses Konzept besser zu verstehen, sollten wir uns die andere Seite der Medaille genauer ansehen - die verschiedenen Möglichkeiten, die Funktionsreinheit zu brechen.

Unreine Funktionen

Eine unsaubere Funktion ist eine Funktion, die Nebeneffekte hat - sie verändert Werte oder greift auf Werte außerhalb des Funktionsumfangs zu (außerhalb des Funktionskörpers).

Ziemlich offensichtliche Nebenwirkungen

Das einfachste Beispiel ist eine Funktion, die eine externe Eigenschaft modifiziert, um einen Zustand zu speichern.

val loalScore = 0

val getScore(score: Int): Int {
    loalScore = score
    return loalScore
}

In diesem Fall macht sich die Verunreinigung nicht stark bemerkbar, da nachfolgende Methodenaufrufe denselben Wert zurückgeben:

getScore(12) // returns 12
getScore(6) // returns 6
getScore(3) // returns 3

Diese Funktion ändert einen externen Wert, gibt aber aufgrund der Wertzuweisung bei jedem Aufruf denselben Wert zurück. Dies ist jedoch nicht immer der Fall. Betrachten wir eine andere unsaubere Funktion:

val addScore = 0

val addScore(score: Int): Int {
    loalScore + score
    return loalScore
}

Diese Funktion ist "stärker verunreinigt", da sie einen externen Wert ändert und bei jedem Aufruf ein anderes Ergebnis zurückgibt - der Zustand wird in der Variablen außerhalb des Funktionsbereichs gespeichert:

addScore(12) // returns 12
addScore(6) // returns 18
addScore(3) // returns 21

Der Nachteil der Beibehaltung des Zustands ist, dass das Testen manchmal komplizierter ist; in vielen Applications lässt sich dies jedoch nicht vermeiden.

Die folgende Funktion greift auf einen Wert außerhalb des Funktionsbereichs zu und wird dadurch unsauber:

fun getString(length: Int): String {
    return Random().nextString(length)
}

Diesmal wird bei jedem Funktionsaufruf mit demselben Argument ein anderer Wert zurückgegeben:

getString(2) // returns "ab"
getString(2) // returns "hh"
getString(2) // returns "zk"

Während die zuvor genannten Nebenwirkungen ziemlich leicht zu erkennen sind, können Nebenwirkungen oft subtiler sein:

Nicht so offensichtliche Nebenwirkungen

Ein interessantes Szenario mit Nebeneffekten ist eine Änderung des als Funktionsargument übergebenen Objekts:

fun increaseHeight(person: Person) {
    person.height++
}

Ein mehrfacher Aufruf dieser Funktion mit der gleichen Person Instanz führt zu unterschiedlichen Ergebnissen, da der Wert außerhalb der Funktion (gespeichert in der Instanz Person) geändert wird.

Die von einer Funktion ausgelöste Ausnahme ist ein hervorragendes Beispiel für die schwieriger zu erkennenden Nebeneffekte:

fun addDistance (a:Int, b:Int): Int {
    if(a < 0) {
        throw IllegalAccessException("a must be >= 0")
    }
     
    return a + b
}

Eine weitere interessante Möglichkeit, Seiteneffekte zu erzeugen, ist der einfache Aufruf einer anderen Funktion mit Seiteneffekt:

fun firstFunction() {
    addDistance(-5, 7)
}

fun addDistance (a:Int, b:Int): Int {
    if(a < 0) {
        throw IllegalAccessException("a must be >= 0")
    }
     
    return a + b
}

Ein weiterer, nicht ganz so offensichtlicher Nebeneffekt ist die Protokollierung. Werfen wir einen Blick auf dieses reale Beispiel aus unserem Base Video Chat Beispielanwendung:

private PublisherKit.PublisherListener publisherListener = new PublisherKit.PublisherListener() {
    @Override
    public void onStreamCreated(PublisherKit publisherKit, Stream stream) {
        Log.d(TAG, "onStreamCreated: Publisher Stream Created. Own stream " + stream.getStreamId());
    }

    @Override
    public void onStreamDestroyed(PublisherKit publisherKit, Stream stream) {
        Log.d(TAG, "onStreamDestroyed: Publisher Stream Destroyed. Own stream " + stream.getStreamId());
    }

    @Override
    public void onError(PublisherKit publisherKit, OpentokError opentokError) {
        Log.d(TAG, "PublisherKit onError: " + opentokError.getMessage());
    }
};

Im obigen Code wirkt sich die Protokollierung als Nebeneffekt nicht auf die Anwendungslogik aus, sondern hilft uns zu verstehen, was in der Anwendung vor sich geht. Später ist es für den Entwickler ein Leichtes, die von Callbacks zurückgegebenen Daten zu verwenden und weitere Seiteneffekte einzuführen.

Bestimmen Sie, ob die Funktion rein ist.

Es gibt zwei Anzeichen dafür, dass eine Funktion unrein ist - sie nimmt keine Argumente entgegen und gibt keinen Wert zurück. Schauen wir uns den ersten Fall an:

list.getItem(): String

Im obigen Beispiel nimmt die Funktion keine Parameter entgegen, sondern gibt den Wert zurück. Dies bedeutet, dass der Wert höchstwahrscheinlich aus dem Klassenzustand abgerufen wird. Betrachten wir nun, was passiert, wenn eine Funktion keinen Wert zurückgibt:

list.setItem("item")

Anhand des Funktionsnamens können wir erkennen, dass der Parameter höchstwahrscheinlich zur Änderung des Klassenstatus verwendet wird.

Und schließlich können wir eine Combo haben, bei der es kein Argument gibt und kein Wert zurückgegeben wird:

list.sort()

Dies sind nur Anhaltspunkte. Das ist nicht immer der Fall, aber diese Hinweise sind oft gute Indikatoren für Reinheit.

Zusammenfassung

Im Paradigma der funktionalen Programmierung sind im Idealfall alle Funktionen rein.

In vielen realen Anwendungen sind die Dinge jedoch nicht ganz so binär. Manchmal lassen sich unreine Funktionen nicht vermeiden, insbesondere wenn eine Anwendung externe Ressourcen wie Persistenz, Benutzereingaben oder Netzwerkdatenzugriff benötigt. Wenn dies der Fall ist, wird die Reinheit der Funktion und der gesamten Anwendung beeinträchtigt, was nicht schlecht ist.

Normalerweise gibt es in einer einzigen Anwendung eine Mischung aus reinen und unreinen Funktionen. Es ist eine gute Praxis, sich der Reinheit/Unreinheit bewusst zu sein, da dies das Testen von Anwendungen erleichtert und uns hilft, Fehler zu vermeiden.

Teilen Sie:

https://a.storyblok.com/f/270183/384x384/8ae5af43bb/igor-wojda.png
Igor WojdaVonage Ehemalige