Skip to content

Implement Robot Simulator #146

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 7, 2016

Conversation

IanWhitney
Copy link
Contributor

@IanWhitney IanWhitney commented Jun 27, 2016

Implement Robot Simulator

This follows the current standard test suite

The stub file, as we discussed in #117, is just enough to run cargo test and not get any errors from ignored tests.

Robot {}
}

pub fn turn_right(&mut self) {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would pre-fill the functions with unimplemented!() just to make sure, that you know where to put the implementation. This is more important for functions which return values, because unimplemented!() always typechecks, no matter the type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Done. Thanks!

@jonasbb
Copy link
Contributor

jonasbb commented Jun 27, 2016

Your test suite will push them to a certain solution in any case. If you set mut it is more likely to change the state of the struct, but if you remove mut and return a new struct, you need to adapt your test cases to support this.

IanWhitney pushed a commit to IanWhitney/xrust that referenced this pull request Jun 27, 2016
As suggested by jonasbb

exercism#146 (comment)
@IanWhitney
Copy link
Contributor Author

Ah, right, if I remove the mut then the signature would change to

fn turn_right(&self) -> Self {
  Robot::New{...}
}

Which might be interesting. I don't think any of our exercises take that approach.

IanWhitney pushed a commit to IanWhitney/xrust that referenced this pull request Jun 29, 2016
As mentioned here:
exercism#146 (comment)

I like the idea of pushing towards a solution with immutable robots.
It's a different way of using structs than our other exercises do. As a
side benefit, it shortens the test functions.

Students can always edit the test file or the stub file, if they so
wish. So they could still implement mutable robots, though I don't think
many (or any, probably) will do so.
@IanWhitney
Copy link
Contributor Author

Two tweaks:

  • Stub & Tests now use immutable robots.
  • I'm silencing some warnings

@IanWhitney
Copy link
Contributor Author

I'm unsure what the direction function should return -- Direction or &Direction?

Currently the Robot new function uses &Direction, which makes sense to me. Students can choose to handle the lifetimes that come with a struct using a borrowed reference, or they can derive Clone and just clone the Direction. That works for me.

But when getting the Direction back out, what is the idiomatic thing to do? My instinct says that the function should be

fn direction(&self) -> &Direction

But my instinct has been wrong a lot.

@steveklabnik
Copy link
Contributor

usually, accessor methods return &T. If they returned T, then they would destroy the owner, or you'd be making a clone every time.

@IanWhitney
Copy link
Contributor Author

Ooh, my instinct was right for once. Go team instinct! 👏

@IanWhitney
Copy link
Contributor Author

What about primitives, @steveklabnik ?

For example, given the struct

struct Robot {
  x: isize,
  y: isize
}

Would you expect a position function to be:

fn postion(&self) -> (isize, isize)

or

fn postion(&self) -> (&isize, &isize)

Alternatively we could create a whole new Position type, like we did with queen attack

@steveklabnik
Copy link
Contributor

If the type is Copy, then they're basically the same, so yeah, returning T is fine. Consider http://doc.rust-lang.org/stable/collections/vec/struct.Vec.html#method.len as an example

@IanWhitney
Copy link
Contributor Author

That's what I was thinking as well. I'm clearly on a roll today.

IanWhitney pushed a commit to IanWhitney/xrust that referenced this pull request Jun 29, 2016
As discussed

exercism#146 (comment)

and

exercism#146 (comment)

Per Steve Klabnik

> usually, accessor methods return &T. If they returned T, then they
> would destroy the owner, or you'd be making a clone every time
@Dr-Emann
Copy link

Taking by value and returning a copy just doesn't feel rusty-y to me. I expect functions to take the most general type of arguments they can, &T if no mutation, &mut T if mutation needed, and T only if ownership is required.

@IanWhitney
Copy link
Contributor Author

Working example in place. Now to figure out where to place this problem.

The solution will involve:

  • Enums (though the stub file provides them)
  • Structs (either a tuple struct, or a normal struct)
  • Some matching on Direction variant
  • Iteration

And, if the student chooses not to clone the Direction, they may end up in a world of Lifetimes. That, though, is optional.

I'm thinking it should go around grade-school. It's got a pretty straightforward use of enums, so it should come before Allergies (which has slightly more complex enum usage). But beyond enums, students will be familiar with all the tools they need to solve this by the time they reach grade-school

@IanWhitney
Copy link
Contributor Author

So what API are you proposing, @Dr-Emann?

@Dr-Emann
Copy link

I would expect turn_right, turn_left, advance, and instructions to take &mut self, instead of self, and return nothing.

@IanWhitney
Copy link
Contributor Author

That was the initial implementation. And it might be the right one. But I'm leaning towards immutable because

  • Immutability is the default in Rust
  • No other problem (that I know of) really features Rust's default immutability
  • Immutability is not my natural inclination

I figure if I don't expect immutability then other programmers with an OO-focused background probably don't expect immutability. So a problem that forces immutability may make me (and them) think about immutability. Which I think is good.

Immutability brings other benefits, though they aren't exposed by the tests. It would be very easy to trace your Robot's path, for example. Re-winding to a specific point on the path would be trivial. All very nice, but not part of the test suite.

IanWhitney pushed a commit to IanWhitney/xrust that referenced this pull request Jun 30, 2016
As per:
exercism#146 (comment)

> The solution will involve:
>
>   Enums (though the stub file provides them)
>   Structs (either a tuple struct, or a normal struct)
>   Some matching on Direction variant
>   Iteration
>
> And, if the student chooses not to clone the Direction, they may end up
> in a world of Lifetimes. That, though, is optional.
>
> I'm thinking it should go around grade-school. It's got a pretty
> straightforward use of enums, so it should come before Allergies (which
> has slightly more complex enum usage). But beyond enums, students will
> be familiar with all the tools they need to solve this by the time they
> reach grade-school
@IanWhitney
Copy link
Contributor Author

Ok. This now passes CI and is basically ready to go. Once we settle on API and improve the example code a bit, it'll be ready to merge.

@IanWhitney
Copy link
Contributor Author

Thinking about the immutability thing more...

As I mentioned earlier, I think the immutable design advantages become clear if you want to rewind to a point. Maybe we could have an optional test that's commented out:

// Optional:
// #[test]
// fn robot_simulator_can_return_robot_at_any_point() {
// let robot = Robot::new(...);
// let simulator = RobotSimulator::new(robot);
// simulator.simulate("LAAAR");
// assert_eq!((-1,2), simulator.at_step(4).robot_position);
// assert_eq!((0,2), simulator.at_step(3).robot_position);
// etc.
// }

@IanWhitney IanWhitney changed the title WIP: Robot Simulator tests & stub file. Do not merge. Implement Robot Simulator Jul 1, 2016
@IanWhitney
Copy link
Contributor Author

I think this is now ready. Leaving the PR open until Tuesday, July 5th, after which I'll merge if there are no issues.

@Ryman
Copy link

Ryman commented Jul 1, 2016

@IanWhitney Is the intention for the example implementation to be showing idiomatic rust, or just a potential solution to pass the tests?

@IanWhitney
Copy link
Contributor Author

@Ryman That is a good question without a clear answer!

Track maintainers discussed this recently: exercism/discussions#27

Unsurprisingly, people had different opinions. I think everyone is agreed that the first job of the example code is to pass the tests (otherwise our CI fails).

Beyond that first requirement, there was general agreement (though not unanimous) that the code should be:

  • not clever
  • idiomatic
  • a good example

Probably my favorite description of the example code was this one:

[T]ry to solve the new proposed exercise as if the track maintainer were a real exercism user.

Which I most certainly am. I ended up writing Rust exercises because I wanted to use Exercism to help me learn Rust. My example code for this exercise is probably pretty close to what I would submit if I did this exercise.

@IanWhitney
Copy link
Contributor Author

That's not to say my example code is exactly idiomatic, though. I'm still quite new to Rust and am learning new stuff all the time. Which is one of the reasons I encourage discussion in these PRs.

}
}

