Week 07 Weekly Exercises
Objectives
- Use and Understand the different `Fn` traits
- Practical usage of beginner and complex macros
- Theoretical understanding on the advantages of macros as a method of metaprogramming
Activities To Be Completed
The following is a list of all the markedactivities available to complete this week...
- Fun types!
- Average Macro
- Repetition
- Currying
- Hooked - our small CPU
The following practice activities are optional and are not marked, or required to be completed for the week.
-
None - all exercises this week are marked.
Preparation
Before attempting the weekly exercises you should re-read the relevant lecture slides and their accompanying examples.
Getting Started
Create a new directory for this week's exercises called lab07,
change to this directory,
and fetch the provided code for this week
by running these commands:
mkdir lab07 cd lab07 6991 fetch lab 07
Or, if you're not working on CSE, you can download the provided code as a tar file.
Exercise:
Fun types!
Fn traits to
do this!
As a reminder, there are three Fn traits.
To quote from the book
FnOnceapplies to closures that can be called once. All closures implement at least this trait, because all closures can be called. A closure that moves captured values out of its body will only implementFnOnceand none of the other Fn traits, because it can only be called once.FnMutapplies to closures that don’t move captured values out of their body, but that might mutate the captured values. These closures can be called more than once.Fnapplies to closures that don’t move captured values out of their body and that don’t mutate captured values, as well as closures that capture nothing from their environment. These closures can be called more than once without mutating their environment, which is important in cases such as calling a closure multiple times concurrently.
1. Implement MyOption::map You've been given a
MyOption enum that is similar to the standard library's
Option
enum. Implement the map method on MyOption that takes a closure as
an argument and returns a MyOption with the closure applied to the
value inside the Some variant. If the MyOption is
None, then return None.
You should not use the standard library's Option::map method in your
implementation.
2. Implement MyVec::map
You've been given a MyVec struct that is a wrapper over a
Vec. Implement
the map method on MyVec that takes a closure as an argument,
and applies that closure to each element in the Vec. The
map method should
act "in place", and should not return anything.
You should not use the standard library's Vec::map method in your
implementation.
3. Implement MyVec::for_each Implement the
for_each method on MyVec that takes a closure as an
argument, and runs that closure with each element in the Vec The
for_each method should not return anything.
You should not use the standard library's Vec::for_each method
in your implementation.
When you think your program is working,
you can use autotest
to run some simple automated tests:
6991 autotest
When you are finished working on this exercise, you must submit your work by running give:
6991 give-crate
The due date for this exercise is Week 8 Wednesday 21:00:00.
Note that this is an individual exercise;
the work you submit with give must be entirely your own.
Exercise:
Average Macro
In this exercise, we will be writing a basic rust macro!
Your task is to create a macro called avg
that takes in comma seperated expressions, and returns the average of those expressions.
We will make the assumption that the macro is always called with expressions that are all:
- numeric
- can be converted to a numeric type for addition and division
fn main() { let a = avg!(1, 2, 3, 4, 5); // might expand to /* let a = { let mut sum = 0; let mut len = 0; sum += 1; len += 1; sum += 2; len += 1; sum += 3; len += 1; sum += 4; len += 1; sum += 5; len += 1; sum / len }; */ }You cannot modify the main function.
When you are finished working on this exercise, you must submit your work by running give:
6991 give-crate
The due date for this exercise is Week 8 Wednesday 21:00:00.
Note that this is an individual exercise;
the work you submit with give must be entirely your own.
Exercise:
Repetition
The following activity is taken verbatim from exercise 6 of MakroKata - a set of exercises which you can use as a resource to learn how to write macros in rust.
Consider the below macro:
macro_rules! listing_literals { (the $e1:literal) => { { let mut my_vec = Vec::new(); my_vec.push($e1); my_vec } }; (the $e1:literal and the $e2:literal) => { { let mut my_vec = Vec::new(); my_vec.push($e1); my_vec.push($e2); my_vec } }; (the $e1:literal and the $e2:literal and the $e3:literal) => { { let mut my_vec = Vec::new(); my_vec.push($e1); my_vec.push($e2); my_vec.push($e3); my_vec } } } fn main() { let vec: Vec<&str> = listing_literals!(the "lion" and the "witch" and the "wardrobe"); assert_eq!(vec, vec!["lion", "witch", "wardrobe"]); let vec: Vec<i32> = listing_literals!(the 9 and the 5); assert_eq!(vec, vec![9, 5]); }
This is very clunky, and involves a large amount of repeated code. Imagine doing this for 10 arguments! What if we could say that we want a variable number of a particular patterns. That would let us say "give me any number of $e:expr tokens, and I'll tell you what to do with them'".
In this task, you will be creating an if_any! macro.
If any of the first arguments are true, it should execute the block
which will always be the last argument.
You may not edit the main function; but once you have completed
the exercise, your if_any! macro should expand to look like the following:
fn main() { // your macro call: // // if_any!(false, 0 == 1, true; { // print_success(); // }) // // should expand to: if (false || 0 == 1 || true) { print_success(); } }
You can test this yourself, by running:
6991 cargo expand main
fn main() {
if (false || 0 == 1 || true) {
print_success();
}
}
When you are finished working on this exercise, you must submit your work by running give:
6991 give-crate
The due date for this exercise is Week 8 Wednesday 21:00:00.
Note that this is an individual exercise;
the work you submit with give must be entirely your own.
Challenge
Exercise:
Currying
This exercise is a sort of culmination of everything you've learned so far about macros. To complete it, you'll need to note one important fact -- macros can recurse into themselves.
enum LinkedList { Node(i32, Box<LinkedList>), Empty } macro_rules! linked_list { () => { LinkedList::Empty }; ($expr:expr $(, $exprs:expr)*) => { LinkedList::Node($expr, Box::new(linked_list!($($exprs),*))) } } fn main() { let my_list = linked_list!(3, 4, 5); }
The above example is very typical. The first rule is the "base case" -- an
empty list of tokens implies an empty linked list.
The second rule always matches one expression first (expr). This allows us to
refer to it on its own, in this case to create the Node. The rest of the
expressions (exprs) are stored in a repetition; and all we'll do with them is
recurse into linked_list!().
If there's no expressions left, that call to
linked_list!() will give back Empty;
otherwise it'll repeat the same process.
While Macro Recursion is incredibly powerful; it is also slow. As a result,
there is a limit to the amount of recursion you are allowed to do. In rustc,
the limit is 128; but you can configure by adding
#![recursion_limit = "256"] as
a crate level attribute (i.e. - the first line of main.rs).
Before you complete the exercise, let's briefly discuss a concept called "currying". If you're already familiar with the concept, perhaps from your own experience of functional programming; you can skip until the exercise description.
In most imperative languages, the syntax to call a function with multiple
arguments is function(arg1, arg2, arg3).
If you do not provide all the
arguments, that is an error.
In many functional languages, however, the syntax
for function calls is more akin to function(arg1)(arg2)(arg3).
The advantage of this notation is that if you specify less than the
required number of arguments; it's not an error -- you get back a function that
takes the rest of the arguments. A function that behaves this way is said to
be "curried" (named after Haskell Curry, a famous mathematician).
A good example of this is a curried add function. In regular Rust, we'd say add
is move |a, b| a + b is the add function.
If we curried that function, we'd have add is
move |a| move |b| a + b. What this means is that we can write
let add_1 = add(1);
and we now have a function at add_1 which, when called,
will add 1 to anything.
In this exercise, you will build a macro which creates a curried function. The
syntax for this function will be
curry!((a: i32) => (b: i32) => _, {a + b}).
Each pair of ident: ty is an argument; and the last __
indicates that the compiler will infer the return type.
The block provided last is, of course, the computation we want to do after
receiving all the arguments.
You can test your solution yourself, by running cargo expand, which should
output:
6991 cargo expand main
fn main() { let is_between = move |min: i32| move |max: i32| move |item: &i32| { min < *item && *item < max }; let curry_filter_between = move |min: i32| move |max: i32| move |vec: &Vec<i32>| { let filter_between = is_between(min)(max); vec.iter() .filter_map(|i| if filter_between(i) { Some(*i) } else { None }) .collect() }; let between_3_7 = curry_filter_between(3)(7); let between_5_10 = curry_filter_between(5)(10); let my_vec = get_example_vec(); let some_numbers: Vec<i32> = between_3_7(&my_vec); print_numbers(&some_numbers); let more_numbers: Vec<i32> = between_5_10(&my_vec); print_numbers(&more_numbers); }
When you are finished working on this exercise, you must submit your work by running give:
6991 give-crate
The due date for this exercise is Week 8 Wednesday 21:00:00.
Note that this is an individual exercise;
the work you submit with give must be entirely your own.
Challenge
Exercise:
Hooked - our small CPU
You have been tasked to extend this CPU
to support the new Callback
instruction, which allows
the CPU to execute a closure some amount of instructions
in the future.
For example, if the CPU is currently at instruction 10, and the next instruction is
Callback(Hook::new(2, |cpu| { println!("Hello World!"); }))
Then the CPU should execute the next two instructions, and then execute the closure, printing "Hello World!".
The CPU should then continue executing instructions as normal.
You may modify whatever code you like, except for the main
function, which should remain unchanged. A Hook
wrapper type has been provided for you, which you should use
to implement the Callback instruction.
The CPU should be able to execute the following program (test 3):
Instruction::Nop,
Instruction::PrintAccumulator,
Instruction::AddLiteral(1),
Instruction::Callback(Hook::new(2, |cpu: &mut Cpu| {
cpu.accumulator += 6991;
})),
Instruction::Nop,
Instruction::Nop,
Instruction::JumpIfCondition(|cpu| cpu.accumulator <= 6991, 3.into()),
Instruction::SubLiteral(1),
Instruction::PrintAccumulator,
Instruction::Quit,
With the output:
...no-op
...print accumulator
Accumulator: 0
...adding 1
...callback instruction
...no-op
...no-op
...conditional jump
...subtracting 1
...print accumulator
Accumulator: 6991
You can run tests with the following command:
6991 cargo run -- 1
Compiling hooked v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 0.35s
Running `target/debug/hooked 1`
...no-op
...print accumulator
Accumulator: 0
...adding 1
...print accumulator
Accumulator: 1
...conditional jump
When you think your program is working,
you can use autotest
to run some simple automated tests:
6991 autotest
When you are finished working on this exercise, you must submit your work by running give:
6991 give-crate
The due date for this exercise is Week 8 Wednesday 21:00:00.
Note that this is an individual exercise;
the work you submit with give must be entirely your own.
Submission
When you are finished each exercise make sure you submit your work by running give.
You can run give multiple times.
Don't submit any exercises you haven't attempted.
If you are working at home, you may find it more convenient to upload your work via give's web interface.
The due date for this week's exercises is Week 8 Wednesday 21:00:00.
You cannot obtain marks by e-mailing your code to tutors or lecturers.
Automarking will be run continuously throughout the term,
using test cases different to those autotest runs for you.
(Hint: do your own testing as well as running autotest.)
After automarking is run you can view your results here or by running this command on a CSE machine:
6991 classrun -sturec