language-ext
Overview
language-ext is a comprehensive functional programming library for C# by Paul Louth. It brings Haskell/F#-inspired types and patterns to C#, including Option<T> (optional values), Either<L, R> (success/failure with typed errors), Try<T> (safe exception handling), Validation<F, S> (accumulating errors), immutable persistent collections (Lst<T>, Map<K,V>, Set<T>), and effectful computation types (Eff<T>, Aff<T>). All types support LINQ query syntax for monadic composition.
NuGet Packages
LanguageExt.Core -- core types (Option, Either, Try, collections)
LanguageExt.Transformers -- monad transformers
LanguageExt.Sys -- system effect types (console, file, etc.)
Option
using LanguageExt;
using static LanguageExt.Prelude;
// Creating options
Option<int> some = Some(42);
Option<int> none = None;
// From nullable
Option<string> fromNullable = Optional(GetNameOrNull());
// Map and Bind
Option<string> greeting = Some("Alice")
.Map(name => $"Hello, {name}!");
// Some("Hello, Alice!")
Option<Address> address = FindUser(123)
.Bind(user => user.Address is not null ? Some(user.Address) : None);
// Match
string result = FindUser(123).Match(
Some: user => $"Found: {user.Name}",
None: () => "User not found");
// IfNone for default values
string name = FindUser(123)
.Map(u => u.Name)
.IfNone("Unknown");
// LINQ syntax
var city =
from user in FindUser(123)
from addr in Optional(user.Address)
from c in Optional(addr.City)
select c;
Either<L, R>
using LanguageExt;
using static LanguageExt.Prelude;
// L = error type (Left), R = success type (Right)
public Either<string, Customer> ValidateCustomer(CreateCustomerRequest request)
{
if (string.IsNullOrWhiteSpace(request.Name))
return Left("Name is required");
if (!request.Email.Contains('@'))
return Left("Invalid email address");
if (request.Name.Length > 100)
return Left("Name too long");
return Right(new Customer(Guid.NewGuid(), request.Name, request.Email));
}
// Chaining with Bind
var result = ValidateCustomer(request)
.Bind(c => SaveToDatabase(c))
.Bind(c => SendWelcomeEmail(c))
.Match(
Right: customer => Results.Created($"/customers/{customer.Id}", customer),
Left: error => Results.BadRequest(new { error }));
// LINQ syntax for Either
Either<string, OrderSummary> ProcessOrder(OrderRequest req) =>
from customer in ValidateCustomer(req.Customer)
from items in ValidateItems(req.Items)
from total in CalculateTotal(items)
select new OrderSummary(customer.Id, items, total);
Validation<F, S> (Accumulating Errors)
using LanguageExt;
using static LanguageExt.Prelude;
// Unlike Either which short-circuits, Validation accumulates all errors
public Validation<Error, string> ValidateName(string name) =>
string.IsNullOrWhiteSpace(name)
? Fail<Error, string>(Error.New("Name is required"))
: Success<Error, string>(name);
public Validation<Error, string> ValidateEmail(string email) =>
!email.Contains('@')
? Fail<Error, string>(Error.New("Invalid email"))
: Success<Error, string>(email);
public Validation<Error, decimal> ValidateAmount(decimal amount) =>
amount <= 0
? Fail<Error, decimal>(Error.New("Amount must be positive"))
: Success<Error, decimal>(amount);
// Apply: accumulates ALL errors instead of stopping at first
var validated = (ValidateName(request.Name),
ValidateEmail(request.Email),
ValidateAmount(request.Amount))
.Apply((name, email, amount) => new Customer(name, email, amount));
// validated might contain: Fail(["Name is required", "Invalid email"])
validated.Match(
Succ: customer => Results.Ok(customer),
Fail: errors => Results.BadRequest(errors.ToList()));
Try
using LanguageExt;
using static LanguageExt.Prelude;
// Safe exception handling
Try<int> safeParse = () => int.Parse("not a number");
int result = safeParse.Match(
Succ: value => value,
Fail: ex => -1);
// Chaining Try operations
Try<Customer> LoadCustomer(Guid id) =>
Try(() => db.Customers.Find(id) ?? throw new KeyNotFoundException());
Try<Order> LoadLatestOrder(Customer customer) =>
Try(() => db.Orders
.Where(o => o.CustomerId == customer.Id)
.OrderByDescending(o => o.CreatedAt)
.First());
var orderTotal = LoadCustomer(customerId)
.Bind(c => LoadLatestOrder(c))
.Map(o => o.Total)
.Match(
Succ: total => $"Total: {total:C}",
Fail: ex => $"Error: {ex.Message}");
Immutable Collections
using LanguageExt;
using static LanguageExt.Prelude;
// Lst<T>: immutable linked list
Lst<int> numbers = List(1, 2, 3, 4, 5);
Lst<int> withSix = numbers.Add(6); // numbers is unchanged
// Map<K, V>: immutable dictionary
Map<string, int> scores = Map(
("alice", 95),
("bob", 87),
("charlie", 92));
Option<int> bobScore = scores.Find("bob"); // Some(87)
Map<string, int> updated = scores.AddOrUpdate("bob", 90);
// Set<T>: immutable set
Set<string> tags = Set("dotnet", "csharp", "functional");
Set<string> moreTags = tags.Add("fp"); // tags is unchanged
bool hasCsharp = tags.Contains("csharp"); // true
// Seq<T>: lazy immutable sequence
Seq<int> lazy = Seq(1, 2, 3, 4, 5)
.Filter(x => x % 2 == 0)
.Map(x => x * x);
Pattern Matching Helpers
using LanguageExt;
using static LanguageExt.Prelude;
// Fluent matching
string message = match(FindUser(123),
Some: user => $"Welcome back, {user.Name}",
None: () => "Please sign in");
// Working with tuples
var (name, age) = match(FindUser(123),
Some: user => (user.Name, user.Age),
None: () => ("Unknown", 0));
// ifSome / ifNone side effects
FindUser(123)
.IfSome(user => logger.LogInformation("User found: {Name}", user.Name))
.IfNone(() => logger.LogWarning("User not found"));
Practical Service Layer Example
using LanguageExt;
using static LanguageExt.Prelude;
public class OrderService
{
private readonly IOrderRepository _orders;
private readonly ICustomerRepository _customers;
private readonly IPaymentGateway _payments;
public OrderService(IOrderRepository orders, ICustomerRepository customers,
IPaymentGateway payments)
{
_orders = orders;
_customers = customers;
_payments = payments;
}
public Either<OrderError, OrderConfirmation> PlaceOrder(PlaceOrderRequest request) =>
from customer in _customers.FindById(request.CustomerId)
.ToEither(OrderError.CustomerNotFound)
from validItems in ValidateItems(request.Items)
from total in Right<OrderError, decimal>(validItems.Sum(i => i.Total))
from payment in _payments.Charge(customer, total)
.MapLeft(e => OrderError.PaymentFailed(e.Message))
from order in _orders.Create(customer.Id, validItems, total, payment.TransactionId)
.MapLeft(e => OrderError.DatabaseError(e.Message))
select new OrderConfirmation(order.Id, total, payment.TransactionId);
}
public abstract record OrderError
{
public sealed record CustomerNotFoundError() : OrderError;
public sealed record InvalidItemsError(string Message) : OrderError;
public sealed record PaymentFailedError(string Message) : OrderError;
public sealed record DatabaseErrorRecord(string Message) : OrderError;
public static readonly OrderError CustomerNotFound = new CustomerNotFoundError();
public static OrderError PaymentFailed(string msg) => new PaymentFailedError(msg);
public static OrderError DatabaseError(string msg) => new DatabaseErrorRecord(msg);
}
language-ext Types Overview
| Type |
Purpose |
Short-circuits? |
Option<T> |
Optional values (replaces null) |
N/A (None propagates) |
Either<L, R> |
Success (R) or typed error (L) |
Yes (Left stops chain) |
Validation<F, S> |
Success or accumulated errors |
No (collects all errors) |
Try<T> |
Exception-safe computation |
Yes (exception stops chain) |
TryAsync<T> |
Async exception-safe computation |
Yes |
Eff<T> / Aff<T> |
Side-effectful computation |
Yes |
Lst<T> |
Immutable linked list |
N/A |
Map<K,V> |
Immutable dictionary |
N/A |
Set<T> |
Immutable set |
N/A |
Best Practices
- Use
Option<T> instead of nullable returns for all methods that may not produce a value; convert at system boundaries using Optional() for nullable-to-Option bridging.
- Use
Either<L, R> for operations that can fail with domain-specific errors; define error types as discriminated unions (sealed record hierarchies) for exhaustive matching.
- Use
Validation<F, S> when you need to accumulate all validation errors (e.g., form validation) rather than stopping at the first failure as Either does.
- Prefer LINQ query syntax (
from ... in ... select) for chaining more than two monadic operations; it reads like sequential code while maintaining functional composition.
- Use
Prelude static imports (using static LanguageExt.Prelude) for ergonomic access to Some, None, Left, Right, Try, List, Map, and Set factory methods.
- Use language-ext immutable collections (
Lst<T>, Map<K,V>, Set<T>) for domain models that must be thread-safe and mutation-free.
- Convert between
Option and Either at service boundaries using ToEither(errorValue) when you need to add error context to an absent value.
- Use
Try<T> and TryAsync<T> at infrastructure boundaries (file I/O, HTTP calls) to convert exceptions into monadic values that compose safely.
- Avoid mixing null-returning APIs with language-ext types in the same layer; establish a boundary where nullables are converted to
Option<T>.
- Use
Match with explicit handlers for all cases to ensure complete handling; avoid IfSome/IfNone in business logic where both paths need to produce a value.
1---2name: language-ext3description: Use when applying comprehensive functional programming patterns in C# with language-ext, including Option, Either, Try, immutable collections, and monadic composition. USE FOR: Option<T> for null elimination, Either<L,R> for typed error handling, Try<T> for exception handling, immutable collections, monadic LINQ composition, Eff/Aff for effectful programming DO NOT USE FOR: lightweight Maybe only (use jflepp-maybe), F# language features (use fsharp), parser combinators (use pidgin), simple nullable handling without monadic composition4license: MIT5---6
7# language-ext
8
9## Overview
10language-ext is a comprehensive functional programming library for C# by Paul Louth. It brings Haskell/F#-inspired types and patterns to C#, including `Option<T>` (optional values), `Either<L, R>` (success/failure with typed errors), `Try<T>` (safe exception handling), `Validation<F, S>` (accumulating errors), immutable persistent collections (`Lst<T>`, `Map<K,V>`, `Set<T>`), and effectful computation types (`Eff<T>`, `Aff<T>`). All types support LINQ query syntax for monadic composition.
11
12## NuGet Packages
13- `LanguageExt.Core` -- core types (Option, Either, Try, collections)
14- `LanguageExt.Transformers` -- monad transformers
15- `LanguageExt.Sys` -- system effect types (console, file, etc.)
16
17## Option<T>
18```csharp
19using LanguageExt;
20using static LanguageExt.Prelude;
21
22// Creating options
23Option<int> some = Some(42);
24Option<int> none = None;
25
26// From nullable
27Option<string> fromNullable = Optional(GetNameOrNull());
28
29// Map and Bind
30Option<string> greeting = Some("Alice")
31 .Map(name => $"Hello, {name}!");
32// Some("Hello, Alice!")
33
34Option<Address> address = FindUser(123)
35 .Bind(user => user.Address is not null ? Some(user.Address) : None);
36
37// Match
38string result = FindUser(123).Match(
39 Some: user => $"Found: {user.Name}",
40 None: () => "User not found");
41
42// IfNone for default values
43string name = FindUser(123)
44 .Map(u => u.Name)
45 .IfNone("Unknown");
46
47// LINQ syntax
48var city =
49 from user in FindUser(123)
50 from addr in Optional(user.Address)
51 from c in Optional(addr.City)
52 select c;
53```
54
55## Either<L, R>
56```csharp
57using LanguageExt;
58using static LanguageExt.Prelude;
59
60// L = error type (Left), R = success type (Right)
61public Either<string, Customer> ValidateCustomer(CreateCustomerRequest request)
62{
63 if (string.IsNullOrWhiteSpace(request.Name))
64 return Left("Name is required");
65 if (!request.Email.Contains('@'))
66 return Left("Invalid email address");
67 if (request.Name.Length > 100)
68 return Left("Name too long");
69
70 return Right(new Customer(Guid.NewGuid(), request.Name, request.Email));
71}
72
73// Chaining with Bind
74var result = ValidateCustomer(request)
75 .Bind(c => SaveToDatabase(c))
76 .Bind(c => SendWelcomeEmail(c))
77 .Match(
78 Right: customer => Results.Created($"/customers/{customer.Id}", customer),
79 Left: error => Results.BadRequest(new { error }));
80
81// LINQ syntax for Either
82Either<string, OrderSummary> ProcessOrder(OrderRequest req) =>
83 from customer in ValidateCustomer(req.Customer)
84 from items in ValidateItems(req.Items)
85 from total in CalculateTotal(items)
86 select new OrderSummary(customer.Id, items, total);
87```
88
89## Validation<F, S> (Accumulating Errors)
90```csharp
91using LanguageExt;
92using static LanguageExt.Prelude;
93
94// Unlike Either which short-circuits, Validation accumulates all errors
95public Validation<Error, string> ValidateName(string name) =>
96 string.IsNullOrWhiteSpace(name)
97 ? Fail<Error, string>(Error.New("Name is required"))
98 : Success<Error, string>(name);
99
100public Validation<Error, string> ValidateEmail(string email) =>
101 !email.Contains('@')
102 ? Fail<Error, string>(Error.New("Invalid email"))
103 : Success<Error, string>(email);
104
105public Validation<Error, decimal> ValidateAmount(decimal amount) =>
106 amount <= 0
107 ? Fail<Error, decimal>(Error.New("Amount must be positive"))
108 : Success<Error, decimal>(amount);
109
110// Apply: accumulates ALL errors instead of stopping at first
111var validated = (ValidateName(request.Name),
112 ValidateEmail(request.Email),
113 ValidateAmount(request.Amount))
114 .Apply((name, email, amount) => new Customer(name, email, amount));
115
116// validated might contain: Fail(["Name is required", "Invalid email"])
117validated.Match(
118 Succ: customer => Results.Ok(customer),
119 Fail: errors => Results.BadRequest(errors.ToList()));
120```
121
122## Try<T>
123```csharp
124using LanguageExt;
125using static LanguageExt.Prelude;
126
127// Safe exception handling
128Try<int> safeParse = () => int.Parse("not a number");
129
130int result = safeParse.Match(
131 Succ: value => value,
132 Fail: ex => -1);
133
134// Chaining Try operations
135Try<Customer> LoadCustomer(Guid id) =>
136 Try(() => db.Customers.Find(id) ?? throw new KeyNotFoundException());
137
138Try<Order> LoadLatestOrder(Customer customer) =>
139 Try(() => db.Orders
140 .Where(o => o.CustomerId == customer.Id)
141 .OrderByDescending(o => o.CreatedAt)
142 .First());
143
144var orderTotal = LoadCustomer(customerId)
145 .Bind(c => LoadLatestOrder(c))
146 .Map(o => o.Total)
147 .Match(
148 Succ: total => $"Total: {total:C}",
149 Fail: ex => $"Error: {ex.Message}");
150```
151
152## Immutable Collections
153```csharp
154using LanguageExt;
155using static LanguageExt.Prelude;
156
157// Lst<T>: immutable linked list
158Lst<int> numbers = List(1, 2, 3, 4, 5);
159Lst<int> withSix = numbers.Add(6); // numbers is unchanged
160
161// Map<K, V>: immutable dictionary
162Map<string, int> scores = Map(
163 ("alice", 95),
164 ("bob", 87),
165 ("charlie", 92));
166
167Option<int> bobScore = scores.Find("bob"); // Some(87)
168Map<string, int> updated = scores.AddOrUpdate("bob", 90);
169
170// Set<T>: immutable set
171Set<string> tags = Set("dotnet", "csharp", "functional");
172Set<string> moreTags = tags.Add("fp"); // tags is unchanged
173bool hasCsharp = tags.Contains("csharp"); // true
174
175// Seq<T>: lazy immutable sequence
176Seq<int> lazy = Seq(1, 2, 3, 4, 5)
177 .Filter(x => x % 2 == 0)
178 .Map(x => x * x);
179```
180
181## Pattern Matching Helpers
182```csharp
183using LanguageExt;
184using static LanguageExt.Prelude;
185
186// Fluent matching
187string message = match(FindUser(123),
188 Some: user => $"Welcome back, {user.Name}",
189 None: () => "Please sign in");
190
191// Working with tuples
192var (name, age) = match(FindUser(123),
193 Some: user => (user.Name, user.Age),
194 None: () => ("Unknown", 0));
195
196// ifSome / ifNone side effects
197FindUser(123)
198 .IfSome(user => logger.LogInformation("User found: {Name}", user.Name))
199 .IfNone(() => logger.LogWarning("User not found"));
200```
201
202## Practical Service Layer Example
203```csharp
204using LanguageExt;
205using static LanguageExt.Prelude;
206
207public class OrderService
208{
209 private readonly IOrderRepository _orders;
210 private readonly ICustomerRepository _customers;
211 private readonly IPaymentGateway _payments;
212
213 public OrderService(IOrderRepository orders, ICustomerRepository customers,
214 IPaymentGateway payments)
215 {
216 _orders = orders;
217 _customers = customers;
218 _payments = payments;
219 }
220
221 public Either<OrderError, OrderConfirmation> PlaceOrder(PlaceOrderRequest request) =>
222 from customer in _customers.FindById(request.CustomerId)
223 .ToEither(OrderError.CustomerNotFound)
224 from validItems in ValidateItems(request.Items)
225 from total in Right<OrderError, decimal>(validItems.Sum(i => i.Total))
226 from payment in _payments.Charge(customer, total)
227 .MapLeft(e => OrderError.PaymentFailed(e.Message))
228 from order in _orders.Create(customer.Id, validItems, total, payment.TransactionId)
229 .MapLeft(e => OrderError.DatabaseError(e.Message))
230 select new OrderConfirmation(order.Id, total, payment.TransactionId);
231}
232
233public abstract record OrderError
234{
235 public sealed record CustomerNotFoundError() : OrderError;
236 public sealed record InvalidItemsError(string Message) : OrderError;
237 public sealed record PaymentFailedError(string Message) : OrderError;
238 public sealed record DatabaseErrorRecord(string Message) : OrderError;
239
240 public static readonly OrderError CustomerNotFound = new CustomerNotFoundError();
241 public static OrderError PaymentFailed(string msg) => new PaymentFailedError(msg);
242 public static OrderError DatabaseError(string msg) => new DatabaseErrorRecord(msg);
243}
244```
245
246## language-ext Types Overview
247
248| Type | Purpose | Short-circuits? |
249|------|---------|----------------|
250| `Option<T>` | Optional values (replaces null) | N/A (None propagates) |
251| `Either<L, R>` | Success (R) or typed error (L) | Yes (Left stops chain) |
252| `Validation<F, S>` | Success or accumulated errors | No (collects all errors) |
253| `Try<T>` | Exception-safe computation | Yes (exception stops chain) |
254| `TryAsync<T>` | Async exception-safe computation | Yes |
255| `Eff<T>` / `Aff<T>` | Side-effectful computation | Yes |
256| `Lst<T>` | Immutable linked list | N/A |
257| `Map<K,V>` | Immutable dictionary | N/A |
258| `Set<T>` | Immutable set | N/A |
259
260## Best Practices
261- Use `Option<T>` instead of nullable returns for all methods that may not produce a value; convert at system boundaries using `Optional()` for nullable-to-Option bridging.
262- Use `Either<L, R>` for operations that can fail with domain-specific errors; define error types as discriminated unions (sealed record hierarchies) for exhaustive matching.
263- Use `Validation<F, S>` when you need to accumulate all validation errors (e.g., form validation) rather than stopping at the first failure as `Either` does.
264- Prefer LINQ query syntax (`from ... in ... select`) for chaining more than two monadic operations; it reads like sequential code while maintaining functional composition.
265- Use `Prelude` static imports (`using static LanguageExt.Prelude`) for ergonomic access to `Some`, `None`, `Left`, `Right`, `Try`, `List`, `Map`, and `Set` factory methods.
266- Use language-ext immutable collections (`Lst<T>`, `Map<K,V>`, `Set<T>`) for domain models that must be thread-safe and mutation-free.
267- Convert between `Option` and `Either` at service boundaries using `ToEither(errorValue)` when you need to add error context to an absent value.
268- Use `Try<T>` and `TryAsync<T>` at infrastructure boundaries (file I/O, HTTP calls) to convert exceptions into monadic values that compose safely.
269- Avoid mixing null-returning APIs with language-ext types in the same layer; establish a boundary where nullables are converted to `Option<T>`.
270- Use `Match` with explicit handlers for all cases to ensure complete handling; avoid `IfSome`/`IfNone` in business logic where both paths need to produce a value.