Programmieren in Rust

Funktionale Programmierung

Inhaltsverzeichnis

  1. Funktionenzeiger
  2. Closures
  3. Opake Typen
  4. Funktionen höherer Ordnung
  5. Monaden
  6. Currying
  7. Literatur

Funktionenzeiger

Der klassische Kontrollfluss in der prozeduralen Programmierung verläuft so: Ein Unterprogramm ruft ggf. ein weiteres Unterprogramm auf, und dieses dann ggf. noch ein weiteres Unterprogramm usw. Der Kontrollfluss besteht aus Verzweigungen, Schleifen und Unterprogrammen.

Den Unterprogrammen kommt hierbei eine besondere Bedeutung zu. Diese modularisieren das Programm, indem sie wiederkehrende Funktionalität zusammenfassen, die internen Details verbergen und über ihre Signatur eine Schnittstelle zum Rest des Programms herstellen.

Nun kann man sich eine Situation überlegen, in der es eine Fallunterscheidung zwischen sehr vielen Unterprogrammen gibt. Da dies noch handhabbar erscheint, will ich diese Überlegung sogleich verschärfen. Was tut man denn, wenn das gewählte Unterprogramm nicht im Vorhinein bekannt ist?

Da ich mathematisch-technisch belastet bin, fallen mir dazu keine besseren Beispiele als numerische Verfahren ein. Betrachten wir das Newton-Verfahren zur Ermittlung von Nullstellen einer Funktion. Man kann dieses Verfahren nun für eine bestimmte reelle Funktion programmieren. Der Algorithmus setzt aber eigentlich keine spezielle Funktion voraus. Für eine allgemeine Formulierung muss die Funktion ein Argument des Verfahrens sein. Ermöglicht wird dies durch das Speichern einer Referenz auf die Funktion in einer Variable, man spricht auch von einem Funktionenzeiger.

Das Newton-Verfahren benötigt die erste Ableitung der Funktion. Diese lässt sich ebenfalls mit einem numerischen Verfahren approximieren. Als Beispiel sei die Funktion f(x) = x2 − 2 gewählt. Die positive Nullstelle ist die Wurzel von zwei.

fn diff(f: fn(f64)->f64, x: f64) -> f64 {
    let h = 0.001;
    (f(x+h) - f(x-h))/(2.0*h)
}

fn newton(f: fn(f64)->f64, x0: f64) -> f64 {
    let mut x = x0;
    for _ in 0..20 {
        x = x - f(x)/diff(f, x);
    }
    x
}

fn main() {
    fn f(x: f64) -> f64 {x*x - 2.0}
    println!("{}", newton(f, 1.0));
}

Der wesentliche Vorteil herbei ist, dass wir numerische Verfahren nun in allgemeiner Form in Bibliotheken aufbewahren können. Das geht nur, wenn das Verfahren nicht an eine feste Funktion gekoppelt ist.

Closures

Aufhebung einer Beschränkung

Funktionenzeiger sind einer Beschränkung unterworfen, die ihre Brauchbarkeit stark reduziert. Zentral für die funktionale Programmierung ist, dass sich Funktionen aus Daten erzeugen lassen. Dies wird erreicht durch Bindung der Werte von Variablen aus der Umgebung der Funktion, man spricht von einem Closure. Die Umgebung wird auch Erstellungskontext genannt.

Angenommen wir wollten mit dem Newton-Verfahren nun die Quadratwurzel-Funktion sqrt implementieren. Die Funktion ergäbe sich offenbar so:

fn sqrt(a: f64) -> f64 {
    fn f(x: f64) -> f64 {x*x - a}
    newton(f, 1.0)
}

Diese Funktion würgt der Compiler jedoch ab, denn die innere Funktion f vermag es nicht, den Wert der Variablen a zu speichern. Diese Variable entstammt ja aus der Umgebung der inneren Funktion.

Allerdings gestattet leichte Modifikation des Programms die explizite Benutzung von Closures und Closurezeigern. In abstrakteren Programmiersprachen sind Funktionen und Closures vereinheitlicht. In Rust ist das etwas komplizierter, weil Closures zur effizienten Programmierung mit den Eigenschaften des Typsystems verwoben sind.

Die verallgemeinerten Schnittstellen bezüglich Closurezeigern gestatten sowohl Funktionen als auch Closures als Argumente. Zwischen Funktionen und Closures besteht einen kleiner Unterschied in der Syntax. Anstelle von

