Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upLib Blitz Style Summary and Overhaul #950
Comments
kbknapp
added this to the Lib Blitz Overhaul milestone
May 9, 2017
kbknapp
self-assigned this
May 9, 2017
kbknapp
added
the
C: meta
label
May 9, 2017
kbknapp
added a commit
that referenced
this issue
Jan 31, 2018
kbknapp
modified the milestones:
Lib Blitz Overhaul,
v3-beta.1
Jul 22, 2018
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
kbknapp commentedMay 9, 2017
•
edited
In an effort to get 3.x underway this will be the summary issue linking to all tracking issues. Many of the issues that will come out of this will be excellent "first time" issues that I'd be willing to mentor!
I will be updating this summary periodically (in addition to working on all the other issues in the queue) as well as linking to tracking issues for individual tracking issues as I create them.
From Rust API Guidelines
Rust API guidelines
Crate conformance checklist
as_,to_,into_conventions (C-CONV)iter,iter_mut,into_iter(C-ITER)_mutand_ref(C-OWN-SUFFIX)Copy,Clone,Eq,PartialEq,Ord,PartialOrd,HashDebug,Display,DefaultFrom,AsRef,AsMut(C-CONV-TRAITS)FromIteratorandExtend(C-COLLECT)Serialize,Deserialize(C-SERDE)"serde"cfg option that enables Serde (C-SERDE-CFG)SendandSyncwhere possible (C-SEND-SYNC)SendandSync(C-SEND-SYNC-ERR)()(C-MEANINGFUL-ERR)Hex,Octal,Binaryformatting (C-NUM-FMT)?, nottry!, notunwrap(C-QUESTION-MARK)readme, keywords, categories
DerefandDerefMut(C-DEREF)DerefandDerefMutnever fail (C-DEREF-FAIL)boolorOption(C-CUSTOM-TYPE)bitflags, not enums (C-BITFLAG)Debug(C-DEBUG)Debugrepresentation is never empty (C-DEBUG-NONEMPTY)Organization
Crate root re-exports common functionality (C-REEXPORT)
Crates
pub usethe most common types for convenience, so that clients do nothave to remember or write the crate's module hierarchy to use these types.
Re-exporting is covered in more detail in the The Rust Programming Language
under Crates and Modules.
Examples from
serde_jsonThe
serde_json::Valuetype is the most commonly used type fromserde_json.It is a re-export of a type that lives elsewhere in the module hierarchy, at
serde_json::value::Value. Theserde_json::valuemodule definesother JSON-value-related things that are not re-exported. For example
serde_json::value::Indexis the trait that defines types that can be used toindex into a
Valueusing square bracket indexing notation. TheIndextraitis not re-exported at the crate root because it would be comparatively rare for
a client crate to need to refer to it.
In addition to types, functions can be re-exported as well. In
serde_jsontheserde_json::from_strfunction is a re-export of a function from theserde_json::dedeserialization module, which contains other less commondeserialization-related functionality that is not re-exported.
Modules provide a sensible API hierarchy (C-HIERARCHY)
Examples from Serde
The
serdecrate is two independent frameworks in one crate - a serializationhalf and a deserialization half. The crate is divided accordingly into
serde::serandserde::de. Part of the deserialization framework isisolated under
serde::de::valuebecause it is a relatively large API surfacethat is relatively unimportant, and it would crowd the more common, more
important functionlity located in
serde::deif it were to share the samenamespace.
Naming
Casing conforms to RFC 430 (C-CASE)
Basic Rust naming conventions are described in RFC 430.
In general, Rust tends to use
CamelCasefor "type-level" constructs (types andtraits) and
snake_casefor "value-level" constructs. More precisely:snake_caseCamelCaseCamelCaseCamelCasesnake_casesnake_caseneworwith_more_detailsfrom_some_other_typesnake_caseSCREAMING_SNAKE_CASESCREAMING_SNAKE_CASECamelCase, usually single uppercase letter:Tlowercase, usually a single letter:'a,'de,'srcIn
CamelCase, acronyms count as one word: useUuidrather thanUUID. Insnake_case, acronyms are lower-cased:is_xid_start.In
snake_caseorSCREAMING_SNAKE_CASE, a "word" should never consist of asingle letter unless it is the last "word". So, we have
btree_maprather thanb_tree_map, butPI_2rather thanPI2.Examples from the standard library
The whole standard library. This guideline should be easy!
Ad-hoc conversions follow
as_,to_,into_conventions (C-CONV)Conversions should be provided as methods, with names prefixed as follows:
as_to_into_For example:
str::as_bytes()gives a&[u8]view into a&str, which is free.str::to_owned()copies a&strto a newString, which may require memoryallocation.
String::into_bytes()takes ownership aStringand yields the underlyingVec<u8>, which is free.BufReader::into_inner()takes ownership of a buffered reader and extractsout the underlying reader, which is free. Data in the buffer is
discarded.
BufWriter::into_inner()takes ownership of a buffered writer and extractsout the underlying writer, which requires a potentially expensive flush of any
buffered data.
Conversions prefixed
as_andinto_typically decrease abstraction, eitherexposing a view into the underlying representation (
as) or deconstructing datainto its underlying representation (
into). Conversions prefixedto_, on theother hand, typically stay at the same level of abstraction but do some work to
change one representation into another.
More examples from the standard library
Result::as_refRefCell::as_ptrPath::to_strslice::to_vecOption::into_iterAtomicBool::into_innerMethods on collections that produce iterators follow
iter,iter_mut,into_iter(C-ITER)Per RFC 199.
For a container with elements of type
U, iterator methods should be named:This guideline applies to data structures that are conceptually homogeneous
collections. As a counterexample, the
strtype is slice of bytes that areguaranteed to be valid UTF-8. This is conceptually more nuanced than a
homogeneous collection so rather than providing the
iter/iter_mut/into_itergroup of iterator methods, it providesstr::bytesto iterate as bytes andstr::charsto iterate as chars.This guideline applies to methods only, not functions. For example
percent_encodefrom theurlcrate returns an iterator over percent-encodedstring fragments. There would be no clarity to be had by using an
iter/iter_mut/into_iterconvention.Examples from the standard library
Vec::iterVec::iter_mutVec::into_iterBTreeMap::iterBTreeMap::iter_mutIterator type names match the methods that produce them (C-ITER-TY)
A method called
into_iter()should return a type calledIntoIterandsimilarly for all other methods that return iterators.
This guideline applies chiefly to methods, but often makes sense for functions
as well. For example the
percent_encodefunction from theurlcratereturns an iterator type called
PercentEncode.These type names make the most sense when prefixed with their owning module, for
example
vec::IntoIter.Examples from the standard library
Vec::iterreturnsIterVec::iter_mutreturnsIterMutVec::into_iterreturnsIntoIterBTreeMap::keysreturnsKeysBTreeMap::valuesreturnsValuesOwnership suffixes use
_mut,_ref(C-OWN-SUFFIX)Functions often come in multiple variants: immutably borrowed, mutably borrowed,
and owned.
The right default depends on the function in question. Variants should be marked
through suffixes.
Exceptions
In the case of iterators, the moving variant can also be understood as an
intoconversion,
into_iter, andfor x in v.into_iter()reads arguably better thanfor x in v.iter_move(), so the convention isinto_iter.For mutably borrowed variants, if the
mutqualifier is part of a type name,it should appear as it would appear in the type. For example
Vec::as_mut_slicereturns a mut slice; it does what it says.Immutably borrowed by default
If
foouses/produces an immutable borrow by default, use:_mutsuffix (e.g.foo_mut) for the mutably borrowed variant._movesuffix (e.g.foo_move) for the owned variant.Examples from the standard library
TODO rust-api-guidelines#37
Owned by default
If
foouses/produces owned data by default, use:_refsuffix (e.g.foo_ref) for the immutably borrowed variant._mutsuffix (e.g.foo_mut) for the mutably borrowed variant.Examples from the standard library
std::io::BufReader::get_refstd::io::BufReader::get_mutSingle-element containers implement appropriate getters (C-GETTERS)
Single-element contains where accessing the element cannot fail should implement
getandget_mut, with the following signatures.Single-element containers where the element is
Copy(e.g.Cell-likecontainers) should instead return the value directly, and not implement a
mutable accessor. TODO rust-api-guidelines#44
For getters that do runtime validation, consider adding unsafe
_uncheckedvariants.
Examples from the standard library
std::io::Cursor::get_mutstd::ptr::Unique::get_mutstd::sync::PoisonError::get_mutstd::sync::atomic::AtomicBool::get_mutstd::collections::hash_map::OccupiedEntry::get_mut<[_]>::get_uncheckedInteroperability
Types eagerly implement common traits (C-COMMON-TRAITS)
Rust's trait system does not allow orphans: roughly, every
implmust liveeither in the crate that defines the trait or the implementing type.
Consequently, crates that define new types should eagerly implement all
applicable, common traits.
To see why, consider the following situation:
stddefines traitDisplay.urldefines typeUrl, without implementingDisplay.webappimports from bothstdandurl,There is no way for
webappto addDisplaytourl, since it definesneither. (Note: the newtype pattern can provide an efficient, but inconvenient
workaround.
The most important common traits to implement from
stdare:CopyCloneEqPartialEqOrdPartialOrdHashDebugDisplayDefaultConversions use the standard traits
From,AsRef,AsMut(C-CONV-TRAITS)The following conversion traits should be implemented where it makes sense:
FromTryFromAsRefAsMutThe following conversion traits should never be implemented:
IntoTryIntoThese traits have a blanket impl based on
FromandTryFrom. Implement thoseinstead.
Examples from the standard library
From<u16>is implemented foru32because a smaller integer can always beconverted to a bigger integer.
From<u32>is not implemented foru16because the conversion may not bepossible if the integer is too big.
TryFrom<u32>is implemented foru16and returns an error if the integer istoo big to fit in
u16.From<Ipv6Addr>is implemented forIpAddr, which is a type that canrepresent both v4 and v6 IP addresses.
Collections implement
FromIteratorandExtend(C-COLLECT)FromIteratorandExtendenable collections to be used conveniently withthe following iterator methods:
Iterator::collectIterator::partitionIterator::unzipFromIteratoris for creating a new collection containing items from aniterator, and
Extendis for adding items from an iterator onto an existingcollection.
Examples from the standard library
Vec<T>implements bothFromIterator<T>andExtend<T>.Data structures implement Serde's
Serialize,Deserialize(C-SERDE)Types that play the role of a data structure should implement
SerializeandDeserialize.An example of a type that plays the role of a data structure is
linked_hash_map::LinkedHashMap.An example of a type that does not play the role of a data structure is
byteorder::LittleEndian.Crate has a
"serde"cfg option that enables Serde (C-SERDE-CFG)If the crate relies on
serde_deriveto provide Serde impls, the name of thecfg can still be simply
"serde"by using this workaround. Do not use adifferent name for the cfg like
"serde_impls"or"serde_serialization".Types are
SendandSyncwhere possible (C-SEND-SYNC)SendandSyncare automatically implemented when the compiler determinesit is appropriate.
In types that manipulate raw pointers, be vigilant that the
SendandSyncstatus of your type accurately reflects its thread safety characteristics. Tests
like the following can help catch unintentional regressions in whether the type
implements
SendorSync.Error types are
SendandSync(C-SEND-SYNC-ERR)An error that is not
Sendcannot be returned by a thread run withthread::spawn. An error that is notSynccannot be passed across threadsusing an
Arc. These are common requirements for basic error handling in amultithreaded application.
Binary number types provide
Hex,Octal,Binaryformatting (C-NUM-FMT)std::fmt::UpperHexstd::fmt::LowerHexstd::fmt::Octalstd::fmt::BinaryThese traits control the representation of a type under the
{:X},{:x},{:o}, and{:b}format specifiers.Implement these traits for any number type on which you would consider doing
bitwise manipulations like
|or&. This is especially appropriate forbitflag types. Numeric quantity types like
struct Nanoseconds(u64)probably donot need these.
Error types are meaningful, not
()(C-MEANINGFUL-ERR)When defining functions that return
Result, and the error carries nouseful additional information, do not use
()as the error type.()does not implement
std::error::Error, and this causes problems forcallers that expect to be able to convert errors to
Error. Commonerror handling libraries like error-chain expect errors to implement
Error.Instead, define a meaningful error type specific to your crate.
Examples from the standard library
ParseBoolErroris returned when failing to parse a bool from a string.Macros
Input syntax is evocative of the output (C-EVOCATIVE)
Rust macros let you dream up practically whatever input syntax you want. Aim to
keep input syntax familiar and cohesive with the rest of your users' code by
mirroring existing Rust syntax where possible. Pay attention to the choice and
placement of keywords and punctuation.
A good guide is to use syntax, especially keywords and punctuation, that is
similar to what will be produced in the output of the macro.
For example if your macro declares a struct with a particular name given in the
input, preface the name with the keyword
structto signal to readers that astruct is being declared with the given name.
Another example is semicolons vs commas. Constants in Rust are followed by
semicolons so if your macro declares a chain of constants, they should likely be
followed by semicolons even if the syntax is otherwise slightly different from
Rust's.
Macros are so diverse that these specific examples won't be relevant, but think
about how to apply the same principles to your situation.
Item macros compose well with attributes (C-MACRO-ATTR)
Macros that produce more than one output item should support adding attributes
to any one of those items. One common use case would be putting individual items
behind a cfg.
Macros that produce a struct or enum as output should support attributes so that
the output can be used with derive.
Item macros work anywhere that items are allowed (C-ANYWHERE)
Rust allows items to be placed at the module level or within a tighter scope
like a function. Item macros should work equally well as ordinary items in all
of these places. The test suite should include invocations of the macro in at
least the module scope and function scope.
As a simple example of how things can go wrong, this macro works great in a
module scope but fails in a function scope.
Item macros support visibility specifiers (C-MACRO-VIS)
Follow Rust syntax for visibility of items produced by a macro. Private by
default, public if
pubis specified.Type fragments are flexible (C-MACRO-TY)
If your macro accepts a type fragment like
$t:tyin the input, it should beusable with all of the following:
u8,&strm::Data::base::Datasuper::DataVec<String>As a simple example of how things can go wrong, this macro works great with
primitives and absolute paths but fails with relative paths.
Documentation
Crate level docs are thorough and include examples (C-CRATE-DOC)
See RFC 1687.
All items have a rustdoc example (C-EXAMPLE)
Every public module, trait, struct, enum, function, method, macro, and type
definition should have an example that exercises the functionality.
The purpose of an example is not always to show how to use the item. For
example users can be expected to know how to instantiate and match on an enum
like
enum E { A, B }. Rather, an example is often intended to show whysomeone would want to use the item.
This guideline should be applied within reason.
A link to an applicable example on another item may be sufficient. For example
if exactly one function uses a particular type, it may be appropriate to write a
single example on either the function or the type and link to it from the other.
Examples use
?, nottry!, notunwrap(C-QUESTION-MARK)Like it or not, example code is often copied verbatim by users. Unwrapping an
error should be a conscious decision that the user needs to make.
A common way of structuring fallible example code is the following. The lines
beginning with
#are compiled bycargo testwhen building the example butwill not appear in user-visible rustdoc.
Function docs include error conditions in "Errors" section (C-ERROR-DOC)
Per RFC 1574.
This applies to trait methods as well. Trait methods for which the
implementation is allowed or expected to return an error should be documented
with an "Errors" section.
Examples from the standard library
Some implementations of the
std::io::Read::readtrait method may return anerror.
Function docs include panic conditions in "Panics" section (C-PANIC-DOC)
Per RFC 1574.
This applies to trait methods as well. Traits methods for which the
implementation is allowed or expected to panic should be documented with a
"Panics" section.
Examples from the standard library
The
Vec::insertmethod may panic.Prose contains hyperlinks to relevant things (C-LINK)
Links to methods within the same type usually look like this:
[`serialize_struct`]: #method.serialize_structLinks to other types usually look like this:
Links may also point to a parent or child module:
This guideline is officially recommended by RFC 1574 under the heading "Link
all the things".
Cargo.toml publishes CI badges for tier 1 platforms (C-CI)
The Rust compiler regards tier 1 platforms as "guaranteed to work."
Specifically they will each satisfy the following requirements:
tests passing.
Stable, high-profile crates should meet the same level of rigor when it comes to
tier 1. To prove it, Cargo.toml should publish CI badges.
Cargo.toml includes all common metadata (C-METADATA)
authorsdescriptionlicensehomepage(though see rust-api-guidelines#26)documentationrepositoryreadmekeywordscategoriesCrate sets html_root_url attribute (C-HTML-ROOT)
It should point to
"https://docs.rs/$crate/$version".Cargo.toml should contain a note next to the version to remember to bump the
html_root_urlwhen bumping the crate version.Cargo.toml documentation key points to docs.rs (C-DOCS-RS)
It should point to
"https://docs.rs/$crate".Predictability
Smart pointers do not add inherent methods (C-SMART-PTR)
For example, this is why the
Box::into_rawfunction is defined the way itis.
If this were defined as an inherent method instead, it would be confusing at the
call site whether the method being called is a method on
Box<T>or a method onT.Conversions live on the most specific type involved (C-CONV-SPECIFIC)
When in doubt, prefer
to_/as_/into_tofrom_, because they are moreergonomic to use (and can be chained with other methods).
For many conversions between two types, one of the types is clearly more
"specific": it provides some additional invariant or interpretation that is not
present in the other type. For example,
stris more specific than&[u8],since it is a UTF-8 encoded sequence of bytes.
Conversions should live with the more specific of the involved types. Thus,
strprovides both theas_bytesmethod and thefrom_utf8constructorfor converting to and from
&[u8]values. Besides being intuitive, thisconvention avoids polluting concrete types like
&[u8]with endless conversionmethods.
Functions with a clear receiver are methods (C-METHOD)
Prefer
over
for any operation that is clearly associated with a particular type.
Methods have numerous advantages over functions:
value of the appropriate type.
T" (especially when using rustdoc).selfnotation, which is more concise and often more clearlyconveys ownership distinctions.
Functions do not take out-parameters (C-NO-OUT)
Prefer
over
for returning multiple
Barvalues.Compound return types like tuples and structs are efficiently compiled and do
not require heap allocation. If a function needs to return multiple values, it
should do so via one of these types.
The primary exception: sometimes a function is meant to modify data that the
caller already owns, for example to re-use a buffer:
Operator overloads are unsurprising (C-OVERLOAD)
Operators with built in syntax (
*,|, and so on) can be provided for a typeby implementing the traits in
std::ops. These operators come with strongexpectations: implement
Mulonly for an operation that bears some resemblanceto multiplication (and shares the expected properties, e.g. associativity), and
so on for the other traits.
Only smart pointers implement
DerefandDerefMut(C-DEREF)The
Dereftraits are used implicitly by the compiler in many circumstances,and interact with method resolution. The relevant rules are designed
specifically to accommodate smart pointers, and so the traits should be used
only for that purpose.
Examples from the standard library
Box<T>Stringis a smartpointer to
strRc<T>Arc<T>Cow<'a, T>DerefandDerefMutnever fail (C-DEREF-FAIL)Because the
Dereftraits are invoked implicitly by the compiler in sometimessubtle ways, failure during dereferencing can be extremely confusing.
Constructors are static, inherent methods (C-CTOR)
In Rust, "constructors" are just a convention:
Constructors are static (no
self) inherent methods for the type that theyconstruct. Combined with the practice of fully importing type names, this
convention leads to informative but concise construction:
This convention also applied to conversion constructors (prefix
fromratherthan
new).Constructors for structs with sensible defaults allow clients to concisely
override using the struct update syntax.
Examples from the standard library
std::io::Error::newis the commonly used constructor for an IO error.std::io::Error::from_raw_os_erroris a constructor based on an error codereceived from the operating system.
Flexibility
Functions expose intermediate results to avoid duplicate work (C-INTERMEDIATE)
Many functions that answer a question also compute interesting related data. If
this data is potentially of interest to the client, consider exposing it in the
API.
Examples from the standard library
Vec::binary_searchdoes not return aboolof whether the value wasfound, nor an
Option<usize>of the index at which the value was maybe found.Instead it returns information about the index if found, and also the index at
which the value would need to be inserted if not found.
String::from_utf8may fail if the input bytes are not UTF-8. In the errorcase it returns an intermediate result that exposes the byte offset up to
which the input was valid UTF-8, as well as handing back ownership of the
input bytes.
Caller decides where to copy and place data (C-CALLER-CONTROL)
If a function requires ownership of an argument, it should take ownership of the
argument rather than borrowing and cloning the argument.
If a function does not require ownership of an argument, it should take a
shared or exclusive borrow of the argument rather than taking ownership and
dropping the argument.
The
Copytrait should only be used as a bound when absolutely needed, not as away of signaling that copies should be cheap to make.
Functions minimize assumptions about parameters by using generics (C-GENERIC)
The fewer assumptions a function makes about its inputs, the more widely usable
it becomes.
Prefer
over any of
if the function only needs to iterate over the data.
More generally, consider using generics to pinpoint the assumptions a function
needs to make about its arguments.
Advantages of generics
Reusability. Generic functions can be applied to an open-ended collection of
types, while giving a clear contract for the functionality those types must
provide.
Static dispatch and optimization. Each use of a generic function is
specialized ("monomorphized") to the particular types implementing the trait
bounds, which means that (1) invocations of trait methods are static, direct
calls to the implementation and (2) the compiler can inline and otherwise
optimize these calls.
Inline layout. If a
structandenumtype is generic over some typeparameter
T, values of typeTwill be laid out inline in thestruct/enum, without any indirection.Inference. Since the type parameters to generic functions can usually be
inferred, generic functions can help cut down on verbosity in code where
explicit conversions or other method calls would usually be necessary.
Precise types. Because generic give a name to the specific type
implementing a trait, it is possible to be precise about places where that
exact type is required or produced. For example, a function
is guaranteed to consume and produce elements of exactly the same type
T; itcannot be invoked with parameters of different types that both implement
Trait.Disadvantages of generics
Code size. Specializing generic functions means that the function body is
duplicated. The increase in code size must be weighed against the performance
benefits of static dispatch.
Homogeneous types. This is the other side of the "precise types" coin: if
Tis a type parameter, it stands for a single actual type. So for examplea
Vec<T>contains elements of a single concrete type (and, indeed, thevector representation is specialized to lay these out in line). Sometimes
heterogeneous collections are useful; see trait objects.
Signature verbosity. Heavy use of generics can make it more difficult to
read and understand a function's signature.
Examples from the standard library
std::fs::File::opentakes an argument of generic typeAsRef<Path>. Thisallows files to be opened conveniently from a string literal
"f.txt", aPath, anOsString, and a few other types.Traits are object-safe if they may be useful as a trait object (C-OBJECT)
Trait objects have some significant limitations: methods invoked through a trait
object cannot use generics, and cannot use
Selfexcept in receiver position.When designing a trait, decide early on whether the trait will be used as an
object or as a bound on generics.
If a trait is meant to be used as an object, its methods should take and return
trait objects rather than use generics.
A
whereclause ofSelf: Sizedmay be used to exclude specific methods fromthe trait's object. The following trait is not object-safe due to the generic
method.
Adding a requirement of
Self: Sizedto the generic method excludes it from thetrait object and makes the trait object-safe.
Advantages of trait objects
(monomorphized) versions of code, which can greatly reduce code size.
Disadvantages of trait objects
indirection and vtable dispatch, which can carry a performance penalty.
cannot use the
Selftype.Examples from the standard library
io::Readandio::Writetraits are often used as objects.Iteratortrait has several generic methods marked withwhere Self: Sizedto retain the ability to useIteratoras an object.Type safety
Newtypes provide static distinctions (C-NEWTYPE)
Newtypes can statically distinguish between different interpretations of an
underlying type.
For example, a
f64value might be used to represent a quantity in miles or inkilometers. Using newtypes, we can keep track of the intended interpretation:
Once we have separated these two types, we can statically ensure that we do not
confuse them. For example, the function
cannot accidentally be called with a
Kilometersvalue. The compiler willremind us to perform the conversion, thus averting certain catastrophic bugs.
Arguments convey meaning through types, not
boolorOption(C-CUSTOM-TYPE)Prefer
over
Core types like
bool,u8andOptionhave many possible interpretations.Use custom types (whether
enums,struct, or tuples) to convey interpretationand invariants. In the above example, it is not immediately clear what
trueand
falseare conveying without looking up the argument names, butSmallandRoundare more suggestive.Using custom types makes it easier to expand the options later on, for example
by adding an
ExtraLargevariant.See the newtype pattern for a no-cost way to wrap existing types
with a distinguished name.
Types for a set of flags are
bitflags, not enums (C-BITFLAG)Rust supports
enumtypes with explicitly specified discriminants:Custom discriminants are useful when an
enumtype needs to be serialized to aninteger value compatibly with some other system/language. They support
"typesafe" APIs: by taking a
Color, rather than an integer, a function isguaranteed to get well-formed inputs, even if it later views those inputs as
integers.
An
enumallows an API to request exactly one choice from among many. Sometimesan API's input is instead the presence or absence of a set of flags. In C code,
this is often done by having each flag correspond to a particular bit, allowing
a single integer to represent, say, 32 or 64 flags. Rust's
bitflagscrateprovides a typesafe representation of this pattern.
Builders enable construction of complex values (C-BUILDER)
Some data structures are complicated to construct, due to their construction
needing:
which can easily lead to a large number of distinct constructors with many
arguments each.
If
Tis such a data structure, consider introducing aTbuilder:TBuilderfor incrementally configuring aTvalue. When possible, choose a better name: e.g.
Commandis the builderfor a child process,
Urlcan be created from aParseOptions.make a
T.including setting up compound inputs (like slices) incrementally. These
methods should return
selfto allow chaining.building a
T.The builder pattern is especially appropriate when building a
Tinvolves sideeffects, such as spawning a task or launching a process.
In Rust, there are two variants of the builder pattern, differing in the
treatment of ownership, as described below.
Non-consuming builders (preferred):
In some cases, constructing the final
Tdoes not require the builder itself tobe consumed. The follow variant on
std::process::Commandis one example:Note that the
spawnmethod, which actually uses the builder configuration tospawn a process, takes the builder by immutable reference. This is possible
because spawning the process does not require ownership of the configuration
data.
Because the terminal
spawnmethod only needs a reference, the configurationmethods take and return a mutable borrow of
self.The benefit
By using borrows throughout,
Commandcan be used conveniently for bothone-liner and more complex constructions:
Consuming builders:
Sometimes builders must transfer ownership when constructing the final type
T,meaning that the terminal methods must take
selfrather than&self.Here, the
stdoutconfiguration involves passing ownership of anio::Write,which must be transferred to the task upon construction (in
spawn).When the terminal methods of the builder require ownership, there is a basic
tradeoff:
If the other builder methods take/return a mutable borrow, the complex
configuration case will work well, but one-liner configuration becomes
impossible.
If the other builder methods take/return an owned
self, one-liners continueto work well but complex configuration is less convenient.
Under the rubric of making easy things easy and hard things possible, all
builder methods for a consuming builder should take and returned an owned
self. Then client code works as follows:One-liners work as before, because ownership is threaded through each of the
builder methods until being consumed by
spawn. Complex configuration, however,is more verbose: it requires re-assigning the builder at each step.
Dependability
Functions validate their arguments (C-VALIDATE)
Rust APIs do not generally follow the robustness principle: "be conservative
in what you send; be liberal in what you accept".
Instead, Rust code should enforce the validity of input whenever practical.
Enforcement can be achieved through the following mechanisms (listed in order of
preference).
Static enforcement:
Choose an argument type that rules out bad inputs.
For example, prefer
over
where
Asciiis a wrapper aroundu8that guarantees the highest bit iszero; see newtype patterns for more details on creating typesafe
wrappers.
Static enforcement usually comes at little run-time cost: it pushes the costs to
the boundaries (e.g. when a
u8is first converted into anAscii). It alsocatches bugs early, during compilation, rather than through run-time failures.
On the other hand, some properties are difficult or impossible to express using
types.
Dynamic enforcement:
Validate the input as it is processed (or ahead of time, if necessary). Dynamic
checking is often easier to implement than static checking, but has several
downsides:
input).
fail!orResult/Optiontypes,which must then be dealt with by client code.
Dynamic enforcement with
debug_assert!:Same as dynamic enforcement, but with the possibility of easily turning off
expensive checks for production builds.
Dynamic enforcement with opt-out:
Same as dynamic enforcement, but adds sibling functions that opt out of the
checking.
The convention is to mark these opt-out functions with a suffix like
_uncheckedor by placing them in arawsubmodule.The unchecked functions can be used judiciously in cases where (1) performance
dictates avoiding checks and (2) the client is otherwise confident that the
inputs are valid.
Destructors never fail (C-DTOR-FAIL)
Destructors are executed on task failure, and in that context a failing
destructor causes the program to abort.
Instead of failing in a destructor, provide a separate method for checking for
clean teardown, e.g. a
closemethod, that returns aResultto signalproblems.
Destructors that may block have alternatives (C-DTOR-BLOCK)
Similarly, destructors should not invoke blocking operations, which can make
debugging much more difficult. Again, consider providing a separate method for
preparing for an infallible, nonblocking teardown.
Debuggability
All public types implement
Debug(C-DEBUG)If there are exceptions, they are rare.
Debugrepresentation is never empty (C-DEBUG-NONEMPTY)Even for conceptually empty values, the
Debugrepresentation should never beempty.
Future proofing
Structs have private fields (C-STRUCT-PRIVATE)
Making a field public is a strong commitment: it pins down a representation
choice, and prevents the type from providing any validation or maintaining any
invariants on the contents of the field, since clients can mutate it arbitrarily.
Public fields are most appropriate for
structtypes in the C spirit: compound,passive data structures. Otherwise, consider providing getter/setter methods and
hiding fields instead.
Newtypes encapsulate implementation details (C-NEWTYPE-HIDE)
A newtype can be used to hide representation details while making precise
promises to the client.
For example, consider a function
my_transformthat returns a compound iteratortype.
We wish to hide this type from the client, so that the client's view of the
return type is roughly
Iterator<Item = (usize, T)>. We can do so using thenewtype pattern:
Aside from simplifying the signature, this use of newtypes allows us to promise
less to the client. The client does not know how the result iterator is
constructed or represented, which means the representation can change in the
future without breaking client code.
In the future the same thing can be accomplished more concisely with the
impl Traitfeature but this is currently unstable.Necessities
Public dependencies of a stable crate are stable (C-STABLE)
A crate cannot be stable (>=1.0.0) without all of its public dependencies being
stable.
Public dependencies are crates from which types are used in the public API of
the current crate.
A crate containing this function cannot be stable unless
other_crateis alsostable.
Be careful because public dependencies can sneak in at unexpected places.
Crate and its dependencies have a permissive license (C-PERMISSIVE)
The software produced by the Rust project is dual-licensed, under
either the MIT or Apache 2.0 licenses. Crates that simply need the
maximum compatibility with the Rust ecosystem are recommended to do
the same, in the manner described herein. Other options are described
below.
These API guidelines do not provide a detailed explanation of Rust's
license, but there is a small amount said in the Rust FAQ. These
guidelines are concerned with matters of interoperability with Rust,
and are not comprehensive over licensing options.
To apply the Rust license to your project, define the
licensefieldin your
Cargo.tomlas:And toward the end of your README.md:
Besides the dual MIT/Apache-2.0 license, another common licensing approach
used by Rust crate authors is to apply a single permissive license such as
MIT or BSD. This license scheme is also entirely compatible with Rust's,
because it imposes the minimal restrictions of Rust's MIT license.
Crates that desire perfect license compatibility with Rust are not
recommended to choose only the Apache license. The Apache license,
though it is a permissive license, imposes restrictions beyond the MIT
and BSD licenses that can discourage or prevent their use in some
scenarios, so Apache-only software cannot be used in some situations
where most of the Rust runtime stack can.
The license of a crate's dependencies can affect the restrictions on
distribution of the crate itself, so a permissively-licensed crate
should generally only depend on permissively-licensed crates.
External Links
License
This guidelines document is licensed under either of
http://www.apache.org/licenses/LICENSE-2.0)
http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in this document by you, as defined in the Apache-2.0 license,
shall be dual licensed as above, without any additional terms or conditions.