-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Extension traits #2812
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
base: main
Are you sure you want to change the base?
Extension traits #2812
Changes from 7 commits
be796c3
408962c
1670b64
bd1b26d
06f251e
21a105e
b64886c
c36f7fe
5ab37d8
841bce5
aa2ab0f
63d3aa3
17ba065
d10c985
771a37a
b58a2a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
--- | ||
minutes: 5 | ||
--- | ||
|
||
# Extension Traits | ||
|
||
In Rust, you can't define new inherent methods for foreign types. | ||
|
||
```rust,compile_fail | ||
randomPoison marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 🛠️❌ | ||
impl &'_ str { | ||
pub fn is_palindrome(&self) -> bool { | ||
self.chars().eq(self.chars().rev()) | ||
} | ||
} | ||
``` | ||
|
||
You can use the **extension trait pattern** to work around this limitation. | ||
|
||
<details> | ||
|
||
- Compile the example to show the compiler error that's emitted. | ||
gribozavr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Highlight how the compiler error message nudges you towards the extension | ||
trait pattern. | ||
|
||
- Explain how many type-system restrictions in Rust aim to prevent _ambiguity_. | ||
gribozavr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
If you were allowed to define new inherent methods on foreign types, there | ||
would need to be a mechanism to disambiguate between distinct inherent methods | ||
with the same name. | ||
|
||
In particular, adding a new inherent method to a library type could cause | ||
errors in downstream code if the name of the new method conflicts with an | ||
inherent method that's been defined in the consuming crate. | ||
gribozavr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
</details> |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,80 @@ | ||||||||||||
--- | ||||||||||||
minutes: 15 | ||||||||||||
--- | ||||||||||||
|
||||||||||||
# Extending Foreign Types | ||||||||||||
|
||||||||||||
An **extension trait** is a local trait definition whose primary purpose is to | ||||||||||||
attach new methods to foreign types. | ||||||||||||
|
||||||||||||
```rust | ||||||||||||
mod ext { | ||||||||||||
pub trait StrExt { | ||||||||||||
fn is_palindrome(&self) -> bool; | ||||||||||||
} | ||||||||||||
|
||||||||||||
impl StrExt for &str { | ||||||||||||
fn is_palindrome(&self) -> bool { | ||||||||||||
self.chars().eq(self.chars().rev()) | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
// Bring the extension trait into scope.. | ||||||||||||
pub use ext::StrExt as _; | ||||||||||||
// ..then invoke its methods as if they were inherent methods | ||||||||||||
LukeMathWalker marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||
assert!("dad".is_palindrome()); | ||||||||||||
assert!(!"grandma".is_palindrome()); | ||||||||||||
``` | ||||||||||||
|
||||||||||||
<details> | ||||||||||||
|
||||||||||||
- The `Ext` suffix is conventionally attached to the name of extension traits. | ||||||||||||
|
||||||||||||
It communicates that the trait is primarily used for extension purposes, and | ||||||||||||
it is therefore not intended to be implemented outside the crate that defines | ||||||||||||
it. | ||||||||||||
|
||||||||||||
Refer to the ["Extension Trait" RFC][1] as the authoritative source for naming | ||||||||||||
conventions. | ||||||||||||
|
||||||||||||
- The trait implementation for the chosen foreign type must belong to the same | ||||||||||||
crate where the trait is defined, otherwise you'll be blocked by Rust's | ||||||||||||
[_orphan rule_][2]. | ||||||||||||
Comment on lines
+41
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Consider this more succinct rewrite. |
||||||||||||
|
||||||||||||
- The extension trait must be in scope when its methods are invoked. | ||||||||||||
|
||||||||||||
Comment out the `use` statement in the example to show the compiler error | ||||||||||||
that's emitted if you try to invoke an extension method without having the | ||||||||||||
corresponding extension trait in scope. | ||||||||||||
|
||||||||||||
- The `as _` syntax reduces the likelihood of naming conflicts when multiple | ||||||||||||
|
||||||||||||
traits are imported. It is conventionally used when importing extension | ||||||||||||
traits. | ||||||||||||
|
||||||||||||
- Some students may be wondering: does the extension trait pattern provide | ||||||||||||
|
||||||||||||
enough value to justify the additional boilerplate? Wouldn't a free function | ||||||||||||
be enough? | ||||||||||||
|
||||||||||||
Show how the same example could be implemented using an `is_palindrome` free | ||||||||||||
function, with a single `&str` input parameter: | ||||||||||||
|
||||||||||||
```rust | ||||||||||||
fn is_palindrome(s: &str) -> bool { | ||||||||||||
s.chars().eq(s.chars().rev()) | ||||||||||||
} | ||||||||||||
``` | ||||||||||||
|
||||||||||||
A bespoke extension trait might be an overkill if you want to add a single | ||||||||||||
method to a foreign type. Both a free function and an extension trait will | ||||||||||||
require an additional import, and the familiarity of the method calling syntax | ||||||||||||
may not be enough to justify the boilerplate of a trait definition. | ||||||||||||
|
||||||||||||
Nonetheless, extension methods can be **easier to discover** than free | ||||||||||||
functions. In particular, language servers (e.g. `rust-analyzer`) will suggest | ||||||||||||
extension methods if you type `.` after an instance of the foreign type. | ||||||||||||
|
||||||||||||
</details> | ||||||||||||
|
||||||||||||
[1]: https://rust-lang.github.io/rfcs/0445-extension-trait-rfc.html | ||||||||||||
LukeMathWalker marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||
[2]: https://github.com/rust-lang/rfcs/blob/master/text/2451-re-rebalancing-coherence.md#what-is-coherence-and-why-do-we-care |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,86 @@ | ||||||||||||||||||||||||
--- | ||||||||||||||||||||||||
minutes: 15 | ||||||||||||||||||||||||
--- | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
# Extending Other Traits | ||||||||||||||||||||||||
randomPoison marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
Extension traits can attach new methods to _all_ implementors of a given trait: | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
```rust | ||||||||||||||||||||||||
mod ext { | ||||||||||||||||||||||||
use std::fmt::Display; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
pub trait DisplayExt { | ||||||||||||||||||||||||
fn quoted(&self) -> String; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
impl<T: Display> DisplayExt for T { | ||||||||||||||||||||||||
fn quoted(&self) -> String { | ||||||||||||||||||||||||
format!("'{}'", self) | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
pub use ext::DisplayExt as _; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
assert_eq!("dad".quoted(), "'dad'"); | ||||||||||||||||||||||||
assert_eq!(4.quoted(), "'4'"); | ||||||||||||||||||||||||
assert_eq!(true.quoted(), "'true'"); | ||||||||||||||||||||||||
LukeMathWalker marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
``` | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
<details> | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
- Highlight how we added new behaviour to _multiple_ distinct types at once. | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
US spelling + removing unnecessary "distinct". |
||||||||||||||||||||||||
`.quoted()` can be called on string slices, numbers and booleans since they | ||||||||||||||||||||||||
all implement the `Display` trait. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
This flavour of the extension trait pattern is built on top of | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||
[_blanket implementations_][1]. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Blanket implementations allow us to implement a trait for a generic type `T`, | ||||||||||||||||||||||||
as long as it satisfies the trait bounds specified in the `impl` block. In | ||||||||||||||||||||||||
this case, the only requirement is that `T` implements the `Display` trait. | ||||||||||||||||||||||||
Comment on lines
+41
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Edits for clarity. |
||||||||||||||||||||||||
|
||||||||||||||||||||||||
- Conventionally, the extension trait is named after the trait it extends, | ||||||||||||||||||||||||
following by the `Ext` suffix. In the example above, `DisplayExt`. | ||||||||||||||||||||||||
randomPoison marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
- There are entire libraries aimed at extending foundational traits with new | ||||||||||||||||||||||||
functionality. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
[`itertools`] provides a wide range of iterator adapters and utilities via the | ||||||||||||||||||||||||
[`Itertools`] trait. [`futures`] provides [`FutureExt`] to extend the | ||||||||||||||||||||||||
[`Future`] trait. | ||||||||||||||||||||||||
Comment on lines
+56
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
More structure with a nested list + a bit more content. |
||||||||||||||||||||||||
|
||||||||||||||||||||||||
## More To Explore | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
- Extension traits can be used by libraries to distinguish between stable and | ||||||||||||||||||||||||
experimental methods. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Stable methods are part of the trait definition. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Experimental methods are provided via an extension trait defined in a | ||||||||||||||||||||||||
different library, with a less restrictive stability policy. Some utility | ||||||||||||||||||||||||
methods are then "promoted" to the core trait definition once they have been | ||||||||||||||||||||||||
proven useful and their design has been refined. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
- Extension traits can be used to split a [dyn-incompatible trait][2] in two: | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an excellent use case, but without a code example it is, most likely, impossible to learn from a mere description. The only type of student who would be satisfied with a terse description is someone who is already familiar with the idea. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I placed this under "More to explore" to signal that the instructor can decide whether to mention/explain/dive into this topic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, that's what I'm hinting at :) |
||||||||||||||||||||||||
|
||||||||||||||||||||||||
- A **dyn-compatible core**, restricted to the methods that satisfy | ||||||||||||||||||||||||
dyn-compatibility requirements. | ||||||||||||||||||||||||
- An **extension trait**, containing the remaining methods that are not | ||||||||||||||||||||||||
dyn-compatible. (e.g., methods with a generic parameter). | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The parenthetical continues the sentence. |
||||||||||||||||||||||||
|
||||||||||||||||||||||||
- Concrete types that implement the core trait will be able to invoke all | ||||||||||||||||||||||||
methods, thanks to the blanket impl for the extension trait. Trait objects | ||||||||||||||||||||||||
(`dyn CoreTrait`) will be able to invoke all methods on the core trait as well | ||||||||||||||||||||||||
as those on the extension trait that don't require `Self: Sized`. | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
</details> | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
[1]: https://doc.rust-lang.org/stable/reference/glossary.html#blanket-implementation | ||||||||||||||||||||||||
[`itertools`]: https://docs.rs/itertools/latest/itertools/ | ||||||||||||||||||||||||
[`Itertools`]: https://docs.rs/itertools/latest/itertools/trait.Itertools.html | ||||||||||||||||||||||||
[`futures`]: https://docs.rs/futures/latest/futures/ | ||||||||||||||||||||||||
[`FutureExt`]: https://docs.rs/futures/latest/futures/future/trait.FutureExt.html | ||||||||||||||||||||||||
[`Future`]: https://docs.rs/futures/latest/futures/future/trait.Future.html | ||||||||||||||||||||||||
[2]: https://doc.rust-lang.org/reference/items/traits.html#r-items.traits.dyn-compatible |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,79 @@ | ||||||||||||
--- | ||||||||||||
minutes: 15 | ||||||||||||
--- | ||||||||||||
|
||||||||||||
# Method Resolution Conflicts | ||||||||||||
|
||||||||||||
What happens when you have a name conflict between an inherent method and an | ||||||||||||
extension method? | ||||||||||||
gribozavr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
|
||||||||||||
```rust | ||||||||||||
mod ext { | ||||||||||||
pub trait StrExt { | ||||||||||||
fn trim_ascii(&self) -> &str; | ||||||||||||
} | ||||||||||||
|
||||||||||||
impl StrExt for &str { | ||||||||||||
fn trim_ascii(&self) -> &str { | ||||||||||||
self.trim_start_matches(|c: char| c.is_ascii_whitespace()) | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
pub use ext::StrExt; | ||||||||||||
// Which `trim_ascii` method is invoked? | ||||||||||||
// The one from `StrExt`? Or the inherent one from `str`? | ||||||||||||
assert_eq!(" dad ".trim_ascii(), "dad"); | ||||||||||||
``` | ||||||||||||
|
||||||||||||
<details> | ||||||||||||
|
||||||||||||
- The foreign type may, in a newer version, add a new inherent method with the | ||||||||||||
gribozavr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
same name of our extension method. | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
|
||||||||||||
Survey the class: what do the students think will happen in the example above? | ||||||||||||
Will there be a compiler error? Will one of the two methods be given higher | ||||||||||||
priority? Which one? | ||||||||||||
|
||||||||||||
Add a `panic!("Extension trait")` in the body of `StrExt::trim_ascii` to | ||||||||||||
clarify which method is being invoked. | ||||||||||||
|
||||||||||||
- [Inherent methods have higher priority than trait methods][1], _if_ they have | ||||||||||||
the same name and the **same receiver**, e.g. they both expect `&self` as | ||||||||||||
LukeMathWalker marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||
input. The situation becomes more nuanced if the use a **different receiver**, | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
e.g. `&mut self` vs `&self`. | ||||||||||||
LukeMathWalker marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||
|
||||||||||||
Change the signature of `StrExt::trim_ascii` to | ||||||||||||
`fn trim_ascii(&mut self) -> &str` and modify the invocation accordingly: | ||||||||||||
|
||||||||||||
```rust | ||||||||||||
assert_eq!((&mut " dad ").trim_ascii(), "dad"); | ||||||||||||
``` | ||||||||||||
|
||||||||||||
Now `StrExt::trim_ascii` is invoked, rather than the inherent method, since | ||||||||||||
`&mut self` has a higher priority than `&self`, the one used by the inherent | ||||||||||||
method. | ||||||||||||
Comment on lines
+53
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is an accurate explanation of what's happening here. The reference explicitly states (in the info box in that section) that I think the reason why the I think the confusion here is because we're implementing the trait on on I think things would be a lot less ambiguous if we were demonstrating this on a regular, non-reference type such as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for looking at it closely! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1, I'd be happy if we explained a basic rule that people can remember, and simply mention that while the Rust language has rules to disambiguate in other cases, the rules are quite complex to remember and apply, and we'd rather not write code that depends on them. |
||||||||||||
|
||||||||||||
Point the students to the Rust reference for more information on | ||||||||||||
[method resolution][2]. An explanation with more extensive examples can be | ||||||||||||
found in [an open PR to the Rust reference][3]. | ||||||||||||
Comment on lines
+57
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think we can just link to the reference, I don't think linking to an open PR is necessary. Eventually the things in that PR will (hopefully) land, so just linking to the reference is enough imo. |
||||||||||||
|
||||||||||||
- Avoid naming conflicts between extension trait methods and inherent methods. | ||||||||||||
Rust's method resolution algorithm is complex and may surprise users of your | ||||||||||||
code. | ||||||||||||
|
||||||||||||
## More to explore | ||||||||||||
|
||||||||||||
- The interaction between the priority search used by Rust's method resolution | ||||||||||||
algorithm and automatic `Deref`ering can be used to emulate | ||||||||||||
LukeMathWalker marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||
[specialization][4] on the stable toolchain, primarily in the context of | ||||||||||||
macro-generated code. Check out ["Autoref Specialization"][5] for the specific | ||||||||||||
details. | ||||||||||||
|
||||||||||||
</details> | ||||||||||||
|
||||||||||||
[1]: https://doc.rust-lang.org/stable/reference/expressions/method-call-expr.html#r-expr.method.candidate-search | ||||||||||||
[2]: https://doc.rust-lang.org/stable/reference/expressions/method-call-expr.html | ||||||||||||
[3]: https://github.com/rust-lang/reference/pull/1725 | ||||||||||||
[4]: https://github.com/rust-lang/rust/issues/31844 | ||||||||||||
[5]: https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,62 @@ | ||||||||||
--- | ||||||||||
minutes: 5 | ||||||||||
--- | ||||||||||
|
||||||||||
# Trait Method Conflicts | ||||||||||
|
||||||||||
What happens when you have a name conflict between two different trait methods | ||||||||||
gribozavr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
implemented for the same type? | ||||||||||
|
||||||||||
```rust,compile_fail | ||||||||||
mod ext { | ||||||||||
pub trait Ext1 { | ||||||||||
fn is_palindrome(&self) -> bool; | ||||||||||
} | ||||||||||
|
||||||||||
pub trait Ext2 { | ||||||||||
fn is_palindrome(&self) -> bool; | ||||||||||
} | ||||||||||
|
||||||||||
impl Ext1 for &str { | ||||||||||
fn is_palindrome(&self) -> bool { | ||||||||||
self.chars().eq(self.chars().rev()) | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
impl Ext2 for &str { | ||||||||||
fn is_palindrome(&self) -> bool { | ||||||||||
self.chars().eq(self.chars().rev()) | ||||||||||
} | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
pub use ext::Ext1; | ||||||||||
pub use ext::Ext2; | ||||||||||
|
||||||||||
// Which method is invoked? | ||||||||||
// The one from `Ext1`? Or the one from `Ext2`? | ||||||||||
assert!("dad".is_palindrome()); | ||||||||||
``` | ||||||||||
|
||||||||||
<details> | ||||||||||
|
||||||||||
- The extended trait may, in a newer version, add a new trait method with the | ||||||||||
same name of our extension method. | ||||||||||
|
- The extended trait may, in a newer version, add a new trait method with the | |
same name of our extension method. | |
- Another extension trait may, in a newer version, add a new trait method with the | |
same name as our extension method. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both cases are actually possible, so I reworded the paragraph to account for both in 17ba065
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the approach of surveying the class is something specific to the instructor and situation. I would do this without prompting for small-group, in-person instruction, but for a large or shy group, or when using Zoom or Meet, it's not a very effective strategy (it results in lots of empty air).
So, it's a very minor point and not worth changing, but I think the question in the comment is sufficient here, and does not need repeating in the notes. Instructors will prompt from the comment if appropriate, or address it in a manner appropriate to the context and their teaching style.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
`Ext2::is_palindrome("dad")`. | |
`Ext2::is_palindrome("dad")`. Demonstrate this syntax and that the updated code compiles. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably worth showing the full syntax to students, since sometimes Trait::method(foo)
isn't enough. Specifically, if the compiler can't infer the type of foo
then it won't be able to resolve which type's trait implementation to use. In those cases you'd have to write <Type as Trait>::method(foo)
. That syntax can be surprising for people new to the language (I know I was confused the first time I saw it), so I think showing it here (and explaining when it's necessary) would be good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1, that would be a good thing for the instructor to demonstrate. Please add a bullet point with the prompt for the instructor.
Uh oh!
There was an error while loading. Please reload this page.