Skip to content

Add Builder guidelines #1036

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

Closed
wants to merge 1 commit into from
Closed
Changes from all 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
136 changes: 136 additions & 0 deletions text/0000-builder-naming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
- Start Date: 2015-04-06
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)

# Summary

Provide guidelines for builder types in the standard library.

# Motivation

We use builder pattern in a few places in the standard library, but they are not consistent. We
have `thread::Builder`, `process::Command` and there’s `fs::OpenOptions`. They are somewhat ad-hoc
and inconsistency makes forming the intuition about them hard.

A common example is people not knowing about `OpenOptions` builder and being confused by its name
and lack of discoverability.

# Detailed design

## Guidelines

### Naming

These guidelines suggest a few naming schemes, depending on situation at hand:

* If the module, which exports the builder, is expected to only export a single builder, even in
the future, and the module itself has a descriptive name, calling the builder “Builder” is
recommended. For example:

* `thread::Builder` builds and spawns threads;
* `process::Builder` builds and spawns processes;

but *not* `fs::Builder`, which opens files;
* If the builder produces one type of objects, use the name of buildee suffixed with “Builder”.
This scheme has an advantage of being searchable and self-documenting. For example:
`FileBuilder` is a builder and produces objects of type `File`;
* If there is no clear buildee (most likely because it contains multiple finalizers), a noun
describing all the buildable objects suffixed with “Builder” may be used. For example:
`ThreadBuilder`, even though it produces `JoinHandle`s and `JoinGuard`s rather than `Thread`s;
* Finally, if the majority (community) decides there exists a superior name for the builder, but it
does not match any of the guidelines above, generously point out the existence of the builder
in the documentation of buildee and module, if appropriate.

### Methods

Option setters – methods which represent options that can be set on the builder – should receive
the builder object as `&mut self` rather than `self` unless there is a good reason to do otherwise.

Finalizers – methods which produce the built object – should receive self via reference, unless
there is a good reason to do otherwise.

In order for method listing in documentation to be predictable, methods in the builder’s `impl`
block should be declared following this order:

* `new` – a method to create the builder object comes first in the `impl` block;
* followed by one or more option setters;
* followed by one or more finalizers.

This ordering stems from conventional use of the builder pattern: create a builder, set the
options and call a finalizer.

#### Method naming

Option setters should be nouns or adjectives. Avoid `is_*` prefix and nouns derived from
adjectives. For example:

* `name`;
* `stack_size`;
* `read` or `readable` (rather than `is_readable` or `readability`);
* `write` or `writable` (rather than `is_writable` or `writability`).

Finalizers should be verbs. For example:

* `spawn`;
* `create`;
* `open`.

## Proposed changes to the standard library

### `thread::Builder`

`thread::Builder` is adjusted to have the following interface:

```rust
pub struct Builder { … }
impl Builder {
fn new() → Builder;
fn name(&mut self, name: String) → &mut Builder;
fn stack_size(&mut self, size: usize) → &mut Builder;
fn spawn<F>(&mut self, f: F) → Result<JoinHandle> where F: FnOnce(), F: Send + 'static;
fn scoped<'a, T, F>(&mut self, f: F) → Result<JoinGuard<'a, T>>
where T: Send + 'a, F: FnOnce() → T, F: Send + 'a
}
```

and `impl Clone for Builder` is provided.

In summary, all methods are changed to take and return `Builder` by mutable reference rather than
Copy link
Member

Choose a reason for hiding this comment

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

Currently this is done because the name is an owned value (e.g. scoped/spawn consumes it), but I do think that this is largely a holdover from the past where Box<Writer> or Box<FnOnce> was contained within and the by-value-ness was needed.

Copy link
Member Author

Choose a reason for hiding this comment

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

Currently this is done because the name is an owned value

Yes, this is the primary reason why finalizers taking self by value would be great, if they didn’t introduce usability issues, as you pointed out in the previous revision.

by value. Strictly speaking this is a breaking change, but most users of the `thread::Builder`
should not encounter any breakage.

### `process::Command`

`impl Clone for Command` is provided.
Copy link
Member

Choose a reason for hiding this comment

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

In the past I've been hesitant to do this as it may prevent us from adding more options to Command in the future. For example if a standard I/O handle is redirected to a file, it's not clear how Clone can be implemented.

We do have various routes to mitigate this, but this may fall under the category of "would like clone, but implementation may not make it possible".


### `fs::OpenOptions`

`OpenOptions` is renamed to `FileBuilder`. `OpenOptions` shall stay as a deprecated alias for
`FileBuilder` for a period.

# Drawbacks

Breaking change after 1.0-beta.
Copy link
Member

Choose a reason for hiding this comment

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

At this point it's pretty unlikely that we're going to accept this as a breaking change, but we're definitely open for a deprecation strategy. Would you be ok re-wording the relevant bits of this RFC to recommend a deprecation strategy instead of a breaking change?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure!


# Alternatives

* Only apply these guidelines to builders introduced after 1.0-beta;
* Construct builders via a method implemented on the buildees. The API looks like this:

let file = File::builder()
.write(true)
.truncate(false)
.open(path);

This allows users of the API never reference the builder type. On the other hand, it is only
suitable for a subset of builders. For example it doesn’t work for `thread::Builder`, because
the finalizers return guards rather than an instance of `Thread`;

Proposed by [@sfackler][sfackler].
* Rename `process::Command` to `process::Builder`.

[sfackler]: https://github.com/rust-lang/rfcs/pull/1044#discussion_r28082235

Copy link
Member

Choose a reason for hiding this comment

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

Could you add @sfackler's alternative here as well? I personally wouldn't advocate for it, but it seems like a legitimate alternative!

# Unresolved questions

None known to the author.