mirror of
https://github.com/LukasKalbertodt/programmieren-in-rust.git
synced 2024-11-18 02:48:58 +01:00
Add sheet 4
This commit is contained in:
parent
7cce646eb0
commit
52c017152c
10
aufgaben/sheet4/README.md
Executable file
10
aufgaben/sheet4/README.md
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
Blatt 4
|
||||||
|
=======
|
||||||
|
|
||||||
|
Wie immer soll dieses Blatt in einem eigenen Branch, `sheet4`, bearbeitet
|
||||||
|
werden.
|
||||||
|
|
||||||
|
Eine weitere generelle Aufgabe: Euer erster Commit soll *nur* die Dateien
|
||||||
|
beinhalten, die euch schon (in diesem Repository) gegeben werden. So fällt es
|
||||||
|
den Tutoren und mir deutlich leichter, den vorgegebenen Teil und eure eigenen
|
||||||
|
Änderungen zu unterscheiden!
|
15
aufgaben/sheet4/task1/README.md
Executable file
15
aufgaben/sheet4/task1/README.md
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
Aufgabe 4.1: Programm schöner machen
|
||||||
|
====================================
|
||||||
|
|
||||||
|
In dieser Aufgabe dürft *ihr* mal die Aufgabe der Tutoren übernehmen. Gegeben
|
||||||
|
ist ein Programm, das funktioniert und das tut, was es soll. Es gibt alle
|
||||||
|
Zahlen zwischen 1 und 20, die sowohl eine Primzahl als auch eine ["fröhliche"
|
||||||
|
Zahl](https://en.wikipedia.org/wiki/Happy_number) sind, aus.
|
||||||
|
|
||||||
|
Jedoch ist der Quellcode alles andere als hübsch. Eure Aufgabe ist es, den
|
||||||
|
Quellcode zu verbessern. D.h., ihr sollt die Funktionsweise des Programms gar
|
||||||
|
nicht verändern, sondern nur den Quellcode.
|
||||||
|
|
||||||
|
Versucht, den Code möglichst kurz, gut lesbar und idiomatisch zu gestalten.
|
||||||
|
So viel sei gesagt: Es gibt *sehr viele* Stellen, an denen etwas verbessert
|
||||||
|
werden kann! Versucht möglichst, alle dieser Stellen zu finden.
|
69
aufgaben/sheet4/task1/bad.rs
Executable file
69
aufgaben/sheet4/task1/bad.rs
Executable file
@ -0,0 +1,69 @@
|
|||||||
|
// print if they are both
|
||||||
|
fn main() -> () {
|
||||||
|
for iterationnumber in &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] {
|
||||||
|
let iterationnumber = *iterationnumber;
|
||||||
|
if !happy_prime(iterationnumber) {} else {
|
||||||
|
print!("{}", iterationnumber);
|
||||||
|
println!(" is a happy prime!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// is it botH?
|
||||||
|
fn happy_prime(n: i32) -> bool {
|
||||||
|
match check_if_number_is_happy(n) {
|
||||||
|
false => return false,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if check_if_number_is_prime(n) == true {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is it a happy number? https://en.wikipedia.org/wiki/Happy_number
|
||||||
|
fn check_if_number_is_happy(number: i32) -> bool {
|
||||||
|
let mut number: i32= number;
|
||||||
|
|
||||||
|
while number > 1 {
|
||||||
|
let mut tmp = 0;
|
||||||
|
while number > 0 {
|
||||||
|
tmp = tmp + (number %10) * (number%10);
|
||||||
|
number = number / 10;
|
||||||
|
}
|
||||||
|
number = tmp;
|
||||||
|
|
||||||
|
// We ended up in a cycle -> not happy
|
||||||
|
if (number == 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// is it priem?
|
||||||
|
fn check_if_number_is_prime(n: i32) -> bool {
|
||||||
|
if n == 1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if n == 2 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut teilerGefunden:bool = false;
|
||||||
|
|
||||||
|
let mut teiler:i32= 2;
|
||||||
|
while (teiler < n) {
|
||||||
|
if (n % teiler == 0) {
|
||||||
|
teilerGefunden = true;
|
||||||
|
}
|
||||||
|
teiler = teiler + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !teilerGefunden;
|
||||||
|
}
|
219
aufgaben/sheet4/task2/README.md
Executable file
219
aufgaben/sheet4/task2/README.md
Executable file
@ -0,0 +1,219 @@
|
|||||||
|
Aufgaben 4.2: Taschenrechner
|
||||||
|
============================
|
||||||
|
|
||||||
|
In dieser Aufgabe sollt ihr ein Programm schreiben, welches einfache
|
||||||
|
Rechenaufgaben lösen kann.
|
||||||
|
Zwar kann man sich einen sehr einfachen Rechner schnell manuell
|
||||||
|
zusammenschreiben.
|
||||||
|
Das Programm dieser Aufgabe soll jedoch nicht quick'n'dirty geschrieben werden,
|
||||||
|
sondern später einfach zu erweitern sein.
|
||||||
|
|
||||||
|
Ein Teil des Codes ist bereits in `calculator.rs` vorgegeben.
|
||||||
|
Das gegebene Programm liest in einer Endlosschleife Eingaben vom Nutzer ein,
|
||||||
|
tut damit aber bislang noch nichts, außer sie auszugeben.
|
||||||
|
Das Programm kann man bekanntlich mit Strg+C beenden.
|
||||||
|
|
||||||
|
Das Programm soll später ungefähr folgende Aufgaben lösen können:
|
||||||
|
|
||||||
|
```
|
||||||
|
calc > 3 + 3
|
||||||
|
6
|
||||||
|
calc > 2-3
|
||||||
|
-1
|
||||||
|
calc > 9 + (7 - 2)
|
||||||
|
14
|
||||||
|
calc > 2
|
||||||
|
2
|
||||||
|
calc > (3 - 2) + ((3 - 2) + 9)
|
||||||
|
11
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
a) Tokenization
|
||||||
|
---------------
|
||||||
|
|
||||||
|
Die ersten beiden Schritte fast aller Compiler jeglicher Art sind:
|
||||||
|
*Tokenization* (auch *Lexing*) und *Parsing*.
|
||||||
|
Gerade Compiler haben einen extrem komplizierten und komplexen Job.
|
||||||
|
Um dieser Komplexität Herr zu werden, werden eine Reihe von unterschiedlichen
|
||||||
|
Abstraktionsstufen genutzt.
|
||||||
|
Der Quellcode wird permanent von einer Darstellung in die nächste überführt;
|
||||||
|
Analysen, wie Typechecking ("werden alle Typen korrekt benutzt?"), werden nie
|
||||||
|
direkt auf dem Quellcode-String, sondern auf abstrakteren Darstellungen
|
||||||
|
ausgeführt.
|
||||||
|
|
||||||
|
Glücklicherweise müsst ihr keinen Compiler schreiben, sondern nur einen
|
||||||
|
Taschenrechner.
|
||||||
|
Aber auch dabei müssen Nutzereingaben mit einer bestimmten Syntax analysiert
|
||||||
|
werden.
|
||||||
|
Daher werden wir die Funktionalität des Rechners ebenfalls in drei Schritte
|
||||||
|
unterteilen:
|
||||||
|
|
||||||
|
1. Tokenization/Lexing
|
||||||
|
2. Parsing
|
||||||
|
3. Errechnen des Ergebnisses
|
||||||
|
|
||||||
|
Im ersten Schritt wird der Eingabestring in eine Liste von sog. *Tokens*
|
||||||
|
umgewandelt. Ein Token kann ein Zeichen repräsentieren (wie z.B. `"+"`) oder
|
||||||
|
auch mehrere (wie z.B. `"127"`).
|
||||||
|
Anstatt also eine Liste von Zeichen zu betrachten, betrachtet man eine Liste
|
||||||
|
von atomaren Elementen im Code.
|
||||||
|
Um mal ein Beispiel zu geben:
|
||||||
|
|
||||||
|
```
|
||||||
|
Eingabe: "23 + 7"
|
||||||
|
|
||||||
|
| | |
|
||||||
|
Tokens: [Number(23), Plus, Number(7)]
|
||||||
|
```
|
||||||
|
|
||||||
|
Eure erste Aufgabe ist es, einen Typen `Token `zu definieren, der einen Token
|
||||||
|
darstellt.
|
||||||
|
Der Tokenization-Schritt produziert nämlich dann `Vec<Token>`.
|
||||||
|
Wie oben schon gezeigt, muss der Rechner in der Lage sein, Addition und
|
||||||
|
Subtraktion von beliebigen positiven Ganzzahlen auszuführen (das Ergebnis kann
|
||||||
|
negativ sein, nur die einzelnen Zahlen in der Eingabe nicht).
|
||||||
|
Außerdem müssen Klammerausdrücke behandelt werden.
|
||||||
|
Entwerft entsprechend den `Token` Typ.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Nachdem der Typ definiert ist, müsst ihr jetzt eine Funktion `tokenize()`
|
||||||
|
schreiben.
|
||||||
|
Diese Funktion bekommt einen String und erzeugt daraus eine Liste von Tokens.
|
||||||
|
Natürlich können schon bei der Tokenization Fehler im Input auffallen.
|
||||||
|
Diese Fehler sollten nicht einfach ignoriert werden.
|
||||||
|
|
||||||
|
Die Funktion soll außerdem Whitespaces in der Eingabe komplett ignorieren, dafür
|
||||||
|
also keinen Token erstellen (das würde in späteren Schritten nur ablenken).
|
||||||
|
|
||||||
|
*Wichtig*: Ihr müsst lediglich mit Zahlen umgehen können, die nur
|
||||||
|
*eine* Ziffer besitzen.
|
||||||
|
Zahlen bestehend aus mehreren Ziffern zu lexen, ist -- selbst mit Hilfe von
|
||||||
|
der `parse()` Funktion aus der Standardbibliothek -- nicht ganz trivial.
|
||||||
|
Wer das trotzdem schafft, wird natürlich vom Tutor gelobt ;-)
|
||||||
|
|
||||||
|
|
||||||
|
Schließlich soll die Nutzereingabe an `tokenize()` gegeben werden und das
|
||||||
|
Ergebnis zu Debugzwecken in `main()` ausgegeben werden.
|
||||||
|
Folgende Eingaben können getestet werden:
|
||||||
|
|
||||||
|
- `3`
|
||||||
|
- `3 + 4`
|
||||||
|
- `3 - (7 - 7)`
|
||||||
|
- `3 + ) - (` -> (in diesem Schritt führt das *noch nicht* zum Fehler!)
|
||||||
|
- `peter` -> Hier sollte ein Fehler ausgegeben werden (nicht panic!)
|
||||||
|
|
||||||
|
**Tipps**:
|
||||||
|
|
||||||
|
- Es lohnt sich, die Dokumentation von
|
||||||
|
[`char`](https://doc.rust-lang.org/std/primitive.char.html) durchzulesen
|
||||||
|
- Um Instanzen selbstdefinierter Typen miteinander vergleichen zu können, kann
|
||||||
|
man `#[derive(PartialEq)]` über die Typdefinition schreiben. Dann kann der
|
||||||
|
Typ per `==` und `!=` verglichen werden.
|
||||||
|
|
||||||
|
b) Definition des Parsetrees
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Bisher haben wir aus einem String einen Tokenstream gemacht.
|
||||||
|
Im Parsing-Schritt wird aus dem Tokenstream ein sog. Parsetree erzeugt.
|
||||||
|
Doch bevor wir uns an das Parsing wagen, müssen wir erstmal Typen definieren,
|
||||||
|
um den Parsetree darzustellen.
|
||||||
|
|
||||||
|
Hier ist mal ein Beispiel eines Parsetrees zu der Eingabe `3 + (6 - 1)` zu
|
||||||
|
sehen:
|
||||||
|
|
||||||
|
```
|
||||||
|
+---------+
|
||||||
|
| Summe |
|
||||||
|
+---------+
|
||||||
|
/ \
|
||||||
|
/ \
|
||||||
|
+-----+ +-----------+
|
||||||
|
| 3 | | Differenz |
|
||||||
|
+-----+ +-----------+
|
||||||
|
/ \
|
||||||
|
/ \
|
||||||
|
+-----+ +-----+
|
||||||
|
| 6 | | 1 |
|
||||||
|
+-----+ +-----+
|
||||||
|
```
|
||||||
|
|
||||||
|
Wir sehen also:
|
||||||
|
|
||||||
|
- Blätter des Baumes stellen die Literale/eingegebenen Zahlen dar
|
||||||
|
- Innere Knoten des Baumes wissen, welche Art von Rechenoperation sie
|
||||||
|
darstellen und welche Kinder sie besitzen
|
||||||
|
|
||||||
|
Erstellt zunächst einen Typ `Op`, welcher eine Rechenoperation darstellt.
|
||||||
|
Dieser Typ soll dann eine Methode `apply()` bekommen.
|
||||||
|
Diese Methode bekommt zwei Zahlen und gibt das Ergebnis der Operation
|
||||||
|
angewendet auf diese beiden Zahlen zurück.
|
||||||
|
|
||||||
|
Danach soll der Typ `Expr` erstellt werden, der letztendlich den Parsetree
|
||||||
|
darstellt.
|
||||||
|
*Tipp*: Auch wenn wir immer nur zwei Kinder pro Knoten erwarten, könnt ihr hier
|
||||||
|
gerne `Vec<_>` benutzen!
|
||||||
|
|
||||||
|
Dieser Typ soll eine Methode `evaluate()` bekommen, welche den ganzen Baum
|
||||||
|
auswertet (also das Ergebnis ausrechnet).
|
||||||
|
Die Methode würde also auf dem oben gezeigten Baum "8" zurückgeben.
|
||||||
|
Das ist also Schritt 3 aus der oben genannten Liste von Schritten.
|
||||||
|
|
||||||
|
Baut euch in einer kleinen Hilfsfunktion manuell eine Instanz (z.B. den oben
|
||||||
|
gezeigten Baum) von `Expr` zusammen und testet `evaluate()` auf dieser Instanz.
|
||||||
|
Ruft diese Hilfsfunktion in `main()` auf, um das Ergebnis zu prüfen.
|
||||||
|
Löscht diese Hilfsfunktion auch nicht, sodass euer Tutor sie später nutzen
|
||||||
|
kann.
|
||||||
|
|
||||||
|
|
||||||
|
c) Parsing
|
||||||
|
----------
|
||||||
|
|
||||||
|
Dieser zweite Schritt ist der komplizierteste.
|
||||||
|
Ihr müsst diese Teilaufgabe nicht komplett lösen, da die komplette Lösung
|
||||||
|
nicht gerade trivial ist.
|
||||||
|
Mindestanforderung ist jedoch, dass ihr die folgenden Eingaben parsen könnt:
|
||||||
|
|
||||||
|
- `3`
|
||||||
|
- `1 + 2`
|
||||||
|
- `9 - 6`
|
||||||
|
|
||||||
|
Das Parsing soll in einer Konstruktorfunktion von `Expr` geschehen.
|
||||||
|
Nennen wir diese Funktion einfach `parse()`.
|
||||||
|
Die Funktion bekommt natürlich den vorher generierten Tokenstream und soll
|
||||||
|
bei Erfolg eine `Expr`-Instanz zurückgeben, welche den Parsetree darstellt.
|
||||||
|
|
||||||
|
Aber natürlich kann das Parsing auch auf Fehler in der Eingabe stoßen.
|
||||||
|
Zum Beispiel sollten Fehler wie eine leere Eingabe und eine falsche Syntax
|
||||||
|
gemeldet werden.
|
||||||
|
|
||||||
|
Ein Hinweis zur Syntax: Wir gehen davon aus, dass jeder Knoten im Baum nur zwei
|
||||||
|
Kinder hat und Rechnungen immer geklammert sind.
|
||||||
|
Eine Eingabe von `3 + 4 + 5` ist also ungültig und müsste als `3 + (4 + 5)`
|
||||||
|
oder `(3 + 4) + 5` eingegeben werden.
|
||||||
|
Das vereinfacht das Parsing stark.
|
||||||
|
Unsere Grammatik ist also:
|
||||||
|
|
||||||
|
```
|
||||||
|
expr := ⟨operand⟩ [⟨op⟩ ⟨operand⟩]
|
||||||
|
operand := ⟨num⟩ | "(" ⟨expr⟩ ")"
|
||||||
|
op := "+" | "-"
|
||||||
|
num := "0" | "1" | ... | "9"
|
||||||
|
```
|
||||||
|
|
||||||
|
*Tipps*:
|
||||||
|
|
||||||
|
- Wenn ihr versucht, den kompletten Parser zu implementieren, bietet
|
||||||
|
sich natürlich eine Art von Rekursion an.
|
||||||
|
- Es gibt recht viele Wege, diesen Parser zu implementieren. Ein Weg, den ich
|
||||||
|
recht bequem finde: Man speichert sich den aktuellen Index im Tokenstream
|
||||||
|
und erhöht diesen immer, wenn man weitere Tokens verarbeitet hat.
|
||||||
|
- Kleine Hilfsfunktionen können doppelten Code vermeiden.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Zuletzt (unabhängig davon, ob ihr nur den halben oder ganzen Parser
|
||||||
|
implementiert habt) soll alles in `main()` benutzt werden, um den
|
||||||
|
Taschenrechner funktionsfähig zu machen.
|
35
aufgaben/sheet4/task2/calculator.rs
Executable file
35
aufgaben/sheet4/task2/calculator.rs
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
fn main() {
|
||||||
|
loop {
|
||||||
|
// Read input from the user and just do nothing when the input is empty
|
||||||
|
let input = read_string();
|
||||||
|
if input.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug output
|
||||||
|
println!("{}", input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Reads a string from the user (with a nice prompt).
|
||||||
|
fn read_string() -> String {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
// Print prompt
|
||||||
|
print!("calc > ");
|
||||||
|
std::io::stdout().flush().unwrap();
|
||||||
|
|
||||||
|
// Read line
|
||||||
|
let mut buffer = String::new();
|
||||||
|
std::io::stdin()
|
||||||
|
.read_line(&mut buffer)
|
||||||
|
.expect("something went horribly wrong...");
|
||||||
|
|
||||||
|
// Discard trailing newline
|
||||||
|
let new_len = buffer.trim_right().len();
|
||||||
|
buffer.truncate(new_len);
|
||||||
|
|
||||||
|
buffer
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user