hd-lang

#Ops

Status: standard library specification draft.

This chapter defines the part of std.ops that lib/std writes in ordinary hd over the language tier:

  • the Default trait;
  • the standard Default implementations, including its tuple template;
  • derived Default, through the trait's template, and the @default variant marker;
  • Eq for the range types.

The language tier keeps the rest of std.ops, since the compiler knows those items by name:

Item Why it stays in the language tier
the operator traits, such as Add and Neg lang items that operators call (Operator Traits)
NumSuffix, StrPrefix, Template the compiler recognizes them by their qualified names (Literal Suffixes)
Range, RangeFrom, RangeTo, RangeFull lang items that range expressions build (Range Expressions)

#Default Trait

Default gives a type's default value:

use std.ops.Default

fn fresh[T < Default]() -> T:
    T::default()

fn start() -> (i32, string, bool):
    fresh()
  1. std-ops.default.traitstd.ops declares trait Default with one associated function, fn default() -> Self, which returns the type's default value.
  2. std-ops.default.importDefault is not a prelude name, so code imports it, as in use std.ops.Default.
  3. std-ops.default.callGeneric code calls it through a bound, as in T::default(), by trait.assoc-call.parameter.

#Standard Implementations

std implements Default for these types:

Rule Type Default value
std-ops.default.std.integerIntegers i8, i16, i32, i64, u8, u16, u32, u64, and usize 0
std-ops.default.std.floatFloats f32 and f64 0.0
std-ops.default.std.boolBoolean bool false
std-ops.default.std.stringString string ""
std-ops.default.std.listList List[T], for any T the empty list []
std-ops.default.std.mapMap Map[K, V], for any valid key type K and any V the empty map {}
std-ops.default.std.optionalOptional T?, for any T .None
std-ops.default.std.tuple.any-sizeTuple a tuple of any size, each of whose elements implements Default; () is included each element's default, as in (0, "", false)
std-ops.default.std.tuple.rest-emptyRest tuple a tuple with a rest element List[T]... whose fixed elements implement Default each fixed element's default, then an empty rest
  1. std-ops.default.std.tuple-templatestd.ops declares a tuple template for Default, which builds each element's default through the tuple's Structure.
  2. std-ops.default.std.rest-anyA rest tuple's T need not implement Default, since its default rest holds no items.
  3. std-ops.default.std.missing-elementA tuple with an element that does not implement Default does not implement Default. Using it where Default is required is an error. Error: unsatisfied-trait-bound.
Error example
use std.ops.Default

struct Secret:
    value: i32

fn fresh[T < Default]() -> T:
    T::default()

fn start() -> (i32, Secret):
    fresh()  # error: unsatisfied-trait-bound

Note. The default of (i32, string, List[i32]...) is (0, ""): each fixed element's default, then an empty rest.

Why. Generic code needs a starting value it cannot write as a literal, as T::zero() gives numbers one. A rest tuple's rest is a list, and an empty list needs nothing from its items.

See also: Associated Function Calls, Derived Tuple Implementations.

#Derived Default

@derive(Default) builds a struct's or an enum's default from its members' defaults. A member's declared default wins over its type's. An enum marks its default variant with @default:

use std.ops.{Default, default}

@derive(Default)
struct Settings:
    name: string
    retries: i32 = 3
    tags: List[string]

@derive(Default)
enum Shape:
    Circle(radius: f64)
    @default
    Square(side: f64, label: string)

fn start() -> (Settings, Shape):
    (Settings::default(), Shape::default())

Settings::default() is Settings { name: "", retries: 3, tags: [] }, and Shape::default() is .Square(0.0, "").

std.ops declares the marker:

@annotate(.Variant)
pub struct DefaultVariant: pass

pub fn default() -> DefaultVariant
  1. std-ops.default.derive.templatestd.ops declares the template of Default, which @derive(Default) instantiates. It is ordinary std.ops code over std.structure, as the templates of Eq and Hash are.
  2. std-ops.default.derive.data.declaredA derived struct's default sets a member that declares a default, as retries: i32 = 3, to that default.
  3. std-ops.default.derive.data.type-defaultIt sets every other member to its own type's default, F::default().
  4. std-ops.default.derive.member-bound.undeclaredThe template requires Default only of the type of a member that declares no default. Such a member whose type does not implement it makes the type not derivable, reported at the opt-in and naming the member. Error: member-not-derivable.
  5. std-ops.default.derive.member-bound.declaredA member that declares a default needs no Default on its type, as secret: Secret = Secret { value: 7 } shows below.
  6. std-ops.default.derive.source-factThe template's source implementation carries fills_defaults(). So build fills a declared default itself, by annot.impl-fact.fills-defaults, and the member is never offered to the source.
  7. std-ops.default.derive.markerstd.ops declares the fact type DefaultVariant and the function default. Code imports default, as in use std.ops.default, and writes @default on a variant, which attaches default().
  8. std-ops.default.derive.enumA derived enum's default is its variant marked @default.
  9. std-ops.default.derive.payloadThe default of a marked payload variant fills each payload member with its own type's default.
  10. std-ops.default.derive.one-variantA derived enum must mark exactly one variant @default. An enum with no marked variant is an error, reported on the @derive line. Error: invalid-default-variant.
  11. std-ops.default.derive.one-variant.severalAn enum with several marked variants is the same error, reported on the second @default. Error: invalid-default-variant.
Error example
use std.ops.{Default, default}

@derive(Default)  # error: invalid-default-variant
enum Mode:
    Fast
    Slow

@derive(Default)
enum Level:
    @default
    Low
    @default  # error: invalid-default-variant
    High
Error example
use std.ops.Default

struct Secret:
    value: i32

@derive(Default)  # error: member-not-derivable
struct Vault:
    secret: Secret
use std.ops.Default

struct Secret:
    value: i32

@derive(Default)
struct Safe:
    secret: Secret = Secret { value: 7 }
    count: i32

fn opened() -> i32:
    Safe::default().secret.value

Note. A generic type gets T < Default for each type parameter that a built member uses, by annot.bound.params. A member that a derivation block omits takes its declared default, by annot.omit.default.

Why. No variant is more neutral than another, so the enum names its default, as Rust's #[default] does. A struct has one shape, so its default needs no marker.

See also: Derived Implementations, Templates.

#Range Equality

The range types compare field by field:

use std.ops.Range

fn same_span(left: Range[i32], right: Range[i32]) -> bool:
    left == right
Rule Type Equal when
std-ops.range.eq.rangeRange Range[T], where T < Eq start, end, and inclusive are each equal
std-ops.range.eq.fromFrom RangeFrom[T], where T < Eq start is equal
std-ops.range.eq.toTo RangeTo[T], where T < Eq end and inclusive are each equal
std-ops.range.eq.fullFull RangeFull always
  1. std-ops.range.eq.inclusiveSo 0..3 and 0..=2 are not equal, though they hold the same integers.

Why. A range is a plain value, and std value types implement Eq. Comparing the fields, not the integers, is what Rust's derived PartialEq for Range does.

See also: Range Expressions.