A first look at rust


#1

Having done a little rust, I decided to spread the word by posting my thoughts after my first non-trivial program:
A first look at Rust

Feedback from the community is encouraged!


#2

Great post!

I would usually say “fighting the borrow checker”.

Hm, you shouldn’t have to. Do you have the code you tried?

Ah, but this isn’t a matter of “good enough”. It’s that new features land as unstable, bake on nightly for a while, and then become stable. So those new features require, well, new features, that isn’t available in the stable channel.

That said, you will be able to compile the Rust compiler with the previous stable version as of 0.10. It will special case the standard library to allow it to compile unstable things.


#3

Whenever I try using a method directly, I get an error that pretty clearly says I can’t do that. Here’s a short example:

struct Foo { x: i32 }

impl Foo {
    fn bump(&mut self) {
        self.x += 1;
    }
}

fn apply(f: &Fn()) {
    f()
}

fn main() {
   let f = Foo {x: 1};
   apply(f.bump)
}

and the error message:

test.rs:15:12: 15:16 error: attempted to take value of method bump on type Foo
test.rs:15 apply(f.bump)
^~~~
test.rs:15:12: 15:16 help: maybe a () to call it is missing? If not, try an anonymous function
error: aborting due to previous error

This is with Rust 1.8.0.


#4

Ah, I see. There’s a few problems with this sample. The end result is:

struct Foo { x: i32 }

impl Foo {
    fn bump(&mut self) {
        self.x += 1;
    }
}

fn apply(f: &Fn(&mut Foo), arg: &mut Foo) {
    f(arg)
}

fn main() {
   let mut f = Foo {x: 1};
   apply(&Foo::bump, &mut f)
}

which would more usually be written as

struct Foo { x: i32 }

impl Foo {
    fn bump(&mut self) {
        self.x += 1;
    }
}

fn apply(f: fn(&mut Foo), arg: &mut Foo) {
    f(arg)
}

fn main() {
   let mut f = Foo {x: 1};
   apply(Foo::bump, &mut f)
}

You have to treat the method like a regular function, and also take self and apply it like any other argument. Does that make sense?


#5

Well, it explains that the problem is a result of the Rust method call syntax being sugar. It still means I can’t pass a method as a function argument unless said argument is specific to the object type in question. Unfortunately, since I mostly ran into this writing gtk-rs code, I can’t change the functions being passed the callback to handle my types.

Still, at most a minor wart. And one that would be cleaned up if a general currying facility were available. That’s probably what I miss most from Haskell & Python when writing Rust. Which is a big step up considering that I’m using Rust instead of C :grinning:


#6

Is this useful to you?

struct Foo { x: i32 }

impl Foo {
    fn bump(&mut self) {
        self.x += 1;
    }
}

fn apply(f: &mut FnMut()) {
    f()
}

fn main() {
    let mut f = Foo {x: 1};
    {
        let mut curry = || -> () {
            f.bump()
        }; // mutable borrow of f
        apply(&mut curry);
    }
    println!("{}", f.x);
}

I’m not quite certain it’s appropriate to name the closure curry. I suppose it’s also possible that gtk-rs has locked you into Fn and not FnMut which was necessary to allow the closure to call a &mut self method. Using a Cell in Foo to exchange compile-time mutability rules for runtime checks permits a drop back to Fn:

use std::cell::Cell;
struct Foo { x: Cell<i32> }

impl Foo {
    fn bump(&self) {
        self.x.set(self.x.get()+1);
    }
}

fn apply(f: & Fn()) {
    f()
}

fn main() {
    let f = Foo {x: Cell::new(1)};
    let curry = || -> () {
        f.bump()
    };
    apply(& curry);
    println!("{}", f.x.get());
}

This is not a recommended use of interior mutability but it’s also a toy example that demonstrates a possibility.


#7

Why should you be able to? A method is a function which takes a value of the type it is a method of as the first argument. Functions have to be applied to all of their arguments.

You want methods to have some special partial application magic so that passing foo.bump is sugar for creating a closure like |args..| Foo::bump(foo, args..). This doesn’t seem unreasonable to me at first glance, it might be a good idea to add it to Rust. But the problem here is that your mental model of what is going on is not accurate to the semantics of the language.

If it’s not good enough for that task yet, do you want to risk it being good enough for yours?

I really dislike this sort of discourse. std using unstable features has nothing to do with whether or not the stable features are “safe” for production use.

I see this sort of statement in tool reviews all the time, and it is nothing but FUD. If there’s something that seems concerning to you about a tool, instead of just throwing it out to the universe as a meme, please investigate whether or not there is a good reason for that state of affairs.


#8

The mutability in the example was incidental - I just wanted the method to actually do something. I don’t recall having problems with rs-gtk callbacks and mutability issues.

But if you need to create a closure to wrap things, why not just do exactly what the rust compiler suggests, and use an anonymous closure for the argument? My usual fix was that, instead of writing the obvious apply(x.bump), I wrote something like apply(|| x.bump()). Same borrow issues as the original, and only a little bit longer, so there’s no real need to give the closure a name unless it’s going to be used more than once.

I agree that “curry” is the wrong name, as the result isn’t really a curry of f.bump. Curry should take a function argument and some subset of it’s parameters, and return a function that accepts the rest of the parameters and returns the value of the original function argument when applied to the union of the two sets of parameters. The only way I can see of doing that in Rust is a macro that builds and returns the appropriate closure. And I’m not sure how I’d go about doing that, even.


#9

Thanks for your effort in sharing your experience about Rust with others. I have one small quibble with the presentation. I am concerned that the way you expressed this issue can lead to negative misunderstandings of what I believe to be the reality of the situation.

I see this sort of misunderstanding often, and so it behoves me to call it your attention. I am urging you and others when you there is some issue that seems out-of-whack to you about a tool, it really would help a lot to investigate a further is there might be a reasonable explanation for the state-of-affairs. I’m concerned that misunderstandings get promulgated as memes. Again thanks for all your effort.

That it tracks lifetimes means you don’t need either an explicit free operation for stack-based storage or a garbage collector!

I’d also like to point out that I think this can be misleading because it appears you are claiming that Rust’s lifetimes can be used in every situation, and that is not true. We had a long discussion about that recently in both the Lifetimes Parameters thread and the tail end of my Inversion-of-control of resources thread.


#10

You want methods to have some special partial application magic so that passing foo.bump is sugar for creating a closure like |args…| Foo::bump(foo, args…). This doesn’t seem unreasonable to me at first glance, it might be a good idea to add it to Rust. But the problem here is that your mental model of what is going on is not accurate to the semantics of the language.

Quite right, my mental model was broken. This discussion has fixed it, and I’ve corrected the blog to reflect that. AFAIC, the issue is now lack of some kind of curry facility. It may well be that not having a curry facility is the best solution (similar to Python’s lack of a multiline lambda), but that doesn’t mean it’s not something that I’d like to see fixed.

"If it’s not good enough for that task yet, do you want to risk it being good enough for yours?"
I really dislike this sort of discourse. std using unstable features has nothing to do with whether or not the stable features are “safe” for production use.

Absolutely. And it that has nothing to do with why I mentioned that point. “Good enough” in this context doesn’t mean “ready for production”, it means “capable of building your application”. Using anything but the stable channel is politically infeasible for the project I’m evaluating Rust for. But as I said in that post, I continuously ran into issues for which the recommended solution was “just use a different channel”. Failure to mention that would be a disservice to my reader.

The inability to build std is the only one I’ve run into yet I haven’t worked around, so naturally the worst, which is why it gets called out. Maybe I should rewrite that to say that all the instructions for doing cross-compiles to bare metal all start with “Install a nightly channel build, as you’re going to need it to compile the core library”? Well, except for those which have that as the second step, after installing multirust?

I’d also like to point out that I think this can be misleading because it appears you are claiming that Rust’s lifetimes can be used in every situation, and that is not true. We had a long discussion about that recently in both the Lifetimes Parameters thread and the tail end of my Inversion-of-control of resources thread.

I do mention later on that Rust has data structures with reference counting should you ever need such facilities. Which makes it pretty explicit that lifetime tracking is not the solution to every problem.


#11

I personally think a special case currying of the receiver would be a reasonable addition. There’s already a ton of special cases around the receiver (method syntax itself, the syntax of the self argument, the Self type in a trait, etc), and I think many peoples’ mental model is consistent with yours.

The only thing I think is a problem is that currently a type can have a field named foo and a method named foo (in fact its quite common) and I’m not sure how to interpret self.foo in that case. I think, for backward compatibility reason, we would need to assume you meant to perform a field access.


#12

Currying is just syntactic sugar for closures that store some arguments. Your “workaround” || x.bump() is currying with the self argument, just like |x| foo(1, 3, x) or |x, y| bar(x, "baz", y).