Osy#betaRun it on Osyrin: osyrin.com

Reference / Function

Tuples and deconstruction

(bool ok, decimal value) Try(string s) { return (true, 1m); } var (ok, value) = Try("1");

A function returns several values by declaring a tuple return type and returning a parenthesized list. The caller reads them by name (`t.ok`), or deconstructs them straight into locals (`var (ok, value) = …`). Tuples are how Osy# expresses the `TryParse` shape — and an `out` parameter is the same thing written C#'s way: the compiler turns it into a returned tuple, because a call here can suspend and resume elsewhere, and only a return survives that.

preview11 examples compiled by CIfunctiontypestuplesauthoring

Summary#

A function that has two things to say should not have to invent a type for them. Declare the return as a tuple:

class Parser {
  public (bool ok, decimal value) Try(string s) {
    if (s == "1") { return (true, 1m); }
    return (false, 0m);
  }
}

The caller takes both:

decimal Read(string s) {
  var p = new Parser();
  var (ok, value) = p.Try(s);
  return ok ? value : 0m;
}

Signature#

(T1 name1, T2 name2) Fn(…) { … }     // a tuple RETURN type; names are optional
return (expr1, expr2);               // a tuple LITERAL
var (a, b) = Fn(…);                  // DECONSTRUCTION into new locals
(T1 a, T2 b) t = Fn(…);              // …or a typed local holding the whole tuple
t.name1                              // an element by the name written on the type
t.Item1                              // …or by position, 1-based

A tuple has at least two elements. Element names are optional ((bool, decimal) is the same type) and may differ between declarations.

Description#

out parameters are a return#

C#'s TryParse shape writes its extra values back through parameters, and you may write it exactly that way: declare out parameters on a function or a class method, and call it with out var x, out int x, out x (an existing variable) or out _. The compiler turns the declaration into a function that RETURNS its result and its outs as a tuple, and each call into the deconstruction of that tuple — because a call here can suspend and resume later, possibly on the other side of the client/server boundary, and only a return survives that.

class Inventory {
  public int Stock = 3;
  public bool TryTake(int wanted, out int left) {
    if (wanted > Stock) { left = Stock; return false; }
    left = Stock - wanted;
    return true;
  }
}

void MinMax(List<int> xs, out int min, out int max) { min = xs.Min(); max = xs.Max(); }

string Report() {
  var inv = new Inventory();
  var said = inv.TryTake(2, out var left) ? "took 2, " + left + " left" : "not enough";
  MinMax(new List<int> { 4, 1, 9 }, out var lo, out var hi);
  return said + "; " + lo + ".." + hi;          // "took 2, 1 left; 1..9"
}

C#'s rules hold: every out is assigned on every path before the method returns, and is not read before it is assigned. A call works in any statement, in a while condition (asked again on every pass) and in a lambda (per element). An async method cannot declare one (C# refuses that too), and ref is not supported.

A class with a Deconstruct(out …) takes apart like a tuple — var (x, y) = point;, foreach (var (x, y) in points), and the positional patterns below.

The identity is the element TYPES, not their names#

Two tuples with the same element types are the same type, whatever their elements are called — C#'s rule, and what keeps them interchangeable:

(bool ok, decimal value) First() { return (true, 1m); }
(bool success, decimal amount) Second() { return (false, 0m); }

decimal Either(bool pick) {
  var a = First();
  var b = Second();
  a = b;                        // the same type — names are labels, not identity
  return a.ok ? 1m : 0m;
}

Names are read at the place they are written: a.ok works because a came from a type spelled with ok.

Reading the elements#

By name, or by position. Both address the same slots, so a tuple never has "two ways to be right":

decimal Both(string s) {
  var p = new Parser();
  var t = p.Try(s);
  return t.ok == t.Item1 ? t.value : t.Item2;
}

A tuple nobody declared#

A tuple does not have to be written as a type anywhere. var t = (1, "a") is a (int, string) built from its elements, exactly as in C# — the compiler gives an inferred tuple its type from its SHAPE, and when the app does write that shape somewhere (a return type, a typed local) the two are the same type.

string Describe() {
  var t = (1, "a");                    // (int, string)
  var named = (count: 2, label: "b");  // names written on the literal
  var label = "c";
  var n = 3;
  var inferred = (label, n);           // names taken from the locals, as C# infers them
  return t.Item2 + named.label + inferred.label + t;   // "abc(1, a)" — a tuple writes itself "(1, a)"
}

Two tuples compare element by element ((1, "a") == (1L, "a") is true, the int widened) and t.Equals(u) agrees, so a tuple works as a Distinct or GroupBy key. A tuple element may be a value, an entity, or a list or array of one; null alone has no type (C# refuses var t = (1, null) too), and a tuple held inside an inferred tuple has no slot — write the type, (int, string) t = …;, for either.

Taking a tuple apart#

Deconstruction reads any tuple, and a KeyValuePair into its key and value; _ takes nothing. Assigning a tuple of EXISTING variables is the swap:

string Parts() {
  var (n, word) = (1, "x");
  var (_, only) = (2, "y");
  var a = 1;
  var b = 2;
  (a, b) = (b, a);                     // the right side is read first, so this swaps
  (int count, string label) = (3, "z"); // each name with its own type…
  (a, var fresh) = (4, 5);             // …or an existing variable beside a new one
  return n + word + only + a + b + count + label + fresh;   // "1xy413z5"
}

Deconstruction evaluates the call ONCE#

var (ok, value) = p.Try(s) reads the call a single time into a hidden local, then takes each element from it. That matters whenever the call does something: writing p.Try(s).Item1 and p.Try(s).Item2 would run it twice.

A switch that reads a tuple by position#

(x, y) switch { … } and t is (1, var s) test each element — a constant, a relational test (> 0), null / not null, _ for anything — and var name binds an element for the arm's value. No arm matching throws, as C#'s switch does.

string Quadrant(int x, int y) => (x, y) switch {
  (0, 0) => "origin",
  (_, 0) => "on the x axis",
  (> 0, var up) => "right, " + up + " up",
  _ => "elsewhere",
};

Each arm reads the elements again, so the switched value is a tuple of locals, a local, or a member — a CALL is refused, naming the fix (put it in a local first), because C# reads it once. An is pattern's var is read in its own condition and in the if branch it guards; over a class, a null value does not match.

Pairs and tuples the LINQ verbs answer#

A dictionary whose type the source infers iterates its KeyValuePairs, and the verbs whose selector-less form answers a pair or a tuple work without a selector:

string Tally() {
  var words = new List<string> { "b", "a", "b" };
  var counts = words.CountBy(w => w);          // KeyValuePair<string, int>s, in first-seen order
  var s = "";
  foreach (var (word, count) in counts) { s += word + count; }
  foreach (var (i, w) in words.Index()) { s += i; }               // (int Index, string Item)
  foreach (var p in new List<int> { 1, 2 }.Zip(words)) { s += p.First + p.Second; }   // (int First, string Second)
  return s + counts.First();                   // "b2a1" + "012" + "1b2a" + "[b, 2]"
}

AggregateBy(key, seed, f) folds each key's elements from ONE seed, as C# does — so the seed is written as a literal or a local (a new List<int>() there is refused: C# shares that one instance across every key). KeyValuePair.Create(k, v) makes a pair. A tuple projection over stored rows — Climb.Select(c => (c.Wall, c.Grade)).ToList() — reads the columns in SQL and builds one tuple per row.

Examples#

A parse that reports whether it succeeded, without a nullable and without a wrapper class:

class Amounts {
  public (bool ok, decimal value) Parse(string raw) {
    if (raw == "") { return (false, 0m); }
    return (true, 42m);
  }
}

decimal Total(string raw) {
  var a = new Amounts();
  var (parsed, amount) = a.Parse(raw);
  return parsed ? amount : 0m;
}

A tuple as a parameter, when a pair travels together:

decimal Apply((bool ok, decimal value) result) {
  return result.ok ? result.value : 0m;
}

Errors#

What you wroteWhat you get
(decimal) Fn(…)a tuple needs at least TWO elements — (bool ok, decimal value). A one-element tuple has nothing to hold that the type itself does not; write the type on its own.
var (only) = Fn();a deconstruction takes at least TWO names — var (ok, value) = …. For one value write var ok = … without the parentheses.
var t = (1, null);this tuple's element 2 is null, which has no type of its own (C# refuses var t = (1, null) too). Write the tuple's type where it is declared — (int, string) t = …; — and the literal will be that type.
var (a, b) = 5;a 'int' cannot be taken apart with var (a, b) = … — that reads a tuple or a KeyValuePair.
(Next(), 2) switch { (1, var s) => … }this switch's patterns test its value once per arm — C# reads it once, so put it in a local first (var v = …; v switch { … }), and it is read once.
xs.AggregateBy(k, new List<int>(), f)AggregateBy's seed is ONE value that every key starts from — C# evaluates it once and shares it. Write it as a literal or put it in a local first …
t.valuthis tuple has no element 'valu'. Did you mean 'value'? — it holds ok, value
bool F(out int x) { return true; }the out parameter 'x' must be assigned before the method returns, on every path that reaches this return (C# requires it too, CS0177) …
Try(s, out var v) where bool Try(string s)Try declares no out parameter for this out argument to receive — C# refuses it too (CS1615). Declare it on Try …
TryFoo(s, out var f) where bool TryFoo(string s, int n)'TryFoo' takes 'n' as a value, not an out — C# refuses an out argument for it too (CS1620). …
void Bump(ref int v)ref cannot be declared on a parameter — a parameter passes a value IN, and only the RETURN comes back out. …

See also#

Related

Functions (the unit of work)

A function is where your app's logic lives — a top-level unit of work, written like a C# method, that runs on the…

Classes

A class is an in-memory shape — data plus the behaviour that belongs to it — and it never touches the database. That is…

Generic classes

A class can declare type parameters, so one shape serves every type it is used with instead of being copied per entity…

Types

The values your app computes with, and the declarations that name and scope them. Most scalar types are exactly C#'s —…