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-basedA 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 wrote | What 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 declares | no 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.valu | this 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#
- Functions (the unit of work) — what a function is, and how it is called
- Classes — the alternative when a pair wants a name and behaviour of its own
- Generic classes — the other way one declaration serves many types