fn f(x: f64) -> f64 {x*x - 2.0}

schreibt man:

let f = |x: f64| -> f64 {x*x - 2.0};

oder mittels Typinferenz kurz:

let f = |x| x*x - 2.0;

Das gewünschte Programm:

fn diff(f: &dyn Fn(f64)->f64, x: f64) -> f64 {
    let h = 0.001;
    (f(x+h) - f(x-h))/(2.0*h)
}

fn newton(f: &dyn Fn(f64)->f64, x0: f64) -> f64 {
    let mut x = x0;
    for _ in 0..20 {
        x = x - f(x)/diff(f, x);
    }
    x
}

fn sqrt(a: f64) -> f64 {
    let f = |x| x*x - a;
    newton(&f, 1.0)
}

fn main() {
    println!("{}", sqrt(2.0));
}

Closures als Objekte

Nun stellen sich Fragen. Was hat es mit der Typsignatur &dyn Fn(X)->Y auf sich? Das ist doch eine Referenz auf ein Trait-Objekt, oder nicht? Muss man sich ohne Closures geschlagen geben?

Man muss sich nicht geschlagen geben, ein Verzicht auf Closures ist möglich, denn diese lassen sich als Objekte mit einer einzigen Methode modellieren. Auf diese Art sind Closures hinter den Kulissen auch in Rust umgesetzt.

Nämlich kann man den Funktionenzeiger und die Variable a zu einem Objekt zusammenfassen. Dem Funktionenzeiger gibt man nun zusätzlich ein Argument zum Zugriff auf das eigene Objekt und damit a. Das ist nichts anderes als das self-Argument, die Funktion wird damit zu einer Methode. Schließlich ist der letzte wesentliche Schritt noch die Abstraktion des Aufrufs – dieser sei call genannt – über einen Trait Fun<X,Y>, die Funktionalität ist damit generisch. Ein expliziter Funktionenzeiger wird dabei nicht mehr benötigt, da dieser in Form des Aufrufs call schon über die Dispatch-Tabelle des Trait-Objekts erzeugt wird.

Die Formulierung ist nun lediglich ein klein wenig umständlicher:

trait Fun<X, Y> {
    fn call(&self, x: X) -> Y;
}

fn diff(f: &dyn Fun<f64, f64>, x: f64) -> f64 {
    let h = 0.001;
    (f.call(x+h) - f.call(x-h))/(2.0*h)
}

fn newton(f: &dyn Fun<f64, f64>, x0: f64) -> f64 {
    let mut x = x0;
    for _ in 0..20 {
        x = x - f.call(x)/diff(f, x);
    }
    x
}

fn sqrt(a: f64) -> f64 {
    struct F {a: f64}
    impl Fun<f64,f64> for F {
        fn call(&self, x: f64) -> f64 {x*x - self.a}
    }
    newton(&F {a}, 1.0)
}

fn main() {
    println!("{}", sqrt(2.0));
}

Warum es ein Trait-Objekt sein muss, wird nun klar: Jedes Closure ergibt ein Objekt von einem jeweils neuen Datentyp, da nicht im Vorhinein bekannt ist, welche und wie viele Werte der Umgebung gebunden werden. Daher muss man von allen diesen Datentypen über eine gemeinsame Schnittstelle abstrahieren. Die Schnittstelle ist hierbei der Funktionsaufruf.

Opake Typen

Die Komposition (Verkettung) zweier reeller Funktionen gf an der Stelle x, das ist

compose(g, f, x) := g(f(x)),

ist implementierbar als:

fn compose<G, F>(g: G, f: F, x: f64) -> f64
where G: Fn(f64) -> f64, F: Fn(f64) -> f64
{
    g(f(x))
}

Die Funktion compose muss parametrisch polymorph über alle Typen GF sein, weil der konkrete Typ wie gesagt bei jedem Closure unterschiedlich ist.

Nun wollen wir compose abändern zu

compose(g, f) := |x| g(f(x)).

Die Frage stellt sich, wie das zu typisieren ist, da nun der Rückgabewert ein Closure darstellt. Man könnte denken, der Rückgabetyp sei ebenfalls einfach durch eine Typvariable zu ersetzen. Denken wir genauer darüber nach, stellen wir jedoch fest, dass dies nicht richtig sein kann. Parametrische Polymorphie bedeutet, dass die Typvariablen durch die konkreten Typen ersetzt werden dürfen, die bei der Applikation der Funktion vorgefunden werden. Zum Rückgabewert entsteht jedoch bei der Applikation ein neuer Typ. Über diesen Typ wissen wir lediglich, dass dieser den Trait Fn(f64)->f64 besitzt. Was wir hier vorliegen haben, ist ein opaker Typ, und der erfüllte Trait ist dessen Schnittstelle zur Außenwelt.

Das Typsystem von Rust unterstützt solche opaken Typen. Der opake Typ mit Trait A als Schnittstelle wird impl A geschrieben – man kann das lesen als »opaker Typ, der A implementiert«. Die gewünschte Funktion wird damit formulierbar:

fn compose<G, F>(g: G, f: F) -> impl Fn(f64) -> f64
where G: Fn(f64) -> f64, F: Fn(f64) -> f64
{
    move |x| g(f(x))
}

Man kann auch die Typen der Argumente als solche opaken Typen schreiben, was zunächst etwas verwirrend sein dürfte:

fn compose(g: impl Fn(f64) - >f64, f: impl Fn(f64) - >f64)
-> impl Fn(f64) -> f64
{
    move |x| g(f(x))
}

Was hat es damit auf sich? Obwohl die beiden Typsignaturen nicht identisch sind, verhalten sie sich äquivalent. Die Idee dabei ist, dass die parametrische Polymorphie aus Sicht außerhalb der Funktion einer opaken Typisierung aus Sicht innerhalb der Funktion entspricht. Was wissen wir denn innerhalb der Funktion über die Typen von gf? Wir wissen nichts über sie, außer dass sie die Schnittstelle Fn(f64)->f64 zu erfüllen haben.

Für die allgemeine Signatur der Komposition, wie sie in der Mathematik definiert ist, ersetzen wir ferner die Typen zu Argumenten und Rückgabewerten von gf durch Typvariablen und gelangen schließlich zu:

fn compose<X, Y, Z>(g: impl Fn(Y) -> Z, f: impl Fn(X) -> Y)
-> impl Fn(X) -> Z
{
    move |x| g(f(x))
}

Wie gesagt ist parametrische Polymorphie nicht identisch mit opak typisierten Argumenten. So darf man bei

fn print<T: std::fmt::Display>(x: T) {
    println!("{}", x);
}

den Typparameter explizit belegen, also bspw. print::<i32> schreiben. Bei

fn print(x: impl std::fmt::Display) {
    println!("{}", x);
}

ist das allerdings nicht erlaubt. Mit diesen Wissen sieht man, dass die vollständig explizite Angabe der Typparameter bei compose nur in beschränkter Weise sinnfällig ist, da Closure-Typen per se nicht benennbar sind.

Funktionen höherer Ordnung

Map

Die Funktion map wendet eine Funktion auf jedes Element eines Arrays an.

fn map<X, Y>(a: &[X], f: &dyn Fn(&X) -> Y) -> Vec<Y> {
    let mut acc = Vec::with_capacity(a.len());
    for x in a {
        acc.push(f(x));
    }
    acc
}

fn main() {
    let a = vec![1, 2, 3 ,4];
    let b = map(&a, &|x| x.to_string());
    println!("{:?}", b);
}

Alternativ ist hier auch die Signatur

f: impl Fn(&X)->Y

erlaubt, bzw. äquivalent:

fn map<X, Y, F>(a: &[X], f: F) -> Vec<Y>
where F: Fn(&X) -> Y

Dies kann zu besser optimiertem Code führen, da die Monomorphisierung mehr Inlining ermöglicht.

Weitere Abstraktion erlaubt die Anwendung der Funktion auf beliebige iterierbare Objekte:

fn map<X, Y, A, F>(a: A, f: F) -> Vec<Y>
where A: IntoIterator<Item = X>, F: Fn(&X) -> Y
{
    let it = a.into_iter();
    let mut acc = Vec::with_capacity(it.size_hint().0);
    for x in it {
        acc.push(f(&x));
    }
    acc
}

fn main() {
    let b = map(1..=4, |x| x.to_string());
    println!("{:?}", b);
}

Die Funktion map der Standardbibliothek ist nochmals vielseitiger. Diese arbeitet auf ausgeklügelte Weise mit Iteratoren und abstrahiert so von den iterierbaren Datenstrukturen. Man benutzt sie so:

fn main() {
    let a = [1, 2, 3, 4];
    let b: Vec<_> = a.iter().map(|x| x.to_string()).collect();
    assert_eq!(b, ["1", "2", "3", "4"]);
}

Führt man map eine fehlbare Operation zu, ist das Ergebnis wie erwartet ein Array von Resultaten. Bspw. macht das Programm

fn main() {
    let a = ["1", "2", "3", "4"];
    let b: Vec<_> = a.iter().map(|x| x.parse::<i32>()).collect();
    println!("{:?}", b);
}

die Ausgabe:

[Ok(1), Ok(2), Ok(3), Ok(4)]

Man kann collect aber auch zu verstehen geben, dass beim ersten auftretenden Fehler abgebrochen werden soll. So liefert

fn main() {
    let a = ["1", "2", "3", "4"];
    let b: Result<Vec<_>, _> = a.iter()
        .map(|x| x.parse::<i32>()).collect();
    println!("{:?}", b);
}

die Ausgabe:

Ok([1, 2, 3, 4])

So wie parse ist collect eine generische Funktion, dessen Typparameter durch Typinferenz bestimmt werden kann, wofür der Kontext allerdings genügend Information über die Typstruktur enthalten muss. Die Notation

let b: Typ = it.collect();

ist demnach mit

let b = it.collect::<Typ>();

gleichbedeutend.

Filter

Die Funktion filter trennt von einem Array alle Elemente ab, welche ein mitgegebenes Prädikat erfüllen. Dem Prädikat widersprechende Elemente werden verworfen.

Das folgende Programm zeigt die Implementierung speziell für Vec. Die Funktion greift aus dem Array a Elemente ab, welche das Prädikat p erfüllen.

fn filter<T>(a: Vec<T>, p: impl Fn(&T) -> bool) -> Vec<T> {
    let mut acc = Vec::new();
    for x in a {
        if p(&x) {acc.push(x);}
    }
    acc
}

fn divisors(n: u32) -> Vec<u32> {
    let a: Vec<u32> = (1..=n).collect();
    filter(a, |&k| n%k == 0)
}

fn main() {
    println!("{:?}", divisors(12));
}

Zur Anwendung kam filter in diesem Programm zur Berechnung der Teilerliste einer Zahl. Die Erzeugung eines Arrays als Zwischenwert war dabei jedoch unnötig. Ersetzung von Vec gegen eine Typvariable I vom Trait IntoIterator verallgemeinert filter dergestalt dass man die Erzeugung des Zwischenwertes entfallen lassen kann.

fn filter<T, I, P>(a: I, p: P) -> Vec<T>
where I: IntoIterator<Item = T>, P: Fn(&T) -> bool
{
    let mut acc = Vec::new();
    for x in a {
        if p(&x) {acc.push(x);}
    }
    acc
}

fn divisors(n: u32) -> Vec<u32> {
    filter(1..=n, |&k| n%k == 0)
}

Die Funktion filter der Standardbibliothek ist nochmals vielseitiger. So wie map arbeitet diese auf ausgeklügelte Weise mit Iteratoren und abstrahiert so von den iterierbaren Datenstrukturen. Man benutzt sie wie folgt zur Berechnung der Teilerliste:

fn divisors(n: u32) -> Vec<u32> {
    (1..=n).filter(|&k| n%k == 0).collect()
}

Fold

Wer das erste Mal die Definition der Funktion fold einschließlich ihrer Typsignatur zu Gesicht bekommt, dem mag diese Funktion ein wenig verworren erscheinen. Das hat wohl damit zu tun, dass im Gegensatz zu konkreten Algorithmen aufgrund der Abstraktion nicht unmittelbar klar wird, was diese Funktion denn bewerkstelligen soll.

Daher möchte ich fold ausgehend von konkreten Funktionen verdeutlichen. Betrachten wir die Algorithmen zur Berechnung der Summe und des Produktes der Elemente eines Arrays.

fn sum(a: &[f64]) -> f64 {
    let mut acc = 0.0;
    for &x in a {acc = acc + x;}
    acc
}

fn prod(a: &[f64]) -> f64 {
    let mut acc = 1.0;
    for &x in a {acc = acc*x;}
    acc
}

Beide Funktionen sind irgendwie von der gleichen Gestalt. Betrachtet man das genauer, kommt man zur folgenden Präzisierung: Eigentlich unterscheidet sich lediglich der Anfangswert und die Operation. Eine Abstraktion entsteht demnach, wenn diese als Argumente an den Algorithmus übergeben werden. Wir gelangen zu:

fn fold(a: &[f64], init: f64, op: impl Fn(f64, f64) -> f64) -> f64 {
    let mut acc = init;
    for &x in a {acc = op(acc, x);}
    acc
}

fn sum(a: &[f64]) -> f64 {
    fold(a, 0.0, |acc, x| acc + x)
}

fn prod(a: &[f64]) -> f64 {
    fold(a, 1.0, |acc, x| acc*x)
}

Ist das Prinzip klar geworden, möchte man fold im Weiteren über den Element-Typ parametrisieren. Eine nochmals weitergehende Verallgemeinerung entsteht dadurch, dass man für linkes und rechtes Argument der Operation einen unterschiedlichen Typ zulässt, womit fold einen zweiten Typparameter erhält.

Außerdem verschiebt man die Dereferenzierung der Laufvariable x am besten in die Operation op, sodass der Element-Typ nicht geklont werden braucht.

fn fold<X, Y>(a: &[X], init: Y, op: impl Fn(Y, &X) -> Y) -> Y {
    let mut acc = init;
    for x in a {acc = op(acc, x);}
    acc
}

fn sum(a: &[f64]) -> f64 {
    fold(a, 0.0, |acc, &x| acc + x)
}

Auch fold ist ohne besondere Schwierigkeiten über beliebige iterierbare Typen parametrisierbar. Wir gelangen schließlich zu:

fn fold<X, Y, I, F>(a: I, init: Y, f: F) -> Y
where I: IntoIterator<Item = X>, F: Fn(Y, X) -> Y
{
    let mut acc = init;
    for x in a {acc = f(acc, x);}
    acc
}

Die Standardbibliothek implementiert fold nicht für IntoIterator, sondern für Iterator. Das heißt, falls nicht bereits ein Iterator vorliegt, muss man vorher erst einen bilden. Bei sum geht das mit iter:

fn sum(a: &[f64]) -> f64 {
    a.iter().fold(0.0, |acc, &x| acc + x)
}

Monaden

Die arithmetischen Operationen der gewöhnlichen primitiven Datentypen sind eigentlich nicht fehlerfrei zu verwenden, dergestalt dass es während der Laufzeit aufgrund von Überlauf und Division durch null zum Programmabbruch kommen kann. Mathematisch gesehen spricht man von einer partiellen Funktion. Damit ist gemeint, dass die Operation bzw. Funktion für bestimmte Werte des Definitionsbereichs nicht definiert ist. Wir nehmen jetzt einfach mal den Standpunkt ein, uns genüge das nicht und wir wollten, dass arithmetische Berechnungen immer terminieren.

Eine naheliegende Überlegung ist die Einschränkung des Definitionsbereichs, die allerdings zu kurz gedacht ist. Nämlich haben die arithmetischen Operationen ja die Signaturen M → M und M×M → M. Aufgrund der Übereinstimmung von Definitionsbereich und Zielmenge ist die Einschränkung widersprüchlich.

Stattdessen definieren wir einen neuen Typ Ru32, ein Resultat-Typ zu u32, der zusätzlich den Fehlerwert undefiniert enthält, im Folgenden intern kodiert als Ru32(None). Mit diesem Winkelzug erreicht man nämlich wieder totale Funktionen, d. h. solche die für alle Werte des Definitionsbereichs definiert sind und daher Funktionen im mathematischen Sinn darstellen.

mod ru32 {
    #[derive(Debug, Clone, Copy)]
    pub struct Ru32(Option<u32>);
    
    impl Ru32 {
        pub fn new(x: u32) -> Self {Self(Some(x))}
    }
    impl std::ops::Add<Self> for Ru32 {
        type Output = Self;
        fn add(self, y: Self) -> Self {
            match self.0 {
                Some(x) => match y.0 {
                    Some(y) => Self(x.checked_add(y)),
                    None => Self(None)
                },
                None => Self(None)
            }
        }
    }
}

use ru32::Ru32;

fn main() {
    let x = Ru32::new(1);
    println!("{:?}", x + x);
}

Auffällig sind die umständlichen Fallunterscheidungen die für jede arithmetische Operation zu implementieren sind. Nun kommt man aber auch zu der Einsicht, dass es sich bei diesen Fallunterscheidungen immer um das gleiche Muster handelt, was die Vermutung nahe legt, das Muster lässt sich aus der Implementation herausfaktorisieren.

Im Zusammenhang damit steht die Eigenschaft von Ru32, ein monadischer Typ zu sein. Damit ist gemeint, dass sich für Ru32 mehr oder weniger praktisch die Funktionen unit und bind so formulieren lassen, dass sie die Axiome für Monaden erfüllen. Zum Verständnis dieser Funktionen betrachten wir die folgende Umformulierung, wobei zunächst nur bind für uns wesentlich ist.

mod ru32 {
    trait Monad<T> where Self: Sized {
        fn unit(x: T) -> Self;
        fn bind(self, f: impl Fn(T)->Self) -> Self;
    }

    #[derive(Debug, Clone, Copy)]
    pub struct Ru32 (Option<u32>);
    
    impl Ru32 {
        pub fn new(x: u32) -> Self {Self(Some(x))}
    }
    impl Monad<u32> for Ru32 {
        fn unit(x: u32) -> Self {Self::new(x)}
        fn bind(self, f: impl Fn(u32)->Self) -> Self {
            match self.0 {
                Some(x) => f(x), None => Self(None)
            }
        }
    }
    
    impl std::ops::Add<Self> for Ru32 {
        type Output = Self;
        fn add(self, y: Self) -> Self {
            self.bind(|x| y.bind(|y| Self(x.checked_add(y))))
        }
    }
}

Bevor es mit Monaden weitergeht, ein kurzer Zusatz. Das ursprüngliche Ziel ist schließlich mit einer weiteren Hilfsfunktion bind_binary erreichbar:

fn bind_binary(x: Ru32, y: Ru32,
    f: impl Fn(u32,u32)->Option<u32>
) -> Ru32 {
    x.bind(|x| y.bind(|y| Ru32(f(x,y))))
}

impl std::ops::Add<Self> for Ru32 {
    type Output = Self;
    fn add(self, y: Self) -> Self {
        bind_binary(self, y, u32::checked_add)
    }
}

Currying

Currying ist die Umformung einer mehrstelligen Funktion in eine äquivalente Funktion höherer Ordnung, bei der die Argumente nacheinander einstellig aufgerufen werden. Betrachten wir bspw. die Funktion

f: Z×Z×ZZ, (x, y, z) ↦ x + y + z,

die in der Form f(xyz) aufgerufen wird. Currying führt zu der Funktion

f: ZZZZ, xyzx + y + z,

die in der Form f(x)(y)(z) aufgerufen wird.

Weil Rust Closures bietet, erlaubt es auch Currying. Allerdings ergibt sich dabei eine kleinere Komplikation, die damit zusammenhängt, dass impl Trait nicht an jeder Stelle verwendbar ist. Betrachten wir die Entsprechung von f in Rust.

fn f(x: i32, y: i32, z: i32) -> i32 {
    x + y + z
}

Currying des ersten Arguments stellt kein Problem dar:

fn f(x: i32) -> impl Fn(i32, i32) -> i32 {
    move |y, z| x + y + z
}

Vollständiges Currying wäre nun:

fn f(x: i32) -> impl Fn(i32) -> impl Fn(i32) -> i32 {
    move |y| move |z| x + y + z
}

Leider ist das nicht erlaubt. Es würde sich durch Boxen umgehen lassen:

fn f(x: i32) -> Box<dyn Fn(i32) -> Box<dyn Fn(i32) -> i32>> {
    Box::new(move |y| Box::new(move |z| x + y + z))
}

bzw.

fn f(x: i32) -> impl Fn(i32) -> Box<dyn Fn(i32) -> i32> {
    move |y| Box::new(move |z| x + y + z)
}

Allerdings führt das nun zu unnötigen Speicherallokationen. Ein gängiges Mittel zur Lösung dieser Art von Problemen ist die Einführung einer Helfer-Struktur.

struct FunFromf {x: i32, y: i32}

impl FunFromf {
    fn call(self, z: i32) -> i32 {self.x + self.y + z}
}

fn f(x: i32) -> impl Fn(i32) -> FunFromf {
    move |y| FunFromf {x, y}
}

Oder alternativ:

struct FunFromf {x: i32}

impl FunFromf {
    fn call(self, y: i32) -> impl Fn(i32)->i32 {
        move |z| self.x + y + z
    }
}

fn f(x: i32) -> FunFromf {
    FunFromf {x}
}

Literatur

  1. Rusty Yato: »Closures: Magic Functions«.