Language reference

Deconstruction, member, and union patterns

These patterns let a match check a record, a deconstructable .NET type, or a union case and take out the values it needs. This avoids separate type checks, casts, and property accesses.

  • RecordType(pattern1, pattern2, …) — nominal deconstruction pattern. Matches when the scrutinee can be treated as RecordType and each positional subpattern matches the corresponding value produced by RecordType’s Deconstruct shape.

    • Record patterns are valid for deconstructable nominal types, including record types and other nominal types that expose an accessible Deconstruct method.
    • Primary-constructor classes and structs with promoted public val / var parameters synthesize a Deconstruct method in declaration order, so the same pattern form works for them.
    • Each positional element is a pattern, so bindings still require let/var unless an outer construct such as if let pattern = expr or while let pattern = expr supplies the binding mode.
    • An element may optionally include a name before the colon (Name: pattern). Named elements bind by Deconstruct parameter name rather than source position and may appear in any order. Unnamed elements continue to fill the remaining unmatched parameters in declaration order.
    • A trailing whole-pattern designation may capture the successfully matched nominal value: Person(1, name, _) person.
    • The number of positional elements must match the selected Deconstruct parameters; mismatches are errors.
    • If a named element does not match any supported deconstruction member/parameter on the target type, the pattern is invalid and reports the member-not-found diagnostic.
    • When the scrutinee is a discriminated union, CaseName(...) in nominal-deconstruction-pattern syntax is interpreted as a discriminated-union case pattern and binds the case payload positionally.

Member patterns

  • .Case / Type.Case — member pattern. Matches a named member by name, including discriminated union cases, constants, or nested types.

    • The leading . resolves against the current scrutinee.

    • Member payloads may supply nested subpatterns matching the member’s parameter list, e.g. .Identifier(text) or Result<int, string>.Failure(let message).

    • Parentheses are optional for parameterless members.

    • Payload arity must match the declared parameters.

    • Each nested subpattern is typed to the corresponding member parameter.

    • A payload element introduces a new binding only when it uses let/var (val is also accepted). Comparing with an existing in-scope runtime value requires == value.

      • Example: .Case(let a, == b) binds a and matches the second payload against the runtime value of in-scope b.
    • A trailing whole-pattern designation may capture the matched member/case value: .Case(...) caseValue.

    • Use _ to explicitly discard a payload.

Union case patterns

Body-form unions synthesize named case types, and those cases may be matched using three equivalent forms. The unqualified case form is the default pattern syntax; the qualified and target-typed forms are alternatives for disambiguation or stylistic clarity:

  • Case(...) or Case — unqualified case pattern. This is the default form when the case name is unambiguous for the scrutinee's union case set.
  • Union.Case(...) — explicitly qualified union-member form.
  • .Case(...) — target-typed/member shorthand when the scrutinee determines the union.

For a case carrying a single unit payload, a bare Case pattern is sugar for Case(()).

Parenthesized unions do not synthesize case names. They are matched using the ordinary pattern for one of their declared variant types:

union Payment(Cash | Card)

let description = match payment {
    Cash(let amount) => "cash $amount"
    Card(let reference) => "card $reference"
}

The carrier and its contained value remain distinct pattern targets. Matching the carrier type itself, such as payment is Payment, tests the union instance. A variant-type property pattern such as payment is Card { Reference: let reference } let card first extracts the active Card through the carrier and then applies the recursive property pattern; the whole-pattern designation receives the extracted Card value.

Qualified variant types also work as bare type tests, for example result is Outcome.Ok<int>. This tests whether that case is active without invoking a case factory or comparing a constructed value. Explicit generic arguments belong to the case type and retain their normal arity and constraint checks. Imported member unions and Raven-produced unions follow the same rule. Qualified enum members and static values retain their existing value-pattern meaning.

Pattern combinators

  • pattern1 and pattern2 — conjunction. Succeeds only when both operands match. Bindings from either operand are available after the conjunction.

  • pattern1 or pattern2 — alternative. Matches when either operand matches.

  • not pattern — complement. Succeeds when the operand fails. not never introduces bindings.

Precedence: not > and > or. or associates left-to-right. Parentheses override precedence.

Imported member-union shorthand

A union imported through the CLI member-union contract may contain existing variant types rather than synthesized named cases. For those carriers, .Ok(let value) selects the unique variant named Ok from the scrutinee's member set, extracts it through the carrier's TryGetValue contract, and applies its Deconstruct method. The leading-dot form does not require a separate import of the variant type.

An unqualified generic variant pattern such as Ok(let value) requires the variant type to be in scope, for example through import Contracts.Choice.*. Raven infers its closed type arguments by matching that type's definition to a unique member of the scrutinee's union. Explicitly qualified patterns such as Choice.Ok<int>(let value) remain available. These are binding rules over ordinary CLI metadata; they do not depend on a particular runtime target or interface-name mapping. Existing pattern syntax is unchanged.

For value-type variants, deconstruction operates on an extracted copy. Matching the same carrier again must not observe mutations made by the earlier deconstructor.

Qualified case type tests contribute to nested exhaustiveness just like case patterns. For example, if Issue has only First and Second, the arms .Problem(Issue.First) and .Problem(Issue.Second) together cover the Problem case whose payload is Issue. A case type test covers all payload values of that case. Omitting either case remains non-exhaustive.

Declaration patterns may infer omitted generic arguments from a matching input type: Box box against a Box<int> input binds box as Box<int>. That inference precedes missing-type-argument diagnostics. An unrelated input, such as object, does not supply those arguments and still requires an explicit constructed type.