pub fn turn_right(self) -> Self {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll not comment on whether we want to go with mutable or immutable interface.

But I want to say that if we want an immutable interface, I suspect we can do turn_right(&self) -> Self instead of turn_right(self) -> Self. Can you check?

(And if it were mutable, it would be turn_right(&mut self))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That results in compiler errors. Can not move out of borrowed content. There are additional changes we'd need to make to get that to work, though I don't know what they are off the top of my head.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment surprised me enough that I checked out your branch to take a look.

My diagnosis: Make your Position #[derive(Clone, Copy)] and it will work.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same with advance and instructions - If your Direction is Copy then they can take &self

Can't do the same for execute though since it can potentially return self, which means a &self wouldn't be able to become a Self.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh. I never would have guessed that from the compiler error. Clearly.

Based on this quote from The Book, I'll update these to take &self

We should default to using &self, as you should prefer borrowing over taking ownership, as well as taking immutable references over mutable ones.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, to be fair, making them Copy was not the only solution - could have only made them Clone, not Copy, and then explicitly call clone() on them. But I suggested Copy simply because https://doc.rust-lang.org/std/marker/trait.Copy.html said "Generally speaking, if your type can implement Copy, it should"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't always do this myself (maybe I should if I run into a compiler output I don't understand), but rustc should have outputted src/lib.rs:67:21: 67:25 help: run rustc --explain E0507 to see a detailed explanation

And I found that one of the solutions rustc --explain E0507 does say you can make it Copy, so that could have been a way to understand.

IanWhitney pushed a commit to IanWhitney/xrust that referenced this pull request Jul 3, 2016
As @petertseng pointed out, I can update these functions to borrow
`self` if I give Direction and Position `Copy`

exercism#146 (comment)
}

pub fn instructions(&self, instructions: &str) -> Self {
instructions.chars().fold(Self::build(self.position, self.direction),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't tested, but if you had Robot derive Clone, I think this Self::build(...) could just be clone() instead. But I don't see this as a change that has to be done.

@IanWhitney IanWhitney force-pushed the implement_robot_simulator branch from 9bc26e9 to 53db50e Compare July 3, 2016 16:05
@petertseng
Copy link
Member

For me, I think I've said all I want to say 👍


impl Robot {
#[allow(unused_variables)]
pub fn new(x: isize, y: isize, d: &Direction) -> Self {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Direction argument being a reference here is a bit out of place when compared to the rest of the interface, for if we assume the user may want to store &Direction in their Robot type, the other methods will not allow this to be updated to anything reasonable, i.e.:

fn turn_right(&self) -> Self {
     Robot { 
         position: self.position,
         // nowhere to tie this lifetime to as it's not an input
         direction: &self.direction.next_clockwise() 
     }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's preferred for structs to take ownership of their values, so I'm OK with pushing students towards that.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, that means fn new(x: isize, y: isize, d: Direction) -> Self rather than the current signature with the push towards the student calling clone to take ownership?

@Ryman
Copy link

Ryman commented Jul 5, 2016

Sorry for the delayed response, I left some feedback on things which may affect the users code and just left some nits on the example itself.

IanWhitney pushed a commit to IanWhitney/xrust that referenced this pull request Jul 6, 2016
@IanWhitney
Copy link
Contributor Author

Unless there's more discussion on this, I'll merge tonight.

Tests come from
https://github.com/exercism/x-common/blob/183934754b1847612809db410a8aeb640678f188/robot-simulator.json

As discussed in exercism#117 we are
providing a stub implementation so that ignored tests do not fail when a
student hasn't yet implemented the functionality they exercise.

A lot of excellent feedback and help provided by

- jonasbb
- petertseng
- steveklabnik
- Ryman
- Dr-Emann

Full details of the design discussion at

exercism#146

--

Some details that people may care about:

----

Robots are immutable because:

- Immutability is the default in Rust
- No other problem (that I know of) really features Rust's default immutability
- Immutability is not my natural inclination

I figure if I don't expect immutability then other programmers with an
OO-focused background probably don't expect immutability. So a problem
that forces immutability may make me (and them) think about
immutability. Which I think is good.

Immutability brings other benefits, though they aren't exposed by the
tests. It would be very easy to trace your Robot's path, for example.
Re-winding to a specific point on the path would be trivial. All very
nice, but not part of the test suite.

----

The example code has the `build` function because:

There's still some awkwardness around the `new` function, since it takes
x/y and all the internals use Position.

I have to preserve the `new(x, y, direction)` function signature because
of the tests (and because the tests should follow the example of Queen
Attack, which also passes `x` and `y` in as separate parameters.

But that function becomes a pain once I'm working inside the Robot and I
have a Position.

The new `build` function creates a robot using a Position and Direction,
so it can be easily used by all of the Robot's internals. And `new` now
just wraps around `build`.

This is kind of the exact opposite way I'd normally do this. I'd expect
a `build` function to be the public API, do the data manipulation and
then call a private `new` function.

But if I were to do that here then all of the tests would contain:

```
Robot::build(x, y, &direction)
```

Which I think would be non-idiomatic and weird to students.

----
@IanWhitney IanWhitney force-pushed the implement_robot_simulator branch from f6827d6 to 9129c83 Compare July 7, 2016 01:52
@IanWhitney
Copy link
Contributor Author

And in it goes. Thanks for all your input!

@IanWhitney IanWhitney merged commit 3597821 into exercism:master Jul 7, 2016
@IanWhitney IanWhitney deleted the implement_robot_simulator branch July 7, 2016 01:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants