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
FnOnce
applies 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 implementFnOnce
and none of the other Fn traits, because it can only be called once.FnMut
applies 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.Fn
applies 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: 6991You 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