Functor
Overview
A function p => p + 3000 adds a 3,000-won shipping charge to a product price. For an array, we must take each price, add the charge, and put it in a new array; if a price might be absent or a lookup might fail, we must check that first. The calculation is one line, but extracting and repackaging values adds different code for each structure.
A functor puts that work in the structure's map operation. Given f, map applies it to the values and returns the result with the outer structure preserved: an array keeps its length and order, Option distinguishes Some from None and calls f only for Some, and Result applies f to Ok while passing Err through. LINQ Select plays this role in C#.
Two laws state what preserving structure means for map. The identity law says that mapping with x => x must leave the original value unchanged; the composition law says that mapping once with f followed by g must equal mapping first with f and then with g. The first rules out dropping or reordering elements, and the second lets the mapping stages be combined or separated without changing the result.
Flow
The same f leaves a list as a list, Option as Option, and Result as Result. Err never passes through f, keeping its error message unchanged.
History
The name comes from category theory. Samuel Eilenberg and Saunders Mac Lane defined categories and functors in "General Theory of Natural Equivalences," presented to the American Mathematical Society in 1942 and published in its Transactions in 1945. A functor in that paper pairs a function on objects with a function on mappings, preserving identity mappings and composition. The two programming laws correspond to those requirements.1
Haskell expresses the pattern through the Functor type class. The Haskell 2010 Report defines class Functor f where fmap :: (a -> b) -> f a -> f b and requires fmap id = id and fmap (f . g) = fmap f . fmap g. Lists, IO, and Maybe belong to the class. Here f is a type constructor such as Maybe, still waiting for a type argument, rather than a complete type such as Int. Constructor classes entered the language in Haskell 1.3. The base documentation describes fmap as applying a function while "preserving the structure of f."234
C# has no common abstraction like Functor. A type parameter denotes a value type or reference type, so it cannot receive an unapplied type constructor such as Option<> and describe "every F with Select" in one interface. Each type defines its own Select instead. Query syntax works through translation: the specification turns from x in e select v into ( e ) . Select ( x => v ), whether Select is an instance method or an extension method.56
Why use it?
The caller passes the calculation p => p + 3000 to an array, Option, or Result. Extraction, repackaging, and the checks for absence or failure belong to the structure, instead of being repeated at each call site.
A lawful map makes Select(f).Select(g) equivalent to Select(x => g(f(x))). With effect-free f and g, splitting or combining those stages leaves the result unchanged.
Filtering elements, changing None into Some, or recovering an Err falls outside the structure-preserving work of map. If f returns Option, the result nests as Option<Option<int>>. A monad provides the operation that flattens and connects those computations; in C#, SelectMany fills that role.
The compiler checks names and signatures, not the two laws. A Select that discards elements still compiles and works in query syntax, while C#'s lack of a common abstraction also prevents writing one function for arbitrary functors.
Example
Add shipping to every price, then turn each result into a string. LINQ Select and query syntax perform the same mapping.
using System;
using System.Collections.Generic;
using System.Linq;
int[] prices = [12000, 0, 45000];
// Select transforms each element while preserving count and order.
var withShipping = prices.Select(p => p + 3000);
// The compiler also translates the query to withShipping.Select(p => $"{p} KRW").
var labels = from p in withShipping select $"{p} KRW";
Console.WriteLine(string.Join(", ", withShipping));
Console.WriteLine(string.Join(", ", labels));
Console.WriteLine($"count: {prices.Length} -> {labels.Count()}");
15000, 3000, 48000
15000 KRW, 3000 KRW, 48000 KRW
count: 3 -> 3The element count stays at 3; only the values change.
A broader example
Apply the same addShipping and format functions to an array, Option, and Result, then compare values to check the two laws. The .NET standard library has neither Option nor Result, so records define them with their own Select methods. A Select on Task awaits its result and applies f. The final Select intentionally breaks a law.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
Func<int, int> addShipping = p => p + 3000;
Func<int, string> format = p => $"{p} KRW";
Func<int, string> addThenFormat = p => format(addShipping(p));
// Apply the same two functions to a list, Option, and Result.
int[] prices = [12000, 0];
Option<int>[] options = [new Option<int>.Some(12000), new Option<int>.None()];
Result<int>[] results = [new Result<int>.Ok(12000), new Result<int>.Err("price missing")];
Console.WriteLine(string.Join(", ", prices.Select(addShipping).Select(format)));
foreach (var o in options) Console.WriteLine(o.Select(addShipping).Select(format));
foreach (var r in results) Console.WriteLine(r.Select(addShipping).Select(format));
// Identity law: Select(x => x) leaves the original value unchanged.
// Composition law: Select(addThenFormat) equals Select(addShipping).Select(format).
var listComp = prices.Select(addThenFormat).SequenceEqual(prices.Select(addShipping).Select(format));
Console.WriteLine($"list: id={prices.Select(x => x).SequenceEqual(prices)}, comp={listComp}");
foreach (var o in options)
{
var comp = o.Select(addThenFormat).Equals(o.Select(addShipping).Select(format));
Console.WriteLine($"option: id={o.Select(x => x).Equals(o)}, comp={comp}");
}
foreach (var r in results)
{
var comp = r.Select(addThenFormat).Equals(r.Select(addShipping).Select(format));
Console.WriteLine($"result: id={r.Select(x => x).Equals(r)}, comp={comp}");
}
// A Select method enables query syntax for Option and Task too.
Console.WriteLine(from p in options[0] select addThenFormat(p));
Console.WriteLine(await (from p in Task.FromResult(12000) select addThenFormat(p)));
// The compiler does not check laws: a Select dropping the first element still compiles.
var batch = new LossyBatch<int>([12000, 0]);
var same = from x in batch select x;
Console.WriteLine($"lossy: {string.Join(", ", same.Items)}, id={same.Items.SequenceEqual(batch.Items)}");
abstract record Option<T>
{
public abstract Option<U> Select<U>(Func<T, U> f);
// Call f only when a value is present.
public sealed record Some(T Value) : Option<T>
{
public override Option<U> Select<U>(Func<T, U> f) => new Option<U>.Some(f(Value));
}
// Return None without calling f when no value is present.
public sealed record None : Option<T>
{
public override Option<U> Select<U>(Func<T, U> f) => new Option<U>.None();
}
}
abstract record Result<T>
{
public abstract Result<U> Select<U>(Func<T, U> f);
// Apply f only to a success value.
public sealed record Ok(T Value) : Result<T>
{
public override Result<U> Select<U>(Func<T, U> f) => new Result<U>.Ok(f(Value));
}
// Pass failure through with the same message, without calling f.
public sealed record Err(string Error) : Result<T>
{
public override Result<U> Select<U>(Func<T, U> f) => new Result<U>.Err(Error);
}
}
static class TaskFunctor
{
// Await the result and apply f; await propagates a task failure as an exception.
public static async Task<U> Select<T, U>(this Task<T> task, Func<T, U> f) => f(await task);
}
// An unlawful Select changes the structure by changing the element count.
sealed class LossyBatch<T>(T[] items)
{
public T[] Items => items;
public LossyBatch<U> Select<U>(Func<T, U> f) => new(items.Skip(1).Select(f).ToArray());
}
15000 KRW, 3000 KRW
Some { Value = 15000 KRW }
None { }
Ok { Value = 15000 KRW }
Err { Error = price missing }
list: id=True, comp=True
option: id=True, comp=True
option: id=True, comp=True
result: id=True, comp=True
result: id=True, comp=True
Some { Value = 15000 KRW }
15000 KRW
lossy: 0, id=FalseNone and Err pass through without calling f, and the two laws hold for the five inputs compared here; that establishes the sample results rather than proving the laws for every input. LossyBatch also compiles because it has Select, but drops the first element, making from x in batch select x differ from the original (id=False). The Task Select propagates an exception if the task fails rather than carrying failure as a value like Result. ContinueWith does something similar but receives the completed Task itself instead of its result, requiring explicit checks for failure and cancellation if used as map.7
TypeScript example
TypeScript arrays use map for the same purpose. Option and Result use unions distinguished by kind, with a mapping function for each structure.
type Option<T> = { kind: "some"; value: T } | { kind: "none" };
type Result<T> = { kind: "ok"; value: T } | { kind: "err"; error: string };
// Call f only for a present value; otherwise return none unchanged.
function mapOption<T, U>(o: Option<T>, f: (x: T) => U): Option<U> {
return o.kind === "some" ? { kind: "some", value: f(o.value) } : o;
}
// Pass the same error through without calling f.
function mapResult<T, U>(r: Result<T>, f: (x: T) => U): Result<U> {
return r.kind === "ok" ? { kind: "ok", value: f(r.value) } : r;
}
const addShipping = (p: number): number => p + 3000;
const format = (p: number): string => `${p} KRW`;
const addThenFormat = (p: number): string => format(addShipping(p));
const id = <T>(x: T): T => x;
const same = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b);
const prices = [12000, 0];
const options: Option<number>[] = [{ kind: "some", value: 12000 }, { kind: "none" }];
const results: Result<number>[] = [{ kind: "ok", value: 12000 }, { kind: "err", error: "price missing" }];
console.log(prices.map(addShipping).map(format).join(", "));
for (const o of options) console.log(JSON.stringify(mapOption(mapOption(o, addShipping), format)));
for (const r of results) console.log(JSON.stringify(mapResult(mapResult(r, addShipping), format)));
// Compare values to check the identity and composition laws.
const listComp = same(prices.map(addThenFormat), prices.map(addShipping).map(format));
console.log(`list: id=${same(prices.map(id), prices)}, comp=${listComp}`);
for (const o of options) {
const comp = same(mapOption(o, addThenFormat), mapOption(mapOption(o, addShipping), format));
console.log(`option: id=${same(mapOption(o, id), o)}, comp=${comp}`);
}
for (const r of results) {
const comp = same(mapResult(r, addThenFormat), mapResult(mapResult(r, addShipping), format));
console.log(`result: id=${same(mapResult(r, id), r)}, comp=${comp}`);
}
15000 KRW, 3000 KRW
{"kind":"some","value":"15000 KRW"}
{"kind":"none"}
{"kind":"ok","value":"15000 KRW"}
{"kind":"err","error":"price missing"}
list: id=true, comp=true
option: id=true, comp=true
option: id=true, comp=true
result: id=true, comp=true
result: id=true, comp=truenone and err return the received object without calling f. Equality uses JSON.stringify here, so the comparison is suitable only for simple objects with matching key order.
See also
Example environment: each C# example is a separate .NET 10 console program; compile TypeScript for ES2022 or later, then run the JavaScript with Node.js.
Footnotes
- Samuel Eilenberg, Saunders MacLane. "General Theory of Natural Equivalences". Transactions of the American Mathematical Society 58 (1945), pp. 231-294. The first page records a presentation on September 8, 1942. §3 defines functors, with conditions (3.1) and (3.3). ↩
- Haskell 2010 Language Report, 6.3.5 The Functor Class ↩
- Changes from Haskell 1.2 to Haskell 1.3, Constructor classes section. ↩
- base 4.22.0.0, Data.Functor, Description of class Functor. ↩
- C# language specification, 8.5 Type parameters ↩
- C# language specification, 12.23.3.6 Select clauses. For instance and extension methods as translation targets, see 12.23.3.1 General. ↩
- Task<TResult>.ContinueWith, Description of the continuationFunction parameter. ↩