Summary#
A class that describes how to work with a value should not have to know which value. Declare it with a type parameter and one declaration serves every type:
class Column<T> {
public string Label;
public Func<T, string> Value;
}Without generics that shape has to be copied once per entity — a ReportColumn, a PersonColumn — each identical
except for one type. With them, Column<Report> and Column<Person> are two types from one declaration.
Signature#
class Name<T> { … } // one type parameter
class Name<TIn, TOut> { … } // several, comma-separated
Name<Report> // a CONSTRUCTED type: the argument supplied at the use site
new Name<Report> { … } // …and at constructionA type parameter is in scope over the whole class body — including in its methods' signatures. A method may also declare parameters of its own, which the call binds rather than the receiver:
class Util { public T Echo<T>(T v) { … } } // the method's own parameter
T Echo<T>(T v) { … } // a top-level function, the same wayentity cannot be generic.
Description#
The type argument goes where the class is USED#
The declaration names the parameter; the use site supplies the argument. Between them, the compiler knows what T
is at every point:
entity Report { [MaxLength(80)] string Title; decimal Total; }
[Composable] component Grid<T>(T[] rows, Column<T>[] columns) {
render {
foreach (var r in rows) {
Row { foreach (var c in columns) { Text(c.Value(r)); } }
}
}
}
[Route("/")] [AllowAnonymous]
component Home() {
var reports = Report.ToList(); // the read is a MEMBER — a render body holds values, it does not fetch
render {
Grid(rows: reports, columns: [
new Column<Report> { Label = "Title", Value = r => r.Title },
new Column<Report> { Label = "Band", Value = r => r.Total > 1000 ? "large" : "small" }
]);
}
}Grid mentions no entity at all. Column<T> mentions no entity. The only place Report appears is the call site —
which is exactly the point.
Reading a member substitutes the argument#
Value is declared Func<T, string>. Read off a Column<Report> it is a Func<Report, string>, so the lambda's
parameter binds to a real row and its body is checked against it:
new Column<Report> { Value = r => r.Title } // ✓ r is a Report
new Column<Report> { Value = r => r.Nmae } // ✗ 'Report' has no property 'Nmae'That substitution is the whole feature. Passing the wrong one is caught too — a Column<Person> where a
Column<Report> is expected is a different type, not merely a differently-labelled Column.
Where a type parameter may appear#
T is first-class on its own, as an array, and inside a function type:
class Holder<T> {
public T Row; // the type parameter itself
public T[] Rows; // an array of it
public Func<T, string> Read; // a selector over it
}What is not built yet is T inside a mutable collection or another generic type — List<T>, HashSet<T>,
Dictionary<string, T>. Those carry a collection kind and a value slot that the compiler derives from the
declaration alone, so they need their own storage rather than being let through. The refusal names the three
spellings that do work.
An unset member typed T reads default(T), decided by the type argument: a Holder<decimal> reads 0m, a
Holder<Report> reads null — because null is default for a type with no meaningful zero.
A query over a type parameter#
Inside a generic class, a type parameter bound to an entity reads like the entity: TNote.Count(),
TNote.Where(…), TRecord.Single(r => r.Id == id) — what Set<T>() gives a C# developer in EF. The class is written
once and used for each kind of row:
entity Customer {
[Required, MaxLength(80)] string Name;
security { allow read, create when IsAuthenticated || IsAnonymous; }
}
entity Note {
[MaxLength(80)] string Text = "";
security { allow read, create when IsAuthenticated || IsAnonymous; }
}
entity CustomerNote : Note {
[Required] Customer Customer;
security { allow read, create when IsAuthenticated || IsAnonymous; }
}
entity DeskNote : Note {
security { allow read, create when IsAuthenticated || IsAnonymous; }
}
public class Notes<TNote, TRecord> where TNote : Note {
public Func<TNote, TRecord> RecordOf;
public Notes(Func<TNote, TRecord> recordOf) { RecordOf = recordOf; }
public int All() => TNote.Count();
public int On(Guid record) => TNote.Count(n => RecordOf(n).Id == record);
}
public class CustomerNotes : Notes<CustomerNote, Customer> {
public CustomerNotes() : base(n => n.Customer) { }
}The compiler closes the class for each use. CustomerNotes : Notes<CustomerNote, Customer> makes one copy of
Notes with TNote written as CustomerNote and TRecord as Customer, so TNote.Count() is
CustomerNote.Count() and reads that type's rows — never a DeskNote, though the two share their base's table.
Every use writes its own copy (new Notes<DeskNote, Note>(…) is another), and each is checked against the class's
where constraints where it is written.
This happens to a generic class whose methods or constructors name a type parameter as a value. One that only
holds a T stays one class, as before; a field default that reads T.Members needs no copy (it is evaluated where
the object is built). A generic class nothing uses is never compiled, as in C++ templates rather than C#: write a use
to have it checked.
A function value inside a query#
RecordOf(n).Id == record above calls a function value inside a query over an entity. The function is put into
the SQL in place of the call — the server takes the value the query holds (n => n.Customer), substitutes its body
with the row, and the predicate becomes n.Customer.Id == @record: one statement, as composing an expression into an
EF query gives. What the function captured where it was made travels with it:
[Test]
void a_type_parameter_reads_its_own_table_and_the_function_is_composed() {
var ada = new Customer { Name = "Ada" };
var bo = new Customer { Name = "Bo" };
new CustomerNote { Customer = ada, Text = "called" };
new CustomerNote { Customer = ada, Text = "wrote" };
new CustomerNote { Customer = bo, Text = "called" };
new DeskNote { Text = "printer" };
UnitOfWork.Commit();
var notes = new CustomerNotes();
Assert.Equal(3, notes.All());
Assert.Equal(2, notes.On(ada.Id));
var wanted = "called";
Func<CustomerNote, bool> isWanted = n => n.Text == wanted;
Assert.Equal(2, CustomerNote.Count(n => isWanted(n)));
}Three conditions, each for a reason:
- The function answers a value (
Func<…, R>). AnActiondoes something, and nothing done can become SQL. - Which function it is does not depend on the row.
RecordOf(n)is one function for the whole read; its argument may be the row. A function read off the row would be a different one per row. - Its body can become SQL — member reads, comparisons, arithmetic, conditionals and the platform's own text,
number and date functions. A body that calls your own code, reads other rows or builds a value is refused when the
query runs, naming the function and the part that stops it. Load the rows first (
.ToList()) and call it over the list instead.
In a page's served query a function value is refused at compile time: the page's read is answered from inputs the browser sends, and a function is not one of them. Do the read in a function.
Methods can have their OWN type parameters#
A method may declare type parameters that the class does not, and they are bound by the call rather than by the receiver — inferred from the argument types:
class Util {
public T Echo<T>(T v) { return v; }
public string Describe<T>(T v) { return "described"; }
}
string UseIt() {
var u = new Util();
var n = u.Echo(2m); // T is decimal here
var s = u.Echo("hello"); // …and string here, from the same declaration
return s;
}This is the difference worth holding on to: a generic class fixes its argument once, where it is used
(Column<Report>), and every member read off it substitutes that one answer. A generic method decides per call.
A lambda is read too: a type parameter that only a Func<…>'s RESULT mentions is whatever the lambda produces, its
parameters typed from the inputs already known — the receiver's, or the other arguments'. That is what lets a method
take a selector the way EF's Property(t => t.Title) does:
entity Parcel {
[Required, MaxLength(40)] string Code;
decimal Weight;
security { allow read when IsAuthenticated; }
}
class Picker<T> {
public V Pick<V>(T item, Func<T, V> by) { return by(item); }
}
string Label(Parcel p) {
var picker = new Picker<Parcel>();
var code = picker.Pick(p, x => x.Code); // V is string
var weight = picker.Pick(p, x => x.Weight); // …and decimal
return $"{code} · {weight} kg";
}Inference reads the WHOLE shape of a parameter, as C# does — List<T> given a List<int> binds T to int,
Dictionary<K, V> binds both, T[] reads the element and a generic class's argument is read off Box<T>:
T First<T>(List<T> xs) { return xs[0]; }
V Lookup<K, V>(Dictionary<K, V> d, K key) { return d[key]; }
[Test]
void Inference_Reads_The_Argument_Shape() {
Assert.Equal(4, First(new List<int> { 4, 5 }));
var names = new Dictionary<int, string> { [1] = "Ada", [2] = "Linus" };
Assert.Equal("Linus", Lookup(names, 2));
}Inference reads through a base class too, as in C#: a function that takes a Job<TIn, TOut>, handed a
Measure declared as class Measure : Job<string, Reading>, binds TIn to string and TOut to Reading — what
Measure supplies for Job. So a typed job is passed by itself, and the result comes back typed:
Done<TOut> Run<TIn, TOut>(Job<TIn, TOut> job, TIn input) { … }
var done = Run(new Measure(), "the wall"); // Done<Reading>; "the wall" is checked as a stringWrite the arguments explicitly when inference has nothing to read them from — a type parameter that appears in no parameter cannot be inferred, and the compiler says so rather than guessing:
string Explicitly() {
var u = new Util();
return u.Echo<string>("hello");
}A top-level function may be generic the same way, and so may a method on a class that is itself generic — there the two sets are bound by different things, the class's by the receiver and the method's by the call. A method may not reuse one of its class's parameter names: the two would be different types wearing one name, and nothing in the source would show the reader which is which.
A function's type parameter can be constrained too#
where T : Bound goes after the parameter list, as in C#. Every call is checked, whether T is written
(On<Shipped>()) or inferred from an argument. Inside the body, T reads as its bound, so e.Name below is a
member of Notice:
class Notice { public string Name = ""; }
class Shipped : Notice { }
bool Listens<TNotice>() where TNotice : Notice { return true; }
string NameOf<T>(T e) where T : Notice { return e.Name; }
bool Wired() { return Listens<Shipped>(); }
string Named() { return NameOf(new Shipped { Name = "shipped" }); }A function with no type argument to infer from, like Listens<TNotice>(), is where the constraint pays for itself:
without it, Listens<Invoice>() on a class that is not a notice would compile and do nothing useful.
Why can an entity not be generic?#
An entity cannot be generic. An entity is a table, and a table has no columns until T is known:
entity Row<T> { string Label; } // ✗ — write `class Row<T>` for an in-memory shapeExamples#
A comparator and a formatter are the same shape as a column, which is why this generalises past grids:
class Sorter<T> {
public string Label;
public Func<T, string> Key;
}
class Formatter<T> {
public Func<T, string> Render;
}Several type parameters are written the way C# writes them:
class Mapping<TIn, TOut> {
public Func<TIn, string> Read;
public Func<TOut, string> Write;
}Deriving from a generic#
A base or a contract may be constructed — : Box<int>, : IRepo<string>. The members that arrive are
substituted, so the subtype holds the closed type and not the parameter:
class Box<T> {
public T Value;
public T Get() { return Value; }
}
// `Value` is an `int` on IntBox, so ordinary arithmetic works on it.
class IntBox : Box<int> {
public int Doubled() { return Value * 2; }
}The subtype may stay generic and pass its own parameter on, and a third type close it. Substitution composes
through the chain, so Leaf.Value is a string:
class Mid<U> : Box<U> { public string Tag = "mid"; }
class Leaf : Mid<string> { }A contract works the same way, and an implementor satisfies it at the arguments it named — a TextRepo is an
IRepo<string> and is not an IRepo<int>:
interface IRepo<T> { T Fetch(); void Store(T v); }
class TextRepo : IRepo<string> {
public string Held = "";
public string Fetch() { return Held; }
public void Store(string v) { Held = v; }
}A where on a subtype goes after the base list, exactly as in C#:
class Shape { public string Kind = "shape"; }
class Holder<T> where T : Shape { public T Held; }
class ShapeHolder<T> : Holder<T> where T : Shape { }Closing a generic base over your own enum#
A kit names a vocabulary it does not know — "the kinds of thing that happen" — as a type parameter, and an app closes it over an enum of its own. An override is written in the subtype's terms, and it is the member the base calls:
public enum DraftNotice { ReadAsked, Finished }
/// The kit's base: it stores a kind by its member's name, and asks the app to say it.
public class Topic<TKind> {
/// A field default reads the enum the subtype closes this over — `DraftNotice.Members` on DraftTopic.
public TKind[] Kinds = TKind.Members;
public List<string> KindNames = TKind.Members.Select(m => m.Name).ToList();
public virtual string Say(TKind kind) { return "the kit's words"; }
public string SayStored(string name) {
var i = KindNames.IndexOf(name);
return i < 0 ? "" : Say(Kinds[i]);
}
}
/// The app's topic: its own enum, its own words, a switch over its own kinds.
public class DraftTopic : Topic<DraftNotice> {
public override string Say(DraftNotice kind) {
switch (kind) {
case DraftNotice.ReadAsked: return "asked to read";
case DraftNotice.Finished: return "finished";
}
return "";
}
}
string Said() { return new DraftTopic().SayStored("Finished"); } // "finished"A kind of another enum passed to a member the base declared in terms of TKind is refused at the call: TKind is
DraftNotice there. A field default reads TKind.Members where the object is built, as above (typeof(TKind).FullName
works there too). Inside one of the class's METHODS, TKind.Members makes the class one the compiler closes for each
use (see a query over a type parameter): in the copy for Topic<DraftNotice> it is DraftNotice.Members.
A generic function knows which enum its T is#
A generic function or method decides its type per call, and each call says which enum it bound: inside the body a
T value says its member's name ($"{kind}", kind.Name, kind.Label), T.Members lists the enum's members, and
typeof(T).Name / typeof(T).FullName name it. One body serves every enum — and anything else, where a value says
its ordinary text:
public enum DraftNotice { ReadAsked, Finished }
public enum OrderNotice { Shipped, Lost }
string Said<T>(T kind) { return $"{typeof(T).Name}.{kind}: {T.Members.Length} kinds"; }
string Both() { return Said(DraftNotice.Finished) + " / " + Said(OrderNotice.Lost); }
// "DraftNotice.Finished: 2 kinds / OrderNotice.Lost: 2 kinds"typeof(T) names whatever type the call bound#
Each call also says WHICH type it bound — an enum, an entity, a class — so typeof(T).FullName and typeof(T).Name
name it whatever kind it is. That is how a kit stores "the event this rule runs on" from the event's class alone,
with no string to keep in step with it (When = On<JobPlaced>(), see [Event] — what happens to a record, declared once):
public sealed class Shipped { public string Code = ""; }
public entity Parcel { [MaxLength(40)] string Code = ""; security { allow read when IsAnonymous || IsAuthenticated; } }
string KindOf<T>() { return typeof(T).FullName; }
string Named() { return KindOf<Shipped>() + " / " + KindOf<Parcel>(); } // "Shipped / Parcel"[Test]
void typeof_names_any_type_a_call_bound() {
Assert.Equal("Shipped / Parcel", Named());
}Errors#
| What you wrote | What you get |
|---|---|
entity Row<T> { … } | 'Row' is an entity, and an entity cannot be generic — it is a table, and a table has no columns until its type argument is known. |
public List<T> Rows; on a class Holder<T> | 'Holder.Rows' is typed List<T>, and a type parameter is not supported inside a mutable collection or a generic type yet — it works on its own (T Rows;), as an ARRAY (T[] Rows;), or inside a function type (Func<T, string> Rows;). |
Column<Report, Report> | 'Column' takes 1 type argument (T), but 2 were written. |
u.Echo<decimal, string>(2m) on T Echo<T>(T v) | 'Echo' declares 1 type parameter ('T'), but 2 were given. |
calling string Make<T>(), which mentions T in no parameter | 'Make' cannot infer its type parameter 'T' from these arguments … Write the type argument explicitly: Make<T>(…). |
public string Show<T>(T v) inside a class Box<T> | 'Show' declares a type parameter 'T', and so does the type that declares it … Rename one of them. |
c.Add<decimal>(1m) where Add declares no type parameters | 'Add' declares no type parameters, so the type argument in Add<…>(…) binds to nothing … Drop the type arguments. |
Column with no argument | 'Column' is generic — it needs a type argument for 'T'. |
a Column<Person> where a Column<Report> is expected | expects Column<T>[] for 'columns', but got Column<Person>[]. |
class Bad : Box<int, string> on a class Box<T> | 'Box' takes 1 type argument (T), but 2 were written on Bad. |
class Bad2 : NotGeneric<int> | 'NotGeneric' is not generic, so Bad2 : NotGeneric<int> has nowhere to put those arguments. |
class BadBox : Box<int> on a class Box<T> where T : Shape | 'Box' constrains 'T' to 'Shape', and 'int' is not a class at all — a bound names a type to derive from. |
Listens<Invoice>() on bool Listens<TNotice>() where TNotice : Notice | 'Listens' constrains 'TNotice' to 'Notice', and Invoice is not it or derived from it. Give 'Listens' a type that derives from 'Notice'. |
bool Bare() where T : Notice { … } | 'Bare' has no type parameters, so there is nothing for a where clause to constrain — did you mean Bare<T>(…) where T : …? |
bool ByInt<T>() where T : int { … } | 'ByInt' constrains 'T' to 'int', which is int — a where bound is a class (or an entity) that every type argument must be or derive from. |
IRepo<string> r = new IntRepo(); where IntRepo : IRepo<int> | cannot implicitly convert 'IntRepo' to 'IRepo<string>'. |
new DraftTopic().Say(OrderNotice.Shipped) on a DraftTopic : Topic<DraftNotice> | argument to 'Say': cannot pass 'OrderNotice' to parameter 'kind' of type 'DraftNotice'. |
class Notes<T> where T : Note used as Notes<Customer>, where Customer is no Note | 'Notes' constrains 'T' to 'Note', and 'Customer' is not it or derived from it. Give 'Notes' a type that derives from 'Note'. |
Notes<List<Note>> on a class closed per use | 'Notes' is closed for each type it is used with, and that type is written as a plain type name — 'T' here is not one. Give it a named type (an entity, a class or an enum). |
Note.Count(n => longOnes(n)) where longOnes holds n => IsLong(n) | longOnes(n) cannot be put into this query: the function it holds, n => IsLong(n), cannot become SQL — it calls IsLong, the app's own code, and SQL cannot call back into it. … (when the query runs) |
| a function value invoked in a page's served query | a function value cannot be composed into a query a page serves — the page's read is answered from inputs the browser sends, and a function is not one of them. … |
typeof(Nope).Name | typeof(Nope): 'Nope' is not a type here. |
See also#
- Func<T, R> —
Func<T, R>as a value: what a selector IS, and why its body reads only its parameters - Classes — the class itself: a value shape, never a table
- class properties — members that run a body on access
- component — generic components, which take their type argument by inference from the call site