Rustのブログ記事でよく言及されることが多いので、API Guidelinesを読んでみました。
本GuidelinesはRustのlibrary teamによってメンテされているみたいです。
Guidelinesの位置付け
本Guidelinesの位置付けはAboutで述べられています。
These guidelines should not in any way be considered a mandate that crate authors must follow, though they may find that crates that conform well to these guidelines integrate better with the existing crate ecosystem than those that do not.
強制ではないが、Guidelinesに沿っておくことで既存のecosystemとよく馴染むというくらいのニュアンスでしょうか。
Naming
命名規則について。雰囲気で決めていましたがこうやって言語化してくれているのは非常に助かります。
とくに複数人開発で、命名がぶれてきたときに好みではなくこのあたりを参照して議論してみると良いのではないでしょうか。
Casing conforms to RFC 430 (C-CASE)
moduleはsnake_case
で型はUpperCamelCase
のような命名規則について述べられています。
一般的には型の文脈ではUpperCamelCase
、値の文脈ではsnake_case
と説明されておりなるほどと思いました。
UUIDやHTMLといったacronymsはUuid
,Html
のように書くように言われています。
Goでは、HTML
のように大文字なので最初は違和感あったのですが、今ではUpperCamelCaseのほうが自然に感じるようになりました。実用的な意味でも、コード生成の文脈ではUpperCamelCase
のほうがよいと考えています。特に多言語やprotobuf,sql等からの変換では、html_parser
をHTMLParser
のようにする必要があり、なんらかの方法でHTML
やUUID
を特別扱いする仕組みが必要になり、gormなんかは自前で辞書をメンテしていたりしていました。
意外なのが、専らlibraryを書く人向けのGuidelinesでlibraryのtop levelであるcratesについてはunclearになっていた点でした。(ただし参照されているRFC 430では, snake_case
(but prefer single word)とされています)
個人的には、packageは"-"、crateは"_"を使うのがいいかなと思っております。
Ad-hoc conversions follow at_
,to_
conventions (C-CONV)
as_
,to_
,into_
の使い分けに関するguide。ownershipと実行コストの観点から使い分けられております。例えば、Path
からstr
はborrowed -> borrowedなのですが、OSのpath名はutf8である保証がないので、str
変換時に確認処理がはいるので、この変換はPath::to_str
になります。
この点を嫌ってか、UTF-8のPath
型を提供するcamino
があります。
camino
では、Path::to_str() -> Option<&str>
が、Utf8Path::as_str() -> &str
になっています。
into_
についてはcostは高い場合と低い場合両方あり得ます。
例えば、String::into_bytes()
は内部的に保持しているVec<u8>
を返すだけなので低いのですが、BufWriter::into_inner()
はbufferされているdataを書き込む処理が走るので重たい処理になる可能性があります。
また、as_
とinto_
は内部的に保持しているデータに変換するので抽象度を下げる働きがある一方で、to_
はその限りでないという説明がおもしろかったです。
Getter names follow Rust convention (C_GETTER)
structのfieldの取得は、get_field
ではなく、field
とそのまま使おうということ。
ただし、Cell::get
のようにget
の対象が明確なときは、get
を使います。
また、getterの中でなんらかのvalidationをしている際は、unsafe fn get_unchecked(&self)
のvariantsを追加することも提案されています。
Methods on collections that produce iterators follow iter
,iter_mut
,into_iter
(C-ITER)
collectionのelementの型がT
だったとしたときに、Iterator
を返すmethodのsignatureは以下のようにしようということ。
ただし、このguideは概念的に同種(conceptually homogeneous)なcollectionにあてはまるとされ、str
はその限りでないので、iter_
ではなく、str::bytes
やstr::chars
が提供されている例が紹介されています。
また、適用範囲はmethodなので、iteratorを返すfunctionには当てはまらないとも書かれています。 加えて、iteratorの型はそれを返すmethodに合致したものにすることもIterator type names match the methods that produce them (C-ITER_TY)で書かれています。(into_iter()
はIntoIter
型を返す)
この点については、existential typeで, impl Iterator<Item=T>
のようにして具体型を隠蔽する方法もあると思うのですが、どちらがよいのかなと思いました。
Feature names are free of placeholder words (C_FEATURE)
feature abc
をuse-abc
やwith-abc
のようにしないこと。
また、featureはadditiveなので、no-abc
といった機能を利用しない形でのfeatureにしないこと。
Names use a consistent word order (C-WORD_ORDER)
JoinPathsError
, ParseIntError
のようなエラー型を定義していたなら、addressのparseに失敗した場合のエラーはParseAddrError
にする。(AddrParseError
ではない)
verb-object-errorという順番にするというguideではなく、crate内で一貫性をもたせようということ。
Interoperability
直訳すると相互運用性らしいのですが、いまいちピンとこず。ecosystemとの親和性みたいなニュアンスなのでしょうか。
Types eagerly implement common traits (C-COMMON_TRAITS)
Rustのorphan ruleによって、基本的にはimpl
はその型を定義しているcrateか実装しようとしているtrait側になければいけない。
したがって、ユーザが定義した型についてstdで定義されているtraitは当該crateでしか定義できない。
例えば、url::Url
がstd::fmt::Display
をimpl
する必要があり、application側でUrl
にDisplay
を定義することはできない。
この点についてはRUST FOR RUSTACEANSでも述べられていました。
featureでserdeのSerialize等を追加できるようにしてあるcrateなんかもあるなーと思っていたら(C-SERDE)で述べられていました。
Conversions use the standard traits From
, AsRef
, AsMut
(C-CONV-TRAITS)
From
,TryFrom
,AsRef
,AsMut
は可能なら実装してあるとよい。Into
とTryInto
はFrom
側にblanket implがあるので実装しないこと。
u32
とu16
のように安全に変換できる場合とエラーになる変換がある場合は、それぞれFrom
とTryFrom
で表現できる。
Collections implement FromIterator
and Extend
(C-COLLECT)
collectionを定義したら、FromIterator
とExtend
を定義しておく。
Data structures implement Serde's Serialize
, Deserialize
(C-SERDE)
data structureの役割を担う型はSerialize
とDeserialize
を実装する。
ある型がdata structureかどうかは必ずしも明確でない場合があるが、LinkedHashMap
やIpAddr
をJsonから読んだり、プロセス間通信で利用できるようにしておくのは理にかなっている。
実装をfeatureにしておくことで、downstream側で必要なときにコストを払うことができるに実装することもできる。
= { = "1.0", = true }
のようにしたり、deriveを利用する場合は以下のようにできる。
[]
= { = "1.0", = true, = ["derive"] }
Types are Send
and Sync
where possible (C-SEND_SYNC)
Send
とSync
はcompilerが自動で実装するので、必要なら以下のテストを書いておく。
時々こういうtestを見かけていたのですが、guidelineにあったんですね。
Error types are meaningful and well-behaved (C-GOOD-ERR)
Result<T,E>
に利用するE
には、std::error::Error
、Send
,Sync
を実装しておくとエラー関連のecosystemで使いやすい。
エラー型として、()
は使わない。必要ならParseBoolError
のように型を定義する。
Binary number types provide Hex, Octal, Binary formatting (C-NUM_FMT)
|
や&
といったbit演算が定義されるようなnumber typeには、std::fmt::{UpperHex, LowerHex,Octal,Binary}
を定義しておく。
Generic reader/writer functions take R: Read
and W: Write
by value (C-RW-VALUE)
read/write処理を行う関数は以下のように定義する。
use Read;
これがcompileできるのは、stdで
が定義されているので、&mut stdin
もRead
になるから。関数側がr: &R
のように参照で定義されていないので、do_read(stdin)
のようにmoveさせてしまい、loopで使えなくなるのがNew Rust usersによくあるらしい。
逆に関数側が、fn do_read_ref<'a, R: Read + ?Sized>(_r: &'a mut R)
のように宣言されている例もみたりするのですが、このguideに従うかぎは不要ということなのでしょうか。
例えば、prettytable-rs::Table::print
Macros
専ら、declarative macroについて述べられています。
Input syntax is evocative of the output (C-EVOCATIVE)
入力のsyntaxが出力を想起させるという意味でしょうか。
macroを使えば実質的にどんなsyntaxを使うこともできるが、できるだけ既存のsyntaxによせようというもの。
例えば、macroの中でstructを宣言するなら、keywordにstructを使う等。
Item macros compose well with attributes (C-MACRO-ATTR)
macroの中にattributeを書けるようにしておこう。
bitflags!
Item macros work anywhere that items are allowed (C-ANYWHERE)
以下のようにmacroがmodule levelでも関数の中でも機能するようにする。
Item macros support visibility specifiers (C-MACRO-VIS)
macroでvisibilityも指定できるようにする。
bitflags!
Type fragments are flexible (C-MACRO-TY)
macroで$t:ty
のようなtype fragmentを利用する場合は以下の入力それぞれに対応できるようにする。
- Primitives:
u8
,&str
- Relative paths:
m::Data
- Absolute paths:
::base::Data
- Upward relative paths:
super::Data
- Generics:
Vec<String>
boilerplate用のhelper macroと違って外部に公開するmacroは考えること多そうで難易度高そうです。
Documentation
codeのコメント(rustdoc)について。
Crate level docs are thorough and include examples (C-CRATE-DOC)
See RFC 1687と書かれ、PRへリンクされています。
リンクでなく内容を書くべきというissueもたっているようでした。
PR作られていたのがnushell作られているJTさんでした。
内容としてはこちらになるのでしょうか。本家にはまだmergeされていないようでした。
All items have a rustdoc example (C-EXAMPLE)
publicなmodule, trait struct enum, function, method, macro, type definitionは合理的な範囲でexampleを持つべき。
合理的というのは、他へのlinkで足りるならそうしてもよいという意味。
また、exampleを示す理由は、使い方を示すというより、なぜこの処理を使うかを示すかにあるとのことです。
Examples use ?
, not try!
, not unwrap
(C-QUESTION-MARK)
documentのexampleであっても、unwrap
しないでerrorをdelegateしようということ。
/// ```rust
/// # use std::error::Error;
/// #
/// # fn main() -> Result<(), Box<dyn Error>> {
/// your;
/// example?;
/// code;
/// #
/// # Ok(())
/// # }
/// ```
上記のように、#
を書くとcargo test
でcompileされるが、user-visibleな箇所には現れないのでこの機能を利用する。
Function docs include error, panic, and safety considerations (C-FAILURE)
Errorを返すなら、# Errors
sectionで説明を加える。panicするなら、# Panics
で。unsafeの場合は、# Safety
でinvariantsについて説明する。
Prose contains hyperlinks to relevant things (C-LINK)
Link all the thingsということで、他の型へのlinkがかける。
linkにはいくつか書き方があり、RFC1946に詳しくのっていた。
Cargo.toml includes all common metadata (C-METADATA)
Cargo.toml
の[package]
に記載すべきfieldについて。licenseは書いておかないとcargo publish
できなかった気がします。
Release notes document all significant changes (C-RELNOTES)
Release notes = CHANGELOGという理解でよいのでしょうか。
CHANGELOGについては、keep a changelogのformatに従っていました。
Unreleased
sectionに変更点をためて行って、releaseのたびにそれをrelease versionに変える方式がやりやすかったです。
Breaking changeも記載されるとおもうのですが、なにが破壊的変更かがきちんと定義されているのはRustらしいと思いました。
また、crates.ioへのpublishされたsourceにはgitのtagを付与することも書かれていました。
Predictability
予測可能性について。
Smart pointers do not add inherent methods (C-SMART-PTR)
Box
のようなsmart pointerの役割を果たす型にfn method(&self)
のようなinherent methodsを定義しないようにする。
理由としては、Box<T>
の値がある場合にboxed_x.method()
という呼び出しがDeref
でT
のmethodなのかBox
側なのか紛らわしいから。
Conversions live on the most specific type involved (C-CONV-SPECIFIC)
型の間で変換を行う際、ある型のほうがより具体的(provide additional invariant)な場合がある。
例えば、str
は&[u8]
に対してUTF-8として有効なbyte列であることを保証する。
このような場合、型変換処理は具体型、この例ではstr
側に定義する。このほうが直感的であることに加えて、&[u8]
の型変換処理methodが増え続けていくことを防止できる。
Functions with a clear receiver are methods (C-METHOD)
特定の型に関連したoperationはmethodにする。
// Prefer
// Over
Methodはfunctionに比べて以下のメリットがある。
- importする必要がなく、値だけで利用できる。
- 呼び出し時にautoborrowingしてくれる。
- 型
T
でなにができるかがわかりやすい self
であることでownershipの区別がよりわかりやすくなる- ここの理由はいまいちわかりませんでした。
個人的にmethodにするか、関数にするか結構悩みます。
具体的にはmethodの中で、structのfieldの一部しか利用しない場合、利用するfieldだけを引数にとる関数を定義したくなったりします。(structを分割することが示唆しているのかもしれませんが)
Functions do not take out-parameters (C-NO-OUT)
いまいちよく理解できなかったのですが、戻り値を入力として受け取って加工して返すみたいなfunctionは避けようということでしょうか。例にあげられているコードがいまいち何を伝えたいかわからずでした。
Preferのfoo
はどうして,Bar
二つ返してるんでしょうか。
// Prefer
例外としては、buffer等のcallerがあらかじめ確保しているdata構造に対する処理として、read系の例があげられていました。
Operator overloads are unsurprising (C-OVERLOAD)
std::ops
を実装すると、*
や|
が使えるようになるが、この実装はMul
として機能するようにする。
Only smart pointers implement Deref
and DerefMut
(C-DEREF)
Deref
で委譲をやるようにしたことあったのですがアンチパターンということでやめました。
Stack overflowでも、delegateやambassadorの利用を推奨している回答がありました。
Constructors are static, inherent methods (C-CTOR)
型のconstructorについて。
基本は、T::new
を実装する。可能ならDefault
も。
domainによっては、new
以外も可能。例えば、File::open
やTcpStream::connect
。
constructorが複数ある場合には、_with_foo
のようにする。
複雑ならbuilder patternを検討する。
ある既存の型の値からconstructする場合は、from_
を検討する。from_
とFrom<T>
の違いは、unsafeにできたり、追加の引数を定義できたりするところにある。
Flexibility
Functions expose intermediate results to avoid duplicate work (C-INTERMEDIATE)
処理の過程で生成された中間データが呼び出し側にとって有用になりうる場合があるならそれを返すようなAPIにする。
これだけだといまいちわかりませんが、具体例として
Vec::binary_search
- 見つからなかった時はinsertに適したindexを返してくれる
String::from_utf8
- 有効なUTF-8でなかった場合、そのoffsetとinputのownershipを戻してくれる
HashMap::insert
- keyに対して既存の値があれば戻してくれる。recoverしようとした際にtableのlookupを予め行う必要がない。
ownershipとるような関数の場合、エラー時に値を返すようなAPIは参考になるなと思いました。
Caller decides where to copy and place data (C-CALLER_CONTROL)
のようにするなら最初からfoo(b: Bar)
でownershipをとるようにする。こうすれば呼び出し側でcloneするかmoveするかを選択できるようになる。
Functions minimize assumptions about parameters by using generics (C-GENERIC)
引数に対する前提(assumption)が少ないほど、関数の再利用性があがる。
ので、fn foo(&[i64])
のようにうけないで
のようにうけて、値のiterateにのみ依存していることを表現する。 ただし、genericsにもdisadvantageがあるのでそれは考慮する。(code size等)
Traits are object-safe if they may be useful as a trait object (C-OBJECT)
traitを設計する際は、genericsのboundとしてかtrait objectとして利用するのかを決めておく。
object safeなtraitにobject safeでないmethodを追加して、object safeを満たさなくならないように注意する必要があるということでしょうか。
trait safeでないmethodを追加する際は、where
にSelf: Sized
を付与してtrait objectからはよべなくする選択肢もあるそうで、Iterator
ではそうしているという例ものっていました。
Send
やSync
と同様にtest caseでobject safeのassertも書いておいた方がよさそうだなと思いました。
Type safety
Newtypes provide static distinctions (C-NEWTYPE)
単位やId(UserId,ItemId,...)等をi64
やString
ではなく専用の型をきって静的に区別する。
;
;
Arguments convery meaning through types, not bool
or Option
(C-CUSTOM-TYPE)
boolじゃなくて専用の型をきろう。boolに対してenumにするのがわかりやすいのは理解できるのですが、Optionも使わない方がよいものなのかなと思いました。
Optionはecosystemと親和性あるから変にwrapすると使いづらくなるのでは。
// Prefer
let w = new
// Over
let w = new
Types for a set of flags are bitflags
, not enums (C-BITFLAG)
他言語やシステムとの互換性の観点や整数値でフラグのsetを管理したい場合はbitflags
を使おう。
use bitflags;
bitflags!
Builders enable construction of complex values (C-BUILDER)
型T
のconstructが複雑になってきたときには、TBuilder
を作ることを検討する。必ずしもBuilderとする必要はなく、stdのchild processに対するCommand
やUrl
のParseOptions
のようにdomainに適した名前があるならそれを利用する。
Builder
のmethodのreceiverに&mut self
をとるか、self
をとるかでそれぞれトレードオフがある。
どちらを採用してもone lineは問題なくかけるが、ifを書こうとするとself
をとるアプローチは再代入させる形にする必要がある。
// Complex configuration
let mut task = new;
task = task.named; // must re-assign to retain ownership
if reroute
Dependability
Functions validate their arguments (C-VALIDATE)
RustのApiは一般的にはrobustness principleに従っていない。
つまり、インプットに対してliberalな態度を取らない。代わりにRustでは実用的な範囲でインプットをvalidateする。
validationは以下の方法でなされる。(優先度の高い順)
Static enforcement
// Prefer
// Over
ここではAscii
はu8
のwrapperでasciiとして有効なbyteであることを保証している。
こうしたstatic enforcementはcostをboundaries(u8
が最初にAscii
に変換される時)によせ、runtime時のcostを抑えてくれる。
Dynamic enforcement
実行時のvalidationの欠点としては
- Runtime overhead
- bugの発見が送れること
Result/Option
が導入されること(clientがハンドリングする必要がある)
また、production buildでのruntime costをさける手段としてdebug_assert!
がある。
さらに、_unchecked
版の処理を提供し、runtime costをopt outする選択肢を提供している場合もある。
Destructors never fail (C-DTOR-FAIL)
Destructorはpanic時も実行される。panic時のdestructorの失敗はprogramのabortにつながる。
destructorを失敗させるのではなく、clean teardownをcheckできる別のmethodを提供する。(close
など)
もし、こうしたclose
が呼ばれなかった場合は、Drop
の実装はエラーを無視するかloggingする。
Destructors that may block have alternatives (C-DTOR-BLOCK)
同様にdestructorはblockingする処理も実行してはならない。
ここでも、infallibleでnonblockingできるように別のmethodを提供する。
Debuggability
All public types implement Debug
(C-DEBUG)
publicな型にはDebug
を実装する。例外は稀。
Debug
representation is never empty (C-DEBUG-NONEMPTY)
空の値を表現していても、[]
なり""
のなんらかの表示をおこなうべきで、空の出力をするべきでない。
Future proofing
Sealed traits protect against downstream implementations (C-SEALED)
crate内でのみ実装を提供したいtraitについては、ユーザ側でimpl
できないようにしておくことで、破壊的変更を避けつつtraitを変更できるように保てる。
/// This trait is sealed and cannot be implemented for types outside this crate.
// Implement for some types.
ユーザはprivate::Sealed
を実装できないので、結果的にTheTrait
を実装できずboundariesのみで利用できる。
libで時々みかけて、最初は意図がわかっていなかったのですが、ユーザにtraitを実装されると、traitのsignatureの変更が破壊的変更になることをきらってのことだとわかりました。もっとはやくガイドラインを読んでおけばよかったです。
Structs have private fields (C-STRUCT-PRIVATE)
publicなfieldをもつことは強いcommitmentになる。ユーザは自由にそのfieldを操作できるのでvalidationやinvariantを維持することができなくなる。
publicなfieldはcompoundでpassiveなデータ構造に適している。(C spirit)
Newtypes encapsulate implementation details (C-NEWTYPE-HIDE)
以下のようなiterationに関するロジックを提供するmy_transform
を考える
use ;
Enumerate<Skip<I>>
をユーザにみせたくないので、Newtypeでwrapすると
use ;
;
こうすることで、ユーザ側のcodeを壊すことなく実装を変更できるようになる。
my_transform<I: Iterator>(input: I) -> impl Iterator<Item =(usize, I::Item)>
のようなsignatureも可能だが、impl Trait
にはトレードオフがある。
例えば、Debug
やClone
を書けなかったりする。
Data structures do not duplicate derived trait bound (C-STRUCT-BOUNDS)
ちょっと理解があやしいですが、trait boundの設けたstruct定義にderive
を書かないということでしょうか。
trait boundを書かなくてもderive
はgenericsがtraitを実装しているかで制御されるし、deriveを追加する際にtrait boundを追加するとそれはbreaking changeになるからという理解です。
// Prefer this:
// Over this:
Necessities
Public dependencies of a stable crate are stable (C-STABLE)
public dependenciesのcrateがstable(>=1.0.0
)でなければそのcrateはstableにできない。
From
の実装等、public dependenciesは以外なところに現れる。
Crate and its dependencies have a permissive licence (C-PERMISSIVE)
Rust projectで作られているsoftwareはMITかApache 2.0のdual licenseになっている。
Rustのecosystemとの親和性を重視するなら同じようにしておく。
crateのdependenciesのlicenseはcrate自身に影響するので、permissively-licensed crateは一般的にpermissively-licensed crateのみを利用する。
まとめ
Rustのapi guidelineをざっと眺めてみました。
いろいろなcrateで共通していることが書かれていた印象です。
自分で書いたりコードレビューしたりする際に参照していきたいです。