@burli: Das mit dem Tupel klingt auch aus funktionaler Sicht unlogisch. Hier ist mal ein kleines Beispiel in Rust, das deinen Code nachbaut, aber gleichzeitig das Tupel (das in Rust so gar nicht möglich wäre, weil das zweite Element den Typen wechselt) vermeidet:
Code: Alles auswählen
#[derive(Debug, thiserror::Error)]
enum Error {
#[error("open")]
Open,
#[error("read")]
Read,
#[error("write")]
Write,
#[error("close")]
Close,
}
struct Serial;
impl Serial {
fn open() -> Result<Self, Error> {
Ok(Self)
}
fn read(&mut self) -> Result<Vec<u8>, Error> {
Ok(vec![1, 2, 3, 4])
}
fn write(&mut self, data: &[u8]) -> Result<(), Error> {
Ok(())
}
fn close(&mut self) -> Result<(), Error> {
Ok(())
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = Serial::open()
.and_then(|mut serial| serial.write(b"++addr\n")
.and_then(|()| serial.read()
.and_then(|data| serial.close()
.and_then(|()| Ok(data))
)
)
);
println!("{result:?}");
// and using language support in the form of `?`
let mut serial = Serial::open()?;
serial.write(b"++addr\n")?;
let data = serial.read()?;
serial.close()?;
println!("{data:?}");
Ok(())
}
(
playground)
Man muss einfach die `bind`-Operationen (die bei PyMonad `then` und in Rust `and_then` heißen) verschachteln, genauso wie in Haskell die `do`-Notation zu verschachtelten `bind`s desugart wird. Das sollte so auch in deinem Code funktionieren.
An dem Beispiel sieht man auch sehr schön, wie sehr es hilft, eine Sprache zu benutzen, die diese Art der Fehlerbehandlung explizit unterstützt: Verglichen mit dem Exception-Beispiel (wo der Python-Code viel lesbarer war) ist jetzt der Code mit monadischer Fehlerbehandlung und Fragezeichenoperator in Rust kürzer und lesbarer als der äquivalente Python-Code. In Haskell wäre das ähnlich:
Code: Alles auswählen
do
serial <- Serial.open
Serial.write serial "++addr\n"
data <- Serial.read serial
Serial.close serial
pure data
Deswegen meine ich, dass man besser
mit der Sprache programmiert als
gegen sie.
Man könnte einige Fehler statisch verhindern, indem man das Typestate-Pattern benutzt. Z. B. indem man eine `close`-Funktion schreibt, die eine `Serial`-Instanz konsumiert und dafür eine `ClosedSerial`-Instanz zurückgibt, die dann keine `read`- und `write`-Methoden mehr hat. Und dann könnte man `open` z. B. so schreiben, dass es nicht ein `Either[Error, Serial]` zurückgibt, sondern eine Funktion mit
der Signatur `Callable[[Serial], tuple[ClosedSerial, T]]` entgegennimmt. Dadurch hat man dann im Typsystem sichergestellt, dass man `close` aufrufen
muss, weil das die einzige Möglichkeit ist, eine `ClosedSerial`-Instanz zu erhalten.
Mit diesem Trick hat man sich dann effektiv ein Feature nachgebaut, das Sprachen mit einem linearen Typsystem fest eingebaut haben: Dass man Werte genau einmal benutzen muss, und der Typechecker wirft einen Fehler, wenn man das vergisst. Auch hier ist es wieder von Vorteil, eine Sprache zu benutzen, die das unterstützt: In Python kann eine Funktion nicht sagen „ich konsumiere mein Argument, das darf danach nicht mehr benutzt werden“. In Rust geht das, da hat man also eine zusätzliche Hilfe vom Typechecker, um Fehler zu vermeiden, die Python (auch mit mypy) nicht bieten kann. Allerdings ist das Typsystem von Rust nicht linear, sondern affin. Das heißt, dass man Werte auch ignorieren darf (man muss sie also höchstens einmal benutzen), also braucht auch Rust in solchen Fällen den Workaround mit dem `Callable` als Parameter.
Der Code hat übrigens noch ein weiteres riesiges Problem: Wenn ein Fehler auftritt, wird `close` nicht aufgerufen. Das solltest du auch noch beheben.
@__deets__: `Box` ist in `alloc`, kann also in der Theorie auch ohne `std` benutzt werden. Das Problem wird eher sein, dass `resume_unwind` und `catch_unwind` in `std` sind. „Exceptions“ scheinen in `no_std`-Umgebungen also eher nicht vorgesehen zu sein.
Wenn man sich die Implementierung von z. B. `catch_unwind` anguckt, dann ist das ein dünner Wrapper über `std::panicking::r#try`, das wiederum ein Wrapper über `std::intrinsics::r#try` ist. Dieses Intrinsic gibt es auch in `core`, also sollte man sich da in nightly vielleicht was zusammenbasteln können?
Die
hier gezeigten `no_std`-Panic-Handler scheinen alle mit `loop {}` zu enden, also nicht zu unwinden, sondern das Programm anzuhalten (und optional die Panic-Message zu loggen).
Ich hab aber auch keine Erfahrung mit Bare-Metal-Rust. Wie wird das denn in C++ gehandhabt? Da sind Exceptions (soweit ich weiß, und zumindest unter Linux) ja quasi gleich implementiert, inklusive Allocation und Type Erasure. Kann man da in Freestanding-Umgebungen Exceptions benutzen? Was passiert, wenn eine Exception aus `main` heraus unwindet?