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

Reference / UI

Func<T, R>

component Grid<T>(Func<T, string> label) — a parameter that takes a lambda and is invoked for its value

A parameter or class field typed `Func<T, R>` takes a lambda and can be invoked for a result, so reusable code can be told HOW to get a value rather than being handed one. It is an expression plus the values it captured, not a compiled closure — which is what keeps it checkable and what makes a typo a compile error.

preview4 examples compiled by CIuiauthoringcompositionfunctions

Summary#

A component that works over data it doesn't know the shape of needs to be told how to get a value, not just which value:

[Composable] component Labelled(string name, Func<string, string> pick) {
  render { Text(pick(name)); }
}

[Page("/")] [AllowAnonymous]
component Home() {
  render {
    Stack(p: 4) {
      Labelled(name: "ada",   pick: x => x + " (picked)");
      Labelled(name: "grace", pick: x => "<" + x + ">");
    }
  }
}

pick is a function value. It is passed as a lambda and invoked with pick(name) wherever a value is wanted.

Signature#

Func<T, R>          // a parameter that takes a lambda of one argument returning R
Func<T1, T2, R>     // …of two

pick(row)           // invoke it for its value

Contrast Action / ()-delegate parameters, which are callbacks: they run and produce nothing. A Func<> produces a value and can stand anywhere a value can.

Description#

It is an expression, not a closure#

A function value is its parameter names, its body, and the values it captured. It is evaluated by binding the parameters and evaluating the body — the same thing a query lambda (Where(r => r.Total > 0)) has always been.

It captures the surrounding scope, by value#

The body may read its parameters and whatever is in scope where you wrote it. Each outer name is read once, where the lambda literal appears — not later, where it is invoked:

component Roster() {
  var admins = RoleGrant.Where(g => g.IsAdmin);       // the page's own query

  // `admins` is captured: read here, when this column list is built.
  var columns = [ new GridColumn<User> {
    Name = "Role",
    Value = u => admins.Any(g => g.User == u) ? "Admin" : "User"
  } ];
}

Why by value, and not read later. A function value travels: a column selector is handed to a grid and invoked deep inside it, where the names your body reads do not exist at all — so a late read could only ever find nothing. Reading at the literal is also the answer you want, because the expression that built the lambda re-evaluates when its own inputs change. When admins reloads, the column list is rebuilt and a fresh selector replaces the old one.

The one thing to know: a captured value is a snapshot. If you mutate a captured list in place, a selector built before the mutation keeps the value it was given.

Everything about the call is checked#

A slot's declared type is its contract, and all three ways of getting it wrong are compile errors:

you wroteyou get
pick: (a, b) => a.Title for a one-argument slottakes Func<Report, string> — 1 parameter(s), but the lambda declares 2
pick: x => x.Amount where the slot returns a stringthe lambda must produce string — it produces int
pick(a, b) on a one-argument functionthis verb takes 1 argument(s) — got 2

So a renamed field breaks the build rather than quietly rendering a blank.

Where it can be used#

A component parameter, and a class field — so a descriptor object can carry its own selector, which is what lets a caller describe a set of columns, filters or sort keys as data:

class Column {
  public Func<string, string> Value;
  public string Label;
}

[Composable] component Grid(string[] rows, Column[] columns) {
  render {
    Stack(gap: 2) {
      Row(gap: 3) { foreach (var h in columns) { Text(h.Label); } }
      foreach (var r in rows) {
        Row(gap: 3) { foreach (var c in columns) { Text(c.Value(r)); } }
      }
    }
  }
}

[Page("/")] [AllowAnonymous]
component Home() {
  string[] names = ["ada", "grace"];
  render {
    Grid(rows: names, columns: [
      new Column { Value = r => "<" + r + ">", Label = "Wrapped" },
      new Column { Value = r => r + "!",       Label = "Banged" }
    ]);
  }
}

The descriptor class can be generic, so one shape serves every row type instead of being copied per entity — see Generic classes:

class Column<T> { public string Label; public Func<T, string> Value; }
new Column<Report> { Label = "Title", Value = r => r.Title }

It is not only a UI thing#

Every example above is a component, but a function value is an ordinary value in an ordinary function too: a class field holds one, and invoking it produces a result wherever a value is wanted.

class Report { public string Title; public Report(string title) { Title = title; } }
class Column<T> { public string Label; public Func<T, string> Value; }

string TitleOf(string title) {
  var col = new Column<Report> { Label = "Title", Value = r => r.Title };
  return col.Value(new Report(title));
}

The one limit: a function value cannot be held across an await that suspends. What would have to travel is a body expression plus a captured environment, which is not a storable value — so invoke it before the await and hold its result instead.

Examples#

[Composable] component Show(int n, Func<int, string> fmt) {
  render { Text(fmt(n)); }
}

[Page("/")] [AllowAnonymous]
component Home() {
  render {
    Stack(p: 4) {
      Show(n: 42, fmt: v => "n = " + v);
      Show(n: 42, fmt: v => "[" + v + "]");
    }
  }
}

See also#

Related

component

The one archetype for all UI: a bounded reactive unit — typed props, reactive members (fields, `live`…

Slot (child content)

A `Slot` marks where a component renders the content block its caller wrapped around it. Writing `Card { Text("hi"); }`…

Cell template (your own content in a control's cell)

A control that paints cells — a grid — writes plain text in each one. A `slot <Field> { row => … }` block on the call…

Generic classes

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