mirror of
https://github.com/Dummi26/mers.git
synced 2025-03-10 14:13:52 +01:00
updated tutor and changed 'while' to 'loop' because while should be while <condition> <statement> while loop is loop <statement>, which is the actual syntax mers uses.
This commit is contained in:
parent
e8ee005743
commit
49f465c444
14
README.md
14
README.md
@ -128,7 +128,7 @@ Let's build a counter app: We start at 0. If the user types '+', we increment th
|
||||
|
||||
The first thing we will need for this is a loop to prevent the app from stopping after the first user input:
|
||||
|
||||
while {
|
||||
loop {
|
||||
println("...")
|
||||
}
|
||||
|
||||
@ -137,7 +137,7 @@ Running this should spam your terminal with '...'.
|
||||
Now let's add a counter variable, read user input and print the status message.
|
||||
|
||||
counter = 0
|
||||
while {
|
||||
loop {
|
||||
input = read_line()
|
||||
println("The counter is currently at {0}. Type + or - to change it.".format(counter.to_string()))
|
||||
}
|
||||
@ -145,7 +145,7 @@ Now let's add a counter variable, read user input and print the status message.
|
||||
We can then use `eq(a b)` to check if the input is equal to + or -, and then decide to increase or decrease counter:
|
||||
|
||||
counter = 0
|
||||
while {
|
||||
loop {
|
||||
input = read_line()
|
||||
if input.eq("+") {
|
||||
counter = counter.add(1)
|
||||
@ -177,7 +177,7 @@ In fact, `fn difference(a int/float b int/float) if a.gt(b) a.sub(b) else b.sub(
|
||||
Let's replace the if statement from before with a match statement!
|
||||
|
||||
counter = 0
|
||||
while {
|
||||
loop {
|
||||
input = read_line()
|
||||
match input {
|
||||
input.eq("+") counter = counter.add(1)
|
||||
@ -199,7 +199,7 @@ Match statements are a lot more powerful than if-else-statements, but this will
|
||||
Loops will break if the value returned in the current iteration matches:
|
||||
|
||||
i = 0
|
||||
res = while {
|
||||
res = loop {
|
||||
i = i.add(1)
|
||||
i.gt(50)
|
||||
}
|
||||
@ -209,7 +209,7 @@ This will increment i until it reaches 51.
|
||||
Because `51.gt(50)` returns `true`, `res` will be set to `true`.
|
||||
|
||||
i = 0
|
||||
res = while {
|
||||
res = loop {
|
||||
i = i.add(1)
|
||||
if i.gt(50) i else []
|
||||
}
|
||||
@ -217,7 +217,7 @@ Because `51.gt(50)` returns `true`, `res` will be set to `true`.
|
||||
|
||||
Because a value of type int matches, we now break with "res: 51". For more complicated examples, using `[i]` instead of just `i` is recommended because `[i]` matches even if `i` doesn't.
|
||||
|
||||
A while loop's return type will be the matches of the inner return type.
|
||||
A loop's return type will be the matches of the inner return type.
|
||||
|
||||
For for loops, which can also end without a value matching, the return type is the same plus the empty tuple `[]`:
|
||||
|
||||
|
@ -588,6 +588,10 @@ fn parse_statement_adv(
|
||||
.to()
|
||||
}
|
||||
"while" => {
|
||||
eprintln!("Warn: 'while' is now 'loop'. At some point, this will just be an error instead of a warning.");
|
||||
break SStatementEnum::Loop(parse_statement(file)?).to();
|
||||
}
|
||||
"loop" => {
|
||||
break SStatementEnum::Loop(parse_statement(file)?).to();
|
||||
}
|
||||
"switch" | "switch!" => {
|
||||
|
@ -195,7 +195,7 @@ impl RStatementEnum {
|
||||
}
|
||||
}
|
||||
Self::Loop(c) => loop {
|
||||
// While loops will break if the value matches.
|
||||
// loops will break if the value matches.
|
||||
if let Some(break_val) = c.run(vars, info).data.matches() {
|
||||
break break_val;
|
||||
}
|
||||
|
@ -18,135 +18,38 @@ pub fn run(tutor: &mut Tutor) {
|
||||
// b = \"some text\" // mers knows: b is a string
|
||||
// sub(a b) // mers knows: it can't subtract a string from an int
|
||||
|
||||
// Traditional statically-typed languages achieve this same type-safety:
|
||||
// C / C++ / Java / C#
|
||||
// int a = 10;
|
||||
// int b = 5;
|
||||
// int c = a - b;
|
||||
// In C#, we can just use 'var' to automatically infer the types
|
||||
// var a = 10;
|
||||
// var b = 5;
|
||||
// var c = a - b; // all of these are ints, and C# knows this
|
||||
// Not specifying a type for variables is the default in Rust...
|
||||
// let a = 10;
|
||||
// let b = 5;
|
||||
// let c = a - b;
|
||||
// ... and Go
|
||||
// a := 10
|
||||
// b := 5
|
||||
// c := a - b
|
||||
// Dynamically-typed languages don't need to know the type of their variables at all:
|
||||
// JavaScript
|
||||
// let a = 10
|
||||
// let b = 5
|
||||
// let c = a - b
|
||||
// Also JavaScript (c becomes NaN in this example)
|
||||
// let a = \"some text\"
|
||||
// let b = 5
|
||||
// let c = a - b
|
||||
// However, there are some things dynamic typing can let us do that static typing can't:
|
||||
// JavaScript
|
||||
// let x
|
||||
// if (condition()) {
|
||||
// x = 10
|
||||
// } else {
|
||||
// x = \"some string\"
|
||||
// }
|
||||
// console.log(x) // we can't know if x is an int or a string, but it will work either way.
|
||||
// We *could* implement this in Rust:
|
||||
// enum StringOrInt {
|
||||
// S(String),
|
||||
// I(i32),
|
||||
// }
|
||||
// impl std::fmt::Display for StringOrInt {
|
||||
// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// match self {
|
||||
// Self::S(s) => write!(f, \"{}\", s),
|
||||
// Self::I(i) => write!(f, \"{}\", i),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// fn main() {
|
||||
// let x;
|
||||
// if condition() {
|
||||
// x = StringOrInt::I(10)
|
||||
// } else {
|
||||
// x = StringOrInt::S(format!(\"some string\"));
|
||||
// }
|
||||
// println!(\"{}\", x);
|
||||
// }
|
||||
// While this works, it's a lot of effort. But don't worry, we can do better!
|
||||
// Mers (it doesn't let you declare variables without initializing them, but that doesn't really matter for the example)
|
||||
// x = if condition() {
|
||||
// 10
|
||||
// } else {
|
||||
// \"some string\"
|
||||
// }
|
||||
// println(\"x = {0}\".format(x))
|
||||
// Okay, but how can we keep the type-safety of statically typed languages if code like this is valid in mers?
|
||||
// To figure this out, let's just ask mers for the type of x using 'switch! var {}'.
|
||||
// (you can always reload and check the top of the file to see mers' full error)
|
||||
// Just like other statically-typed languages, mers achieves this safety by assigning a certain type to each variable (technically to each statement).
|
||||
// However, mers' type-system has one quirk that sets it apart from most others:
|
||||
|
||||
a = \"this is clearly a string\"
|
||||
switch! a {} // string
|
||||
|
||||
b = 10
|
||||
switch! b {} // int
|
||||
|
||||
// Now comment out the two switch statements, reload the file and look at the error produced by 'switch! x {}'.
|
||||
|
||||
x = if true {
|
||||
10
|
||||
a = if true {
|
||||
\"some string\"
|
||||
} else {
|
||||
\"a string\"
|
||||
12
|
||||
}
|
||||
switch! x {}
|
||||
switch! a {}
|
||||
|
||||
// Mers doesn't know if x is an int or a string, but it knows that it has to be either of those and can't be anything else.
|
||||
// And this doesn't even rely on any fancy technology, it's all just based on simple and intuitive rules:
|
||||
// x is the result of an if-else statement, meaning x's type is just the return types from the two branches combined: int and string -> int/string.
|
||||
// if we remove the else, x's type would be int/[]: either an int or nothing.
|
||||
// Instead of using switch! to get a compiler error, you can also look at the types at runtime using the builtin debug() function:
|
||||
// x.debug() // (or debug(x)) | this outputs 'int/string :: int :: 10' | debug()'s format is:
|
||||
// the statement's type (this is constant and determined at compile-time) :: the value's type (this can change, but it's always one of the types in the constant type) :: string-representation of the value
|
||||
// (don't forget to comment out the third switch statement, too. you can't return to the menu otherwise)
|
||||
// A type in mers can consist of multiple single types: The type of a is 'string/int', because it could be either a string or an int.
|
||||
// You can see this type mentioned in the error at the top of the file, which shows up because 'switch!' wants us to handle all possible types,
|
||||
// yet we don't handle any ('{}').
|
||||
|
||||
// By combining multiple-types with tuples, we can express complicated data structures without needing structs or enums:
|
||||
// [int/float int/float]/int/float // one or two numbers
|
||||
// [int/float ...] // a list of numbers
|
||||
// By combining tuples ('[a b c]') with the idea of multiple-types, you can create complex datastructures.
|
||||
// You effectively have all the power of Rust enums (enums containing values) and structs combined:
|
||||
// Rust's Option<T>: t/[] or [t]/[] if t can be [] itself
|
||||
// Rust's Result<T, E>: T/Err(E)
|
||||
|
||||
// Mers does have enums, but they are different from what other languages have.
|
||||
// Enums are just identifiers that can be used to specify what a certain type is supposed to be used for.
|
||||
// Consider a function read_file() that wants to return a the file's contents as a string.
|
||||
// If the file doesn't exist or can't be read, the function should return some sort of error.
|
||||
// Both of these can be the type string: \"this is my file\" and \"Couldn't read file: Permission denied.\".
|
||||
// To avoid this ambiguity, we can wrap the error in an enum:
|
||||
// \"this is my file\" and Err: \"Couldn't read file: Permission denied.\".
|
||||
// Instead of the function just returning string, it now returns string/Err(string).
|
||||
// This shows programmers that your function can fail and at the same time tells mers
|
||||
// that the function returns two different types that both need to be handeled:
|
||||
fn read_file() {
|
||||
if false {
|
||||
// successfully read the file
|
||||
\"this is my file\"
|
||||
} else {
|
||||
Err: \"Couldn't read file: I didn't even try.\"
|
||||
}
|
||||
}
|
||||
file = read_file()
|
||||
// without switching, I can't get to the file's content:
|
||||
println(file) // this causes an error!
|
||||
// using switch! instead of switch forces me to handle all types, including the error path.
|
||||
switch! file {
|
||||
string {
|
||||
println(\"File content: {0}\".format(file))
|
||||
}
|
||||
Err(string) {
|
||||
println(\"Error! {0}\".format(file.noenum()))
|
||||
}
|
||||
}
|
||||
// The Err(E) is mers' version of an enum. An enum in mers is an identifier ('Err') wrapping a type ('E').
|
||||
// They don't need to be declared anywhere. You can just return 'PossiblyWrongValue: 0.33' from your function and mers will handle the rest.
|
||||
// To access the inner value, you can use the noenum() function:
|
||||
// result = SomeValueInAnEnum: \"a string\"
|
||||
// println(result) // error - result is not a string
|
||||
// println(result.noenum()) // works because result is an enum containing a string
|
||||
|
||||
// To return to the menu, fix all the errors in this file.
|
||||
// the \\S+ regex matches anything but whitespaces
|
||||
words_in_string = \"some string\".regex(\"\\\\S+\")
|
||||
switch! words_in_string {}
|
||||
// Types to cover: [string ...]/Err(string) - If the regex is invalid, regex() will return an error.
|
||||
|
||||
// To return to the menu, fix all compiler errors (comment out all switch! statements).
|
||||
|
||||
true
|
||||
"));
|
||||
|
59
mers/src/tutor/error_handling.rs
Normal file
59
mers/src/tutor/error_handling.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use crate::script::val_data::VDataEnum;
|
||||
|
||||
use super::Tutor;
|
||||
|
||||
pub fn run(tutor: &mut Tutor) {
|
||||
tutor.update(Some("
|
||||
// Error handling in mers is inspired by Rust. Errors aren't a language feature,
|
||||
// they are just a value like any other. Usually, errors have the type Err(string) or Err(SomeOtherEnum(string)),
|
||||
// but this is still just a value in an enum.
|
||||
// Because of the type system in mers, errors can just be used and don't need any special language features.
|
||||
// This part of the mers-tutor isn't as much about error handling as it is about dealing with multiple types when you only want some of them, not all,
|
||||
// but since error handling is the main use-case for this, it felt like the better title for this section.
|
||||
|
||||
// 1. [t]/[]
|
||||
// This acts like null/nil in most languages or Option<T> in rust.
|
||||
// This type indicates either '[]', a tuple of length 0 and therefore no data (null/nil/None)
|
||||
// or '[t]', a tuple of length 1 - one value. This has to be [t] and not just t because t might be [], which would otherwise cause ambiguity.
|
||||
|
||||
// The type [t]/[] is returned by get(), a function that retrieves an item from a list and returns [] if the index was less than 0 or too big for the given list:
|
||||
list = [1 2 3 4 5 ...]
|
||||
first = list.get(0) // = [1]
|
||||
second = list.get(1) // = [2]
|
||||
does_not_exist = list.get(9) // = []
|
||||
|
||||
// To handle the result from get(), we can switch on the type:
|
||||
switch! first {
|
||||
[int] \"First element in the list: {0}\".format(first.0.to_string())
|
||||
[] \"List was empty!\"
|
||||
}
|
||||
|
||||
// If we already know that the list isn't empty, we can use assume1(). This function takes a [t]/[] and returns t. If it gets called with [], it will crash your program.
|
||||
\"First element in the list: {0}\".format(first.assume1().to_string())
|
||||
|
||||
// 2. t/Err(e)
|
||||
// This acts like Rust's Result<T, E> and is used in error-handling.
|
||||
// This is mainly used by functions that do I/O (fs_* and run_command) and can also be handeled using switch or switch! statements.
|
||||
// Use switch! or .debug() to see the types returned by these functions in detail.
|
||||
// If switching is too much effort for you and you would like to just crash the program on any error,
|
||||
// you can use assume_no_enum() to ignore all enum types:
|
||||
// - t/Err(e) becomes t
|
||||
// - int/float/string/Err(e)/Err(a) becomes int/float/string
|
||||
|
||||
// To return to the menu, change the index in list.get() so that it returns a value of type [int] instead of [].
|
||||
list.get(8)
|
||||
"));
|
||||
loop {
|
||||
match tutor.let_user_make_change().run(vec![]).data {
|
||||
VDataEnum::Tuple(v) if !v.is_empty() => {
|
||||
break;
|
||||
}
|
||||
other => {
|
||||
tutor.set_status(format!(
|
||||
" - Returned {other} instead of a value of type [int]."
|
||||
));
|
||||
tutor.update(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ use crate::script::val_data::VDataEnum;
|
||||
|
||||
use super::Tutor;
|
||||
|
||||
pub const MAX_POS: usize = 6;
|
||||
pub const MAX_POS: usize = 7;
|
||||
|
||||
pub fn run(mut tutor: Tutor) {
|
||||
loop {
|
||||
@ -18,6 +18,7 @@ fn go_to() 0
|
||||
// 4 Variables
|
||||
// 5 Returns
|
||||
// 6 Types
|
||||
// 7 Error handling
|
||||
|
||||
go_to()
|
||||
",
|
||||
@ -34,6 +35,7 @@ go_to()
|
||||
4 => super::base_variables::run(&mut tutor),
|
||||
5 => super::base_return::run(&mut tutor),
|
||||
6 => super::base_types::run(&mut tutor),
|
||||
7 => super::error_handling::run(&mut tutor),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ mod base_return;
|
||||
mod base_types;
|
||||
mod base_values;
|
||||
mod base_variables;
|
||||
mod error_handling;
|
||||
mod menu;
|
||||
|
||||
pub fn start(spawn_new_terminal_for_editor: bool) {
|
||||
@ -21,6 +22,8 @@ pub fn start(spawn_new_terminal_for_editor: bool) {
|
||||
|
||||
// This is an interactive experience. After making a change to this file,
|
||||
// save and then reload it to see the tutor's updates.
|
||||
// DO NOT save the file twice without reloading because you might overwrite changes made by the tutor,
|
||||
// which can completely ruin the file's formatting until the next full update (page change)!
|
||||
// To begin, change the following value from false to true:
|
||||
|
||||
false
|
||||
|
Loading…
Reference in New Issue
Block a user