Skip to content
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

Objects of type T should be implicitely convertible to &T #137

Closed
wants to merge 4 commits into from
Closed

Objects of type T should be implicitely convertible to &T #137

wants to merge 4 commits into from

Conversation

tomaka
Copy link

@tomaka tomaka commented Jun 25, 2014

@pnkfelix
Copy link
Member

A comment: Note that for your local variable example, while you cannot write this today:

let x: int = 5;
let y: &int = x; 

you can write this today:

let x: int = 5;
let ref y: int = x;

which has the same effect of making y have type &int (and is a borrowed reference to x).

But of course I suspect your driving motivation here is not the local variable case, but is instead the other case: argument passing.


The choice to do auto-borrow and auto-deref magic on the receiver in recv.field and recv.method(..) expressions, but avoid doing so in other contexts such as the other arguments being passed to a method invocation, is an explicit one that the core team has stuck to in the past.

I personally prefer the status quo over this change, but that may be because I consumed the "dot is magic" kool-aid early on.

@pnkfelix
Copy link
Member

There are two other areas of trouble that I am not sure this RFC as written addresses:

  1. What is the impact on type inference?
  2. What is the impact on quality of error messages?

In some cases I might want to make a Vec<int>, and in others I might want to make a Vec<&int>. (And more generally, sometimes I will want Vec<T> and sometimes Vec<&T> for some concrete type T). I think this proposal is going to require type annotations to disambiguate. (And I'm not sure whether it will be easy to guess the appropriate choice to default to, if we go down that path.)

Here is a concrete example of what I am talking about:

extern crate debug;

fn main() {
    let mut v1 = Vec::new();
    let mut v2 = Vec::new();
    let x = 3;   let y = 4;
    v1.push(1);  v1.push(2);
    v2.push(&x); v2.push(&y);
    println!("v1: {:?}, v2: {:?}", v1.as_slice(), v2.as_slice())
}

resulting in the following run transcript:

% rustc /tmp/bar.rs && ./bar
v1: &[1, 2], v2: &[&3, &4]

If I understand the RFC correctly, after this change is put in, I might choose to rewrite the above push invocations as follows:

    v1.push(1);  v1.push(2);
    v2.push(x);  v2.push(y);

and perhaps even this would be accepted:

    v1.push(1);  v1.push(2);
    v2.push(&x); v2.push(y);

and one might think this should work as well:

    v1.push(1);  v1.push(2);
    v2.push(x);  v2.push(&y);

and I think any of these should work and produce the same results, assuming I have at least added appropriate type annotations to the bindings of v1 and v2.

But without those type annotations on v1 and v2, what happens? Are the type annotations required, for disambiguation? Or does the first case just treat v2 as a Vec<int> instead of Vec<&int> ? And would the second case then produce a type check failure? Or would it infer Vec<&int> in the second case? What about the third case?

@tomaka
Copy link
Author

tomaka commented Jun 25, 2014

@pnkfelix: x and y remain integers. If you write v2.push(x); v2.push(y), it will simply create a Vec of integers and move both values into it.
I didn't specify this in the RFC, but it's only when the compiler fails to match the parameters by value that it will try to borrow it. If it is possible to simply pass the value, then the value should be passed instead.

If I'm correct the way type inference works today is that the type is inferred depending on by the order of the function calls.

So in the second example, when you write v2.push(&x) the compiler will find that v2 is a Vec<&int>, which means that the push function now expects an &int. Then v2.push(y) will borrow the y variable because you pass an int where an &int is expected. (more specifically, the compiler will infer a Vec<&any integer type>).

In the third example, the compiler will determine that v2 is a Vec<int> (or more specifically a Vec of generic integers) because the first call to push passes an integer. Then the second push call will fail because you can't convert an &int into an int.

I don't really understand why according to you all three examples should work and produce the same result. The idea of this RFC is to relax a bit the rules, and it's not possible to make everything magically work anyway.
And if you produce the wrong kind of vector, you will get a compilation error sooner or later.

@gsingh93
Copy link

I think this hurts readability. With the explicit &, I know whether I'm passing a reference to a function or moving it into the function. Without it, I have to check the function signature to know that.

@pnkfelix
Copy link
Member

@Tomaka17 wait, I do not understand. Are you saying that you don't think this should work under your RFC ?

extern crate debug;

fn main() {
    let mut v1 : Vec<int> = Vec::new();
    let mut v2 : Vec<&int> = Vec::new();
    let x = 3;   let y = 4;
    v1.push(1);  v1.push(2);
    v2.push(x); v2.push(&y);
    println!("v1: {:?}, v2: {:?}", v1.as_slice(), v2.as_slice())
}

The above is what I meant when I said: "assuming I have at least added appropriate type annotations to the bindings of v1 and v2."

@tomaka
Copy link
Author

tomaka commented Jun 25, 2014

@gsingh93 Right now when you call a.foo() you don't know whether you pass a reference to a or if you move it without checking the signature of foo. When you pass foo(a) you don't know if you move a (if a is a value) or if you pass a reference (if a is a reference).
I feel that the compiler is very strict about this reference/value thing and not strict at all in other situations, so let's be consistent and loosen the restrictions.

@tomaka
Copy link
Author

tomaka commented Jun 25, 2014

@pcwalton Oh, I didn't get this correctly.
Of course it should work if you add the type annotations. Ignore the last paragraph of my previous comment. I was thinking of the case where you didn't have annotations.

@bstrie
Copy link
Contributor

bstrie commented Jun 26, 2014

In the past we have resisted this. The majority of C++ programmers that I've spoken to seem to think that being forced to explicitly reference is a feature (those same programmers have never complained about the autoref behavior of method calls). It may very well be that this asymmetry is valuable, and that seeking fundamental consistency between method calls and function calls is a boondoggle.

However, I'm very sympathetic to the issue of exposing consistent APIs among functions that are commonly used together, such as map and filter on iterators. It's a tough tradeoff.

Ultimately I'd like to see more consideration of how this would interact with our other automatic borrowing rules. We already have autoderef, and we have a near-universal desire for auto-cross-borrowing sometime in the future. For those not aware, auto-cross-borrowing makes stuff like this possible:

fn foo(_: &int) {}
let x = box 2;
foo(x);  // with auto-cross-borrowing
foo(&*x);  // without auto-cross-borrowing

In some ways, auto-cross-borrowing is morally equivalent to autoref, just with a layer of indirection. I dunno. I really want to run this idea by some heavy C++ users.

@dobkeratops
Copy link

Requiring blah(&mut foo) might be nice to make it clear what is written,
would that be strange alongside auto borrowing or not?

really what you need to know at a glance is whats modifiable, its would be nice to just assume an immutable input is going to be either a value or reference depending on whats sensible, so auto borrow would be nice overall.

is it something that could be done experimentally with a feature gate

are there any weird cases like moving a pointer vs passing a reference to a Box that might stop being clear ?

it is the case that rust' pointer safety does away with the need for C++ T* vs T& and hence -> vs . ... feels a bit strange initially but the error messages help you get used to it.

@zwarich
Copy link

zwarich commented Jun 26, 2014

This proposal also has some slightly strange interaction with Copy types. Generally speaking, if I have

let mut x: int = 1;
let a = foo(x);
x = 2;

I would expect that x is being copied, but with this extension it could actually be autoref'd, returned as part of a, and forbid the mutation on the next line.

@ghost
Copy link

ghost commented Jul 2, 2014

Note that this only concerns &T. The &mut syntax should still be mandatory when passing an object by mutable reference, because it explicitly says that the variable will be modified.

I do not understand this reasoning.

@tomaka
Copy link
Author

tomaka commented Jul 2, 2014

@almale :
When writing foo(object), programmers usually don't expect object to be modified.
Instead if you write foo(&mut object) it makes it clear that it will be.

That's not an issue with the language, just a personal preference. I think it should be clear when an object will be modified.
But this can be debated.

@ghost
Copy link

ghost commented Jul 2, 2014

@Tomaka17:

I think it should be clear when an object will be modified.

Of course.
But it is in foo (mut object) too.

This feature will encourage people to add an `&` before their parameter type whenever they only need to read the value instead of consuming it.
For example the `Vec::get` function would have the `fn get(&self, &uint)` definition instead.

Note that this only concerns `&T`. The `&mut` syntax should still be mandatory when passing an object by mutable reference, because it explicitly says that the variable will be modified.
Copy link
Member

Choose a reason for hiding this comment

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

Note that (some) variables passed by & can still be modified. http://is.gd/x2XQMn

use std::cell::Cell;

fn foo(x: &Cell<uint>) { x.set(2) }

fn main() {
    let x = Cell::new(1);
    println!("{}", x); // 1
    foo(&x);
    println!("{}", x); // 2
}

@brson
Copy link
Contributor

brson commented Jul 10, 2014

Closing. There are a number of unsolved technical problems here and no agreement that this is something we want. Thank you.

@brson brson closed this Jul 10, 2014
@bvssvni
Copy link

bvssvni commented Jul 15, 2014

I think this proposal could been modified to only implicitly convert T to &T when the kind is Copy.

In the case where a function takes T: Copy as argument and the variable is used multiple times, it will be copied. &T always implements Copy, so when you pass an immutable reference you could say the reference is "copied". There are use cases where it is tempting to use T: Copy instead of &T to get rid of the &, for example numerical libraries. I was faced with this decision recently and decided to require Copy in favor of nicer syntax. The same applies to strings:

For example, when we take &T by argument and the kind is Copy, the & is required:

fn foo<T: std::fmt::Show>(a: &T) {
    println!("{}", a);
}

fn main() {
    let x = "hello!";
    // '&' is required
    foo(&x);
    foo(&x);
}

When we take T: Copy, the semantics is similar, but the & is optional.

fn foo<T: std::fmt::Show + Copy>(a: T) {
    println!("{}", a);
}

fn main() {
    let x = "hello!";
    // '&' is optional
    foo(x);
    foo(&x);
}

@bvssvni
Copy link

bvssvni commented Jul 15, 2014

The problem @zwarich points out can be solved by only making & optional for arguments without an anonymous lifetime and if the kind is Copy.

let mut x: int = 1;
let a = foo(&x); // '&' is required when the argument has a lifetime
let a = bar(x); // '&' is optional when the argument has a not a lifetime
x = 2;

@bvssvni
Copy link

bvssvni commented Jul 15, 2014

This rule also solves the problem @pnkfelix pointed out with inferring the type:

extern crate debug;

fn main() {
    let mut v1 = Vec::new();
    let mut v2 = Vec::new();
    let x = 3;   let y = 4;
    v1.push(1);  v1.push(2);
    // This would not be allowed since it the argument is not anonymous lifetime
    v2.push(&x); v2.push(y);
    println!("v1: {:?}, v2: {:?}", v1.as_slice(), v2.as_slice())
}

withoutboats pushed a commit to withoutboats/rfcs that referenced this pull request Jan 15, 2017
Registering unpark is the responsibility of the callee
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.

None yet

9 participants