Haskell bei antei

25.09.2020 - raichoo

Haskell gehört zu den ausgereiftesten funktionalen Programmiersprachen und mit der steigenden Popularität von FP Konzepten hat sie in den letzen Jahrzehnten zunehmend auch in der Industrie an Relevanz gewonnen. Oft als akademische Einstiegssprache für Algorithmen und Datenstrukturen abgetan, erfreut sich Haskell allerdings großer Beliebtheit z.B. im Finanzsektor, bei großen sozialen Netzwerken oder innovativen Automobilherstellern. Vor allem da, wo es um robuste, langlebige und effiziente Software geht, setzen Unternehmen auf Haskell. In diesem Artikel werden wir uns mit ein paar Techniken befassen die aufzeigen, welche Vorteile das Arbeiten mit dieser Programmiersprache bietet.

Warum gerade Haskell?

Bei der Wahl einer Programmiersprache sind viele Faktoren zu beachten. Eine der ersten Fragen, die wir uns stellen sollten ist, ob wir gezwungen sind das Rad neu zu erfinden. Die beste Programmiersprache hilft uns nicht, wenn grundlegende Bibliotheken nicht zur Verfügung stehen und bei jedem Projekt vorab von Grund auf neu entwickelt werden müssen. Glücklicherweise verfügt Haskell über ein umfangreiches Ökosystem mit tausenden von Bibliotheken, wie wir es von einer Allzweckprogrammiersprache erwarten. Dazu gehören zum Beispiel:

Dies bildet erst einmal die Grundvoraussetzung. Viele Sprachen liefern uns all diese Bausteine ebenfalls, warum sollte also Haskell besonders interessant für uns sein?

Das ausschlaggebende Kriterium für uns war hierbei das Vertrauen, das wir in den von uns geschriebenen Code haben können. Haskell verfügt über eines der am weitesten entwickelten Typsysteme, die in der Industrie ihre Verwendung finden und auch der Fokus auf mathematische Funktionen bietet viele Vorteile.

Programmieren mit mathematischen Funktionen

Der Begriff der “Funktionalen Programmierung” ist in den letzten Jahren deutlich populärer geworden und hat den Sprung aus dem akademischen Umfeld in die Industrie vollzogen. Was allerdings genau damit gemeint ist, wird leider seltener spezifiziert. Wir werden den Begriff der FP hier mit dem programmieren mit “mathematischen Funktionen” (oft auch “reine” oder “pure Funktionen” genannt) gleichsetzen. Verglichen mit dem Funktionsbegriff der uns oft in der Programmierung begegnet, handelt es sich bei mathematischen Funktionen um Funktionen ohne Seiteneffekte. Insbesondere bedeutet das, dass eine Funktion für die gleiche Eingabe immer die gleiche Ausgabe erzeugt und auch den Zustand der Applikation nicht unbemerkt verändert (in der Literatur wird hier auch teilweise von “referenzieller Transparenz” gesprochen, der Einfachheit halber werden wir uns aber hier nur auf Seiteneffekte konzentrieren). Daraus folgt, dass eine Funktion auch nicht in der Lage ist Effekte zu verursachen, die sich später auf das Ergebnis anderer Funktionen auszuwirken.

Um genauer zu verstehen wie Seiteneffekte zu Problemen führen können, werfen wir einen kleinen Blick auf ein stark vereinfachtes Code Beispiel in der Sprache JavaScript. Das es sich hier um JavaScript handelt ist nicht weiter ausschlaggebend, ähnliche Effekte lassen sich auch mit den meisten anderen Sprachen auf ähnliche Weise beobachten.

var glob = 1;

function f(x) {
  return glob + x;
}

function g() {
  glob++;
}

console.log(f(1)); // Ausgabe: 2.
g();
console.log(f(1)); // Ausgabe: 3.

Wir sehen, dass die Aufrufe der Funktionen f und g jeweils Seiteneffekte aufweisen. f liest die globale Variable glob aus und g inkrementiert den Wert von glob bei jedem Aufruf. Aufgrund eines vorangegangenen Aufrufs von g liefert f trotz der gleichen Eingabe beim zweiten Aufruf ein anderes Ergebnis. In realer Software sind diese Seiteneffekte oft sehr viel umfangreicher und auch subtiler, da es einer Funktion nicht anzusehen ist, ob sie Seiteneffekte verursacht oder nicht. Dies ist vor allem in Zeiten von Multithreading eine nicht zu unterschätzende Fehlerquelle, da die Reihenfolge in der Seiteneffekte geschehen können nicht unbedingt vorhersehbar ist.

Das Aufrufen einer reinen Funktion ist auch in einem Multithreading Kontext unbedenklich, da hier nur Berechnungen stattfinden können die den weiteren Verlauf lediglich durch ihr Ergebnis, welches durch die Eingabe eindeutig ist, beeinflussen können.

In Haskell müssen Seiteneffekte durch die Typsignatur einer Funktion “angekündigt” werden und können dadurch nicht unbemerkt mit reinen Funktionen vermischt werden, was eine Vielzahl von Fehlerquellen von vornherein ausschliesst. Hierzu wird der Typkonstruktor IO verwendet.

import Data.IORef

main :: IO ()
main = do
  -- Erstellt eine geteiltle veränderbare Referenz `IORef`.
  glob <- newIORef 1

  -- Greift auf `glob` zu und inkrementiert dessen Wert. Diese Funktion darf
  -- Seiteneffekte verursachen da sie im `IO` "Kontext" ausgeführt wird.
  let g :: IO ()
      g = modifyIORef glob (+1)

  -- Diese Funktion darf keine Seiteneffekte nutzen da sie nicht im `IO`
  -- "Kontext" ausgeführt wird.
  let f :: Int -> Int
      f x = readIORef glob + x

  print (f 1)
  g
  print (f 1)

Dieser Code wird vom ghc (Haskell Compiler) abgewiesen, da wir versuchen Seiteneffekte in der reinen Funktion f zu nutzen. Das Auslesen auf eine geteilte, veränderbare Referenz via readIORef wird als solch ein Effekt interpretiert. Wir werden mit folgender Fehlermeldung konfrontiert, welche uns darauf hinweist, dass wir hier genau gegen diese Vorgabe verstoßen.

Main.hs:16:13: error:
    • Couldn't match expected type ‘Int’ with actual type ‘IO a0’
    • In the expression: readIORef glob + x
      In an equation for ‘f’: f x = readIORef glob + x
      In the expression:
        do glob <- newIORef 1
           let g :: IO ()
               g = modifyIORef glob (+ 1)
           let f :: Int -> Int
               f x = readIORef glob + x
           print (f 1)
           ....
    • Relevant bindings include glob :: IORef a0 (bound at Test.hs:6:3)
   |
16 |       f x = readIORef glob + x
   |             ^^^^^^^^^^^^^^^^^^

Der große Vorteil hier ist, dass Entwickler*innen nicht zufällig auf Seiteneffekte stoßen können und somit auch nicht durch unerwartetes Verhalten durch selbige überrascht werden. Der Compiler unterstützt uns mit Fehlermeldungen um Code, der gegen die Konventionen, die wir in Typsignaturen kommunizieren verstößt, abzulehnen. Das Typsystem in Haskell ist mächtig genug um viele Invarianten, die in anderen Sprachen schwer oder unmöglich abzubilden sind, darzustellen und uns bei einer Verletzung dieser darüber zu informieren. So können wir zum Beispiel Seiteneffekte während Transaktionen verhindern, sollten diese unerwartet zurück gerollt werden müssen.

main :: IO ()
main =
  atomically $ do -- Starten einer Transaktion.
    -- Führe nicht weiter spezifizierte Operationen in unserer Transaktion aus.
    doOperation

    -- Raketen lassen sich einmal angefeuert nicht wieder zurückrufen.
    -- Der Compiler weißt diesen Code ab.
    fireTheMissles

    retry -- Rollt Transaktion zurück und startet erneut.

Die Striktheit des Haskell Typsystems mag wie ein Hindernis erscheinen, aber gerade diese macht es so hilfreich. Code der gegen vereinbarte Konventionen verstößt, sollte so früh wie möglich im Entwicklungsprozess abgewiesen werden. Dies gibt uns zusätzliche Sicherheit, dass die Software sich nicht unerwartet verhält, sowie Unterstützung wenn Code erweitert und angepasst werden muss. Tatsächlich gibt es in der Haskell Community ein Sprichwort das besagt “Code der kompiliert, ist meistens richtig”. Wie bei vielen Sprichwörtern sollte man auch hier nicht allzu blauäugig vorgehen und wir werden in einem späteren Abschnitt über Lauftzeittests mit QuickCheck reden. Dennoch stimmt es, dass das Typsystem viele gängige Fehler, deren Vermeidung und Erkennung im Regelfall auf die Entwicker*innen ausgelagert wird, schon im Vorfeld vermeidet und uns als wertvolles Werkzeug zur Qualitätssicherung zur Seite steht.

NULL - Eine Designentscheidung und ihre Folgen

Sogenannte NULL-Referenzen sind in den meisten Industriesprachen Gang und Gäbe, genauso wie die mit ihnen verbundenen Programmierfehler. Den meisten Entwickler*innen ist die Abkürzung NPE (NULL Pointer Exception) ein schmerzlich bekannter Begriff. Sir Tony Hoare, der maßgeblich für die Einführung und Popularisierung dieses Konzepts bekannt ist, hat folgendes darüber zu sagen:

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

Dennoch haben viele sich damit arrangiert, dass NULL in den meisten Sprachen alternativlos ist wenn es darum geht, fehlende Werte zu kommunizieren. Haskell jedoch verfügt gar nicht erst über NULL sondern bedient sich einer bestechend simplen und eleganten Lösung für dieses Problem, dem Datentyp Maybe.

-- Generischer Datentyp `Maybe` mit 2 Konstruktoren
data Maybe a
  = Nothing -- Kein Wert vom generischen Typ `a` vorhanden.
  | Just a -- Enthält einen Wert vom generischen Typ `a`.

Dies ist ausreichend, um einen neuen Datentypen Maybe in Haskell zu beschreiben (Maybe ist Teil der Haskell Standardbibliothek und muss nicht durch uns neu definiert werden). Insbesondere beinhaltet diese Definition alle Möglichkeiten einen Wert vom Typen Maybe a zu konstruieren, nämlich über die Konstruktoren Just, welcher das Vorhandenseins eines Wertes von einem generischen Typ a signalisiert, oder Nothing für das Nichtvorhandensein. Es gibt keine andere Möglichkeit einen Wert vom Typen Maybe a zu konstruieren.

Um nun einen Wert aus diesem Datentypen zu extrahieren bedient sich Haskell eines Konzeptes namens Pattern Matching.

Nehmen wir an wir haben eine Funktion process die einen Wert vom Typen Maybe Int als Eingabe erhält und mit dem eventuell vorhandenen Inhalt einen Seiteneffekt verursachen soll.

-- Funktion `proccess` nimmt einen Wert vom Typ `Mabye Int` entgegen und
-- verursacht dabei Seiteneffekte (siehe `IO`).
process :: Maybe Int -> IO ()
-- Bei der Eingabe handelt es sich um ein via `Just` konstruiertes
-- `Maybe Int`. Wir extrahieren `x` via Pattern Matching.
process (Just x) = print x
-- `Maybe Int` wurde via `Nothing` konstruiert.
process Nothing = putStrLn "Leider ist kein Wert vorhanden"

Unsere Funktion behandelt beide Arten, wie ein Wert vom Typen Maybe Int generiert werden kann. Unter anderem überprüft ghc auch während des kompilierens, ob wir alle Fälle behandeln. Dies unterscheidet sich von Überprüfungen auf NULL, welche nicht Pflicht sind. Sollten wir beispielsweise den Nothing Fall ignorieren, weißt uns ghc mit einer Warnung auf die fehlende Behandlung hin.

Main.hs:8:1: warning: [-Wincomplete-patterns]
    Pattern match(es) are non-exhaustive
    In an equation for ‘process’: Patterns not matched: Nothing
  |
8 | process (Just x) = print x
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^

Für einen einfachen Datentypen wie Maybe ist es einfach mit bloßem Auge und ohne Hilfe zu erkennen, dass ein Fall fehlt. ghc allerdings überprüft für beliebig komplexe Datentypen, ob wir Fälle nicht behandeln. Dies ist besonders hilfreich, da sich Software immer weiter entwickelt und Datentypen neue Konstruktoren über den Verlauf der Softwareentwicklung hinzubekommen können. Diese Haskell Datentypen unterscheiden sich von herkömmlichen Enumerationen insofern, dass es sich um Algebraische Datentypen handelt welche sich durch eine deutliche höhere Expressivität auszeichnen.

Unbereinigte Nutzereingaben und damit verbundene Sicherheitslücken

Injection Angriffe, ob es sich jetzt um SQLI, XSS oder Shell Injection handelt, sind seit Jahrzehnten ein Problem welches immer wieder in Applikationen auftritt. Obwohl die Ursachen und Auswirkungen dieser Sicherheitslücken längst bekannt sind, gelten sie weiterhin als einer der verbreitetsten Angriffsvektoren. Ein Weg sich diesem Problem anzunehmen, sind zeit- und kostenintensive Penetration Tests. Haskell setzt mit auf sein Typsystem um die Angriffsfläche für solche Lücken drastisch zu reduzieren.

Die Ursache für Injection Angriffe ist in allen Fällen gleich. Dabei ist es egal, um welche konkrete Ausprägung dieses Angriffs es sich handelt. Hierbei nutzt die angreifende Partei eine Schwachstelle, um eigene Eingaben als Code zu interpretieren.

Betrachten wir ein einfaches Beispiel, hier in der Programmiersprache JavaScript. Die Wahl der Sprache ist hier wieder nicht ausschlaggebend, wir könnten genauso Java oder auch jede andere gebräuchliche Sprache verwenden.

function hello(name) {
  return `Hallo <span class="name">${name}</span>`;
}

Code wie dieser ist in Webapplikationen leider nicht unüblich und verhält sich im Normalfall auch unproblematisch, wenn der Name vorher bereinigt wird. Ist dies nicht der Fall, kann hier allerdings eine angreifende Partei einen Wert für name wählen, welcher zum Beispiel bösartigen JavaScript Code einbinden könnte. Dieser wird dann ausgeführt, wenn sich beispielsweise die Administration dieser Webapplikation alle im System registrierten Namen auflisten lässt.

Blaze-HTML eine Haskell Templating Bibliothek unterbindet das unbereinigte Einbinden von Userdaten in das ausgegebene HTML Dokument.

So wird der Programmcode renderHtml "<script>alert('XSS');</scipt>" direkt vom Kompiler mit einem Typfehler abgewiesen. Um ein Element in ein HTML Dokument einzubinden muss dieses vorher mit toHTML bereinigt werden. So wird schon zur Compilezeit unterbunden, dass Userdaten unbereinigt eingebunden werden. Der Compiler unterstützt uns beim Vermeiden einer Sicherheitslücke.

Wie Blaze-HTML mit Nutzereingaben verfährt, läßt sich in ghci ausprobieren. Einen Versuch userInput unbereinigt einzubinden würde der Compiler abweisen.

ghci> let userInput ="<script>alert('XSS');</script>"
ghci> renderHtml (toHtml userInput)
"&lt;script&gt;alert(&#39;XSS&#39;);&lt;/script&gt;"

SQL Injections funktionieren nach den gleichen Prinzip. Wie auch bei XSS gilt es hier Daten in das System einzuschleusen, die dann vom Datenbankserver interpretiert und ausgeführt werden. So verwendet PostgreSQL-Simple einen Query Datentypen um achtlosen Umgang, welcher zu Sicherheitslücken führen kann, zu unterbinden.

Boilerplate vermeiden mit Datatype-Generic Programming

In den letzten Abschnitten haben wir uns voranging mit der Vermeidung von möglichen Programmierfehlern durch den Einsatz des Haskell Typsystems beschäftigt. Das Haskell Typsystem bietet allerdings weitere Vorteile, da es dem Compiler Einsicht in die Struktur von Datentypen gibt. So kann zum Beispiel ghc für uns repetitiven Code, der sonst per Hand geschrieben werden müsste, automatisch durch die Analyse von Datentypen generieren. Dieser Ansatz wird “Datatype-Generic Programming” genannt.

Um eine Idee davon zu bekommen wie das Ganze funktioniert, werfen wir einen Blick auf Aeson, einer Bibliothek zum Arbeiten mit dem weit verbreiteten Datenformat JSON.

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
module JSON where

import Data.Aeson
import GHC.Generics

-- Neuen Datentyp `Person` mit Feldern `firstName`, `lastName` und `age`
-- erstellen.
data Person = Person
  { firstName :: String
  , lastName :: String
  , age :: Int
  -- `deriving` weißt `ghc` an automatisch bestimmten Code zu generieren.
  -- `Generic` wird für `Aeson`'s `FromJSON` und `ToJSON` benötigt.
  } deriving (Generic, ToJSON, FromJSON)

Dieser Code ist ausreichend, um einen Wert vom Typ Person in JSON zu serialisieren beziehungsweise zu deserialisieren. Lediglich durch die Anweisung deriving (Generic, ToJSON, FromJSON) bekommt ghc alle Informationen, um den Code für uns nur Anhand der Struktur von Person zu generieren. Wichtig ist hier die Anmerkung, dass der Compiler selbst gar keine Kenntnis von JSON hat sondern Aeson von ghc generierten Code nutzt, um den von uns angeforderten Code zu generieren.

Wie in vorherigen Beispielen können wir unseren Code einfach im ghci Interpreter ausprobieren.

ghci> let person = Person "Erika" "Mustermann" 42
ghci> encode person
"{\"lastName\":\"Mustermann\",\"age\":42,\"firstName\":\"Erika\"}"

Natürlich sind wir weiterhin in der Lage diesen Code selber zu schreiben, sollte dies eine unserer Anforderungen sein. Durch diesen Ansatz reduzieren wir allerdings große Mengen an Code, den wir sonst händisch zu pflegen hätten. Datentypen können sich im Laufe der Zeit ändern, ohne dass wir Parser oder Formatter von Hand anpassen müssen.

Der Ansatz von “Datatype-Generic Programming” ist in der Haskell Community durchaus verbreitet. So gibt es neben Aeson zum Behandeln von JSON auch postgresql-simple und cassava, um nur ein paar der populärsten Bibliotheken zu nennen. Diese geben uns den gleichen Komfort für die Kommunikation mit der Postgres Datenbank, oder zur Arbeit mit dem CSV Datenformat. Natürlich lassen sich auch komfortabel eigene Bibliotheken implementieren die “Datatype-Generic Programming” nutzen können.

Testfälle automatisch generieren mit QuickCheck

Testen ist ein wichtiger Bestandteil der Softwareentwicklung. Zwar haben wir in den vorhergehenden Artikeln behandlet, wie Haskells Typsystem bestimmte Fehlerklassen schon zur Compilezeit abfängt, wir wollen aber in der Lage sein, das Netz noch enger zu ziehen um weitere mögliche Laufzeitfehler zu erkennen.

In Haskell wurde die Technik des Property-Based Testing entwickelt. Diese Art des Testens erinnert an Fuzzing. Hierbei wird Code mit zufälligen Eingaben gefüttert und überprüft, ob alle angegebenen Invariaten unverletzt bleiben. QuickCheck nutzt dafür die Typsignatur einer Eigenschaft, um zufällig Testfälle zu generieren und damit Randfälle zu entdecken.

Betrachten wir ein Stück offensichtlich fehlerhaften Code. uniq soll Duplikate aus einer Liste filtern (die Reihenfolge der Ergebnisliste ist nicht weiter relevant, ebenso vernachlässigen wir in diesem Beispiel Laufzeitkomplexität).

Wir legen folgende Eigenschaft fest die uniq haben soll:

Jedes Element in unserer Ausgangsliste muss exakt einmal im Ergbnis von uniq vorkommen.

prop_uniq_filters_duplicates :: [Int] -> Bool
prop_uniq_filters_duplicates list = go list
  where
    result = uniq list

    -- Leere Liste erfüllt unsere Bedingung automatisch.
    go [] = True
    -- Jedes Element `x` in `list` MUSS einmal aber nicht öfter im Ergebnis
    -- enthalten sein.
    go (x:rest) =
      length (filter (== x) result) == 1 && go rest

Als offensichtlichen Fehler bauen wir ein dass unsere Implementation von uniq Primzahlen übergeht.

uniq :: [Int] -> [Int]
uniq = go []
  where
    -- Ende der Eingabeliste erreicht
    go acc [] = acc

    -- Erstes Element `x` der Eingabe überprüfen und mit Rest `xs`
    -- weiterverfahren.
    go acc (x:xs)
      | isPrime x = go acc xs -- FEHLER! Primzahlen werden verworfen.
      | x `elem` acc = go acc xs -- Duplikat überspringen.
      | otherwise = go (x:acc) xs -- Element in Ergebnisliste eintragen.

Dies verletzt offensichtlich unsere Annahme. Für den Fall, dass unsere Eingabeliste eine Primzahl aufweist, wird diese nicht mehr im Ergebnis von uniq auffindbar sein. Anhand der Typsignatur von prop_uniq_filters_duplicates erkennt QuickCheck, dass es Listen mit zufälligen Integern generieren muss, um diese Eigenschaft zu testen.

Schauen wir uns an wie QuickCheck diesen Fall behandelt.

ghci> quickCheck prop_uniq_filters_duplicates
*** Failed! Falsified (after 5 tests and 1 shrink):
[2]

Wir finden nach 5 Versuchen ein Minimalbeispiel was unsere gewünschte Eigenschaft verletzt. Eine Liste die nur aus der Primzahl 2 besteht bringt uniq dazu ein falsches Ergbnis zu liefern. Da es sich hier um zufällig generierte Listen handelt, können auch andere Fälle von QuickCheck gefunden werden.

ghci> quickCheck prop_uniq_contains_all
*** Failed! Falsified (after 8 tests and 2 shrinks):
[7]

Als Anmerkung ist hier noch durchaus interessant fetzustellen, dass QuickCheck keinerlei Kenntnis von dem Fakt hat, dass unsere Implementation von uniq fehlerhaft mit Primzahlen verfährt. Dies wird automatisch durch den Testvorgang erkannt.

QuickCheck erzeugt in der Standard Konfiguration bis zu 100 zufällig generierte Fälle und versucht, jedes gefundene Gegenbeispiel zu einem Minimalfall zu reduzieren.

Wenn wir unsere fehlerhafte Bedingung entfernen sehen wir, dass alle 100 Testfälle erfolgreich durchlaufen werden.

uniq :: [Int] -> [Int]
uniq = go []
  where
    go acc [] = acc
    go acc (x:xs)
      | x `elem` acc = go acc xs
      | otherwise = go (x:acc) xs
ghci> quickCheck prop_uniq_filters_duplicates
+++ OK, passed 100 tests.

Durch das automatisierte Testen von Eigenschaften lassen sich schnell und einfach Randfälle finden, die oft lange unbemerkt bleiben. So wurde QuickCheck erfolgreich verwendet um Fehler in TLS Implementationen aufzuspüren.

Fazit

Haskell vermeidet viele Klassen von Fehlerquellen bereits im Design der Sprache. Reine Funktionen ermöglichen es uns, komplexe Programmverläufe einfacher nachzuvollziehen, da Seiteneffekte explizit gekapselt werden. Algebraische Datentypen und Pattern Matching erlauben uns ohne viel Aufwand beschreibende Datentypen zu erstellen und lückenlos zu handeln. “Datatype-Generic Programming” reduziert die Notwendigkeit für fragilen Boilerplate Code und versetzt uns in die Lage, schnell und einfach über Datenformate und/oder Datenbanken Informationen auszutauschen. QuickCheck rundet des Paket mit Property-Based Testing ab.

Wir haben lediglich an der Spitze des Eisbergs gekratzt. Haskell ist eine mächtige moderne Allzweckprogrammiersprache, die es Entwickler*innen erlaubt mit Sicherheit komplexe Probleme zu lösen, da sie tatkräftig von der Sprache unterstützt zu werden. Das senkt aufkommende Wartungskosten und reduziert die Entwicklungszeit. Dies sind einige unserer Gründe, warum wir Haskell für das richtige Werkzeug zum lösen einfacher sowie komplexer Probleme sehen, und weswegen unsere Entwickler es seit vielen Jahren erfolgreich einsetzen.