c218d875 · core f63c1e69 · built 2026-10-26 22:39 UTCRename Perhaps to Option
Status — under review (2026-10-24). design settlement committed to a release milestone (tracking issue linked).
Status — accepted (2026-10-26). Rename accepted by the language owner; hard cut, shadowable, RFC-0182 rewritten, dated posts left as records
Status — integrated (2026-10-26). Spec section renamed with a new rule for the removed name; cross-checked against the integrated spec and the sibling RFCs
Status — implemented (2026-10-26). Implemented in metel-core#1625: the type is Option, the removed name is T0003 with a hint
Summary
Rename the core absence type Perhaps<T> to Option<T>. The variants Some { value } and
None are unchanged, as are all methods (is_some, unwrap_or, and_then, ok_or,
yolo, ...). Only the type's name changes, in the type position, in qualified paths
(std::core::Option::Some), in diagnostics, and in the specification.
fun first(xs: [i64]) -> Option<i64> { // was Perhaps<i64>
if (xs.len() == 0) { return None; }
Some { value = xs[0] }
}
This reverses a deliberate naming choice, so it states the case for and against below
rather than treating the rename as cosmetic. It is a breaking change to a name used in a
large part of every program that handles absence, so it needs this RFC first (see
metel_rfc_before_implementation).
Motivation
Familiarity
Option (Rust, Scala, OCaml, F#; Optional in Swift and Java) is what most readers already
call this type. RFC-0020
renamed the empty variant from nope to None on exactly this ground: "None is the
near-universal spelling for an absent optional value … nope is memorable but teaches the
wrong habit and surprises every developer familiar with typed languages." The type name has
the same property: Some/None are the universal pair of variants and Option is the
universal name of the type that has them; Perhaps is the one piece of the trio that is
not.
The name reads against its own variants
Perhaps<T> with variants Some and None mixes registers: "perhaps" is an adverb of
uncertainty, "some/none" are quantities. Option<T> reads as a noun for a thing that may or
may not hold a value, and its spelling tells the reader what Some/None are variants of.
In diagnostics today, the user sees Perhaps<String> next to Some/None in the same
message and has to learn they belong to the same thing.
Collision pressure from user code
Programs written by people who know the type as Option define their own: the integration
fixture int_03_generic_option_chain.mtl declares enum Option<T>. Since metel-core#1585
shadowing a core type is allowed and the user's definition wins, so this works, but it is a
sign of what authors reach for. With the core type called Option, there is nothing to
reinvent.
Not a new axis
This is a pure rename. It changes no semantics, no AST shape, no type-checking behaviour.
Prior decisions this has to answer
- RFC-0098 lists
Perhaps<T>notOption<T>among the places where Metel "already diverges from Rust's naming where it costs nothing — … a deliberate identity rather than an accident of copying Rust's grammar". That RFC's rationale is the main argument against this one, and its premise ("costs nothing") is the thing to check: the cost of keepingPerhapsis the familiarity friction above, paid by every reader, while the cost of renaming is paid once, now, by a small user base. Whether the identity argument outweighs that is the central open question (Open Question 1). - RFC-0020 deferred the rename of
.yolo()"pending the stdlib migration ofResultandPerhaps; the rename will be decided as part of that work, when the full method surface of both types is known." That migration has happened (PerhapsandResultare declared instd::core), and this RFC does not touch.yolo(); if it is to be reconsidered it is a separate question. - RFC-0079 (refused) describes
PerhapsandResultas the two primary types; nothing in it is a design objection to the name.
1. Scope of the rename
| Surface | Change |
|---|---|
Type name in std::core (enum Perhaps<T> in core.mtl) | enum Option<T> |
Qualified paths (std::core::Perhaps::Some) | std::core::Option::Some |
Diagnostics that print the type (cannot unify Perhaps<String> with …) | Option<String> |
Variants Some { value }, None; bare-variant patterns | unchanged |
Methods (ok_or, unwrap_or, yolo, and_then, is_some, ...) | unchanged |
Result::ok(self) -> Perhaps<T> and every other signature that mentions the type | spelled Option<T> |
Spec anchors such as spec.types.perhaps-t.* | keep stable ids; the prose and headings change |
| Other RFCs' prose and worked examples | updated mechanically when this lands |
Implementation surface (for sizing, not a design)
std::coredeclarations incore.mtl(about 30 occurrences) and one inenv.mtl.- The compiler names the type as a string literal at 13 sites across 8 Rust files (inference and construction of enum literals, path normalisation, builtins, display, symbols, the stdlib docs checker, stdlib tests). These are the same by-name sites that metel-core#1570 / #1585 track; the rename is a good moment to key them by the core declaration's id instead of by spelling.
- Roughly 60 integration fixtures and 50 spec lines mention
Perhaps; about 70 Markdown files in this repository do.
The path forward for users
Pre-1.0 and no edition gating (RFC-0020's precedent). Two options, in Open Question 2:
a hard cut, or one release in which Perhaps remains as a deprecated alias.
2. Alternatives considered
- Keep
Perhaps. The identity argument of RFC-0098: the name is distinctive, already shipped, and every doc and fixture uses it. Costs nothing to do; the friction is ongoing and unmeasured. Maybe(Haskell/Elm). Shorter and also an English word, but theSome/Nonevariants are the Rust/ML family's, andMaybegoes withJust/Nothing. It trades one mismatch for another.Optional(Swift, Java). Longer thanOption, no stronger ground.- Both names for ever,
Optionas an alias ofPerhaps. Two spellings of the most common type in the language split every codebase's style and every diagnostic's wording; rejected for permanence, though a transitional alias is Open Question 2. - Sugar
T?(Kotlin/Swift). A separate surface-syntax question that would conflict with?as the propagation operator (RFC-0182 and the spec's "The ? Operator"). Not proposed.
3. Costs
- A breaking rename of a type named in most programs. Every signature, annotation,
qualified path and message that says
Perhapschanges. The rewrite is mechanical (tokenPerhapsin type or path position, never a variable name), but it is a whole-codebase edit. - Churn in implemented RFCs and the spec. RFC-0020, RFC-0098, RFC-0142 and
others use
Perhapsin normative examples; the spec has dozens of mentions. Anchors stay stable; prose changes. - Loss of a distinctive spelling (RFC-0098's point). Unquantifiable, and the thing most likely to decide the RFC either way.
- Interaction with open drafts. RFC-0182 (
?onPerhaps) and other drafts name the type; whichever merges second rewrites its examples.
Open Questions
-
Does the identity argument (RFC-0098) outweigh the familiarity argument? This is a taste decision with a recorded precedent on each side (
nope→Nonefor familiarity,Perhapskept for identity). The RFC's job is to make the trade explicit; the decision is the language owner's.Decided (2026-10-26): rename. Familiarity outweighs the identity argument for this name. RFC-0098's "costs nothing" premise does not hold here: the cost of keeping
Perhapsis paid by every reader who knowsSome/Noneas the variants ofOption. -
Hard cut or transitional alias? A one-release
type Perhaps<T> := Option<T>would carry type positions, but qualified construction through an alias (Perhaps::Some { … }) needs checking, and the shadow rules (metel-core#1585) mean an alias instd::corecan itself be shadowed. If the alias cannot keep construction paths working, the choice is a hard cut with a precise diagnostic ("Perhapsis nowOption").Decided (2026-10-26): hard cut, no alias. After this lands
std::coredeclares noPerhaps. Naming it where a type, a path, a pattern or an expression is expected is the existingT0003(unknown type, unresolved path, undefined name), and the message adds thatPerhapsis nowOption. The hint applies only while the name resolves to nothing: a module that declares its ownPerhapsis unaffected. Pre-1.0 and a small user base make a one-release alias not worth its shadow-rule edge cases (metel-core#1585). -
Reserved or shadowable?
OptionjoinsResultandListas astd::coretype that a module may shadow (spec.declarations.variables.scoping-and-shadowing.legality-9). Is that acceptable for the most-used type, or should it be reserved like the intrinsic types (legality-8)? Not specific to this RFC; the rename just makes it likelier that a user declares a type with that name.Decided (2026-10-26): shadowable, like
ResultandList(legality-9). No new reserved-name rule;int_03_generic_option_chainand the shadowing fixtures keep working. -
Timing relative to
?onOption. If RFC-0182 is accepted, should it be rewritten to sayOptionfirst so the new rule never ships under the old name?Decided (2026-10-26): yes. RFC-0182's prose and examples were rewritten to
Optionwith this RFC's acceptance. RFC-0182 stays under review; only its wording changed. -
Documentation and blog. The v0.14.0 records post and getting-started pages show
Perhaps. Are published posts rewritten, or left as dated records with a note?Decided (2026-10-26): left as dated records, with no note added. The specification, the tutorials, the getting-started pages and the README change to
Optionwith the implementation; the v0.14.0 records post and other dated posts stay as published.
References
- RFC-0020 — Language Keyword Rebranding (
nope→None;.yolo()deferral). - RFC-0098 — Surface Keyword Renames (the "deliberate identity" rationale for
Perhaps). - RFC-0079 —
Perhaps<T>andResult<T, E>(refused; the types as described). - RFC-0182 —
?onPerhaps. - metel-core#1585, #1570 — by-name lookups of core types, the same sites the rename touches.
reference/spec/types.md— "Perhaps" ( spec.types.perhaps-t.*).
Integration cross-check (2026-10-26)
The rename is not implemented, so these worked examples are derived from the integrated spec's rules, not run; each becomes a fixture with the implementation (metel-core#1625). They hunt for interactions with already-integrated features and with the sibling RFCs still under review.
1. The removed name, at every site the hint covers (spec.types.perhaps-t.legality-2).
import std::core::Perhaps; // T0003, "`Perhaps` is now `Option`"
fun f(x: Perhaps<i64>) -> i64 { 0 } // T0003, same hint (type position)
fun g() { let a := std::core::Perhaps::None; } // T0003, same hint (qualified path)
fun h() { let b := Perhaps::None; } // T0003, same hint (leading path segment)
The hint is an addition to the message of the existing code; no new code, and no change when the name
resolves to something. It must not fire for a path that merely ends in Perhaps (other::Perhaps where
other is a user module that declares no such item): that is an ordinary unresolved path.
2. A module that declares its own Perhaps is a user type like any other. After the rename it no longer
shadows anything, so enum Perhaps { Maybe, Not } plus Perhaps::Maybe resolves to the user's enum with no
hint, and the shadowing fixtures written against the old name keep their meaning once they shadow Option.
3. Shadowing Option (scoping-and-shadowing.legality-9, metel-core#1585). A module-level or
block-local enum Option<T> { Has { v: T }, Nothing } wins for names written in that module; a value
std::env::get returns keeps the core type, so match on it names core's Some/None. Naming the core
type from inside the shadowing module works in a type position (std::core::Option<i64>, #1587). The
qualified expression form (std::core::Option::Some { value = 1 }) resolves to the module's own Option
today for the same reason it does for Result (metel-core#1616, open): the rename inherits that bug, it
does not add one. The interaction is recorded here so the fixture for it is written against #1616's fix, not
against the bug.
4. Aliased imports of the core type (#1588). import std::core::Option as Opt; then
let r: Opt<i64> := Opt::Some { value = 1 }; constructs against the annotation, as import std::core::Result as CR; does today. The alias is a user-chosen name, so the hint never applies to it.
5. ? on Option (RFC-0182, rewritten to Option with this acceptance). The enclosing-return-type
constraint compares the core Option, not a name; a function returning a shadowing user Option is neither
Result nor the core type and keeps the "needs the enclosing function to return" error. No interaction with
the rename beyond the spelling in the messages.
6. The intrinsic-name reservation (legality-8). Option is not an intrinsic type name and is not added
to the list; T0038 does not apply, and int_03_generic_option_chain (a user enum Option<T>) keeps
compiling. The reserved list and the shadowable core list stay disjoint.
7. Diagnostics that print the type. Unification, bound and record-diff messages print Option<String>.
Nothing in the message grammar depends on the name; the T0001 text in RFC-0182's example becomes
cannot unify Option<String> with Result<?1, ?2>.
8. Identity. The core declaration is interned once under its new name, and a user Perhaps or Option
gets its own Declared id (ADR-0065: ids are tagged, so no kind can collide). The by-name registry sites
the RFC lists compare the declaring module's id since metel-core#1585/#1604; the rename changes the
registered string and the Builtin variant, not how a site finds the type.
9. Sibling RFCs. RFC-0182 (rewritten, no remaining dependency on the old name). RFC-0183 (:= is
initialization-only), RFC-0185 (type-directed dereference) and RFC-0186 (row operator) do not mention the
type or its variants in a rule; their examples that say Perhaps change spelling only, when those RFCs
are next edited.
Result. No contradiction with the integrated spec and none with the RFCs active in the same cluster. One dependency is recorded (example 3, #1616); it is an existing bug the rename does not worsen.
Decision
Outcome: accepted, 2026-10-26, by the language owner.
Perhaps<T> is renamed Option<T>. Variants (Some { value }, None) and methods are unchanged,
and so are semantics, AST shape and type checking.
- Hard cut.
std::coredeclares noPerhapsonce this lands, and there is no alias. Using the name where nothing declares it isT0003with a message sayingPerhapsis nowOption. - Shadowable.
Optionmay be shadowed by a module-level or block-local declaration, exactly asResultandListmay. - RFC-0182 is rewritten to say
Option(done with this acceptance). - Dated posts are left as records; live pages change with the implementation.
- Spec anchors such as
spec.types.perhaps-t.*keep their ids; prose and headings change.