Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

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 — there are no `out` parameters, because a call here can suspend and resume elsewhere, and only a return survives that.

preview6 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#

Why not out#

C#'s TryParse shape writes its second value back through a parameter. Osy# has no out (nor ref) to declare, and the reason is not stylistic: a call here can suspend and resume later, possibly on the other side of the client/server boundary. A pending write-back into a caller's frame has no meaning once that frame may be gone. A return survives by construction, so the value comes back as one.

```osy title="the shape out would have had" syntax public bool Try(string s, out decimal v) { … } // ✗ not supported — see the refusal, which names this page public (bool ok, decimal value) Try(string s) // ✓ the same information, as a return


⚠ **The built-in `T.TryParse` is the exception, and it is not one you have to remember.**
`decimal.TryParse(s, out var v)` — the CALL, not a declaration of your own — compiles: the compiler rewrites it
into `decimal? v = null; try { v = decimal.Parse(s); } catch { }` ahead of the statement. That is only available for
the stdlib parses; a function of your own that wants to hand back two values returns a tuple, as below.

### The identity is the element TYPES, not their names   {#identity}
Two tuples with the same element types are the **same type**, whatever their elements are called — C#'s rule, and
what keeps them interchangeable:

```osy title="one shape, two spellings, one type" test app=function-tuples
(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;
}

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.

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.
a literal whose shape nothing declaresno tuple type (decimal, decimal) is declared in this app, so there is nothing for this literal to be. A tuple shape comes from a DECLARATION — write it as a return type … and the literal will match it.
t.valuthis tuple has no element 'valu'. Did you mean 'value'? — it holds ok, value
public bool Try(out string v)out is not supported — a parameter passes a value IN, and only the RETURN comes back out … For the TryParse shape return a NULLABLE and test it, … or return a small class.

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 —…