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

Tutorial doc #13676

Merged
merged 8 commits into from
May 4, 2014
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/doc/guide-tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ fn print_message() { println!("I am running in a different task!"); }
spawn(print_message);

// Print something more profound in a different task using a lambda expression
// This uses the proc() keyword to assign to spawn a function with no name
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think the term assign is great here since it's a normal parameter being passed.

Copy link
Member

Choose a reason for hiding this comment

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

The language there is poor in a number of respects, sorry I missed that earlier.

Copy link
Member

Choose a reason for hiding this comment

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

(filed as PR #13946.)

// That function will call println!(...) as requested
spawn(proc() println!("I am also running in a different task!") );
~~~~

Expand Down
70 changes: 55 additions & 15 deletions src/doc/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -1717,23 +1717,35 @@ environment (sometimes referred to as "capturing" variables in their
environment). For example, you couldn't write the following:

~~~~ {.ignore}
let foo = 10;
let x = 3;

fn bar() -> int {
return foo; // `bar` cannot refer to `foo`
}
// `fun` cannot refer to `x`
fn fun() -> () { println!("{}", x); }
~~~~

Rust also supports _closures_, functions that can access variables in
the enclosing scope.
A _closure_ does support accessing the enclosing scope; below we will create
2 _closures_ (nameless functions). Compare how `||` replaces `()` and how
they try to access `x`:

~~~~
fn call_closure_with_ten(b: |int|) { b(10); }
~~~~ {.ignore}
let x = 3;

// `fun` is an invalid definition
fn fun () -> () { println!("{}", x) } // cannot capture enclosing scope
let closure = || -> () { println!("{}", x) }; // can capture enclosing scope

// `fun_arg` is an invalid definition
fn fun_arg (arg: int) -> () { println!("{}", arg + x) } // cannot capture enclosing scope
Copy link
Member

Choose a reason for hiding this comment

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

Have you been previewing the results of make doc/tutorial.html with your changes? This line appears to exceed the maximum length for the inline code examples.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

...No, not really. I've been pushing it to GitHub and looking to see what they look like there. It looks fine on GitHub. I'll can test it next time I build it.

let closure_arg = |arg: int| -> () { println!("{}", arg + x) }; // can capture enclosing scope
// ^
// Requires a type because the implementation needs to know which `+` to use.
// In the future, the implementation may not need the help.

let captured_var = 20;
let closure = |arg| println!("captured_var={}, arg={}", captured_var, arg);
fun(); // Still won't work
closure(); // Prints: 3

call_closure_with_ten(closure);
fun_arg(7); // Still won't work
closure_arg(7); // Prints: 10
~~~~

Closures begin with the argument list between vertical bars and are followed by
Expand All @@ -1742,13 +1754,41 @@ considered a single expression: it evaluates to the result of the last
expression it contains if that expression is not followed by a semicolon,
otherwise the block evaluates to `()`.

The types of the arguments are generally omitted, as is the return type,
because the compiler can almost always infer them. In the rare case where the
compiler needs assistance, though, the arguments and return types may be
annotated.
Since a closure is an expression, the compiler can usually infer the argument and
Copy link
Member

Choose a reason for hiding this comment

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

My mental model of how type inference works does not assign such special roles to "expression" vs "declaration" in terms of what is enabling inference here. (For example, if you look at ML, you will see that top-level function definitions (fun, as opposed their expression form fn) are also assigned types via inference, despite not being expression forms.)

There are good reasons we have inference for closures and not for fn items, but this tutorial is not the place to discuss such matters. In my PR for @mdinger I will have a revision to this part of the text to sidestep this problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reasons there is a distinction may not be important here but when asking questions about closures on IRC, I learned a closure is similar in form to a function; yet I found the seemingly arbitrary disregard for function rules very perplexing. Labeling them as coming from different types of forms was very helpful.

Hence I thought it worthwhile to include some explanation to stave off others future confusion.

return types; so they are often omitted. This is in contrast to a function which
is a declaration and _not_ an expression. Declarations require the types to be
specified and carry no inference. Compare:

~~~~ {.ignore}
// `fun` cannot infer the type of `x` so it must be provided because it is a function.
fn fun (x: int) -> () { println!("{}", x) };
Copy link
Member

Choose a reason for hiding this comment

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

The semicolon at the end of this fn item is unnecessary, and including it probably violates some style guideline somewhere. (I'm a little surprised this actually parses.)

(As opposed to semi-colon at the end of the let closure = ...;, which is necessary.)

let closure = |x | -> () { println!("{}", x) };

fun(10); // Prints 10
closure(20); // Prints 20

fun("String"); // Error: wrong type
// Error: This type is different from when `x` was originally evaluated
Copy link
Member

Choose a reason for hiding this comment

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

I think this would be better phrased as "closure has already been assigned the type |int| -> () via inference." (I'm planning on providing @mdinger with a pull request which will show him another phrasing.)

closure("String");
~~~~

The null arguments `()` are typically dropped so the end result
Copy link
Member

Choose a reason for hiding this comment

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

First of all, it would be better to say "The unit type ()" rather than "The null arguments ()". See the manual for further sample text showing how to refer to the unit type (and value) denoted by ().

Second: "typically dropped" is probably poor word choice. The word "drop" has a certain technical meaning in Rust related to destructors, and I'd prefer to avoid using the word "drop" in other contexts if we can avoid it. Maybe "are typically omitted" would be a better choice.

Third: There are two separate details here that I think your presentation is conflating:

  1. When an fn item has unit as its return type (e.g. fn (x: int) -> () { ... }), the return type declaration can be omitted entirely, including the -> arrow: fn (x: int) { ... }.
  2. Return types (be they unit or non-unit) can often inferred on closures. But I believe that is orthogonal to the point above about how unit is treated for fn items.

In your presentation, you mention that () is "typically dropped", but then you only provide an example using a closure, not a fn item. This makes it hard for me as a reader to tell whether you are talking about detail 1 or detail 2 described above.

I would either revise the text to say that the types of closures are often inferred (and say nothing about ()), or, if you insist on including this detail about (), then you should include a new definition of fn fun (x: int) { println!("{}", x) } that shows () being omitted on an fn item.

is more compact.

~~~~
let closure = |x| { println!("{}", x) };

closure(20); // Prints 20
~~~~

Here, in the rare case where the compiler needs assistance,
the arguments and return types may be annotated.

~~~~
let square = |x: int| -> uint { (x * x) as uint };

println!("{}", square(20)); // 400
println!("{}", square(-20)); // 400
~~~~

There are several forms of closure, each with its own role. The most
Expand Down