GitHub Repository
Previously: Countable and uncountable sets in .NET (clean version).
Thesis
There is a full featured support of countable sets in .NET: IEnumerable<T>
. What about uncountable sets; sets defined by predicate? How can they be manipulated and interact with IEnumerable<T>
?
Solution
Let;s introduce two library classes: Universe
and Set
, where Universe
is a factory of Sets
and Sets
are defined by predicate, condition like Func<T, bool>
.
Example:
using static Universe;
Set<int> integers = Set<int>();
Set<int> zero = Set<int>(i => i == 0);
Set<int> positive = Set<int>(i => i > 0);
We define some basic calculus on sets:
Set<int> nonPositive = !positive;
Set<int> negative = !positive – zero;
Set<int> nonZero = positive | negative;
Intersection:
Set<int> liquidFreshWaterC = Universe.Set<int>(t => t > 0 && t < 100);
Set<int> liquidSaltWaterC = Universe.Set<int>(t => t > -21.1 && t < 102);
Set<int> liquidWaterC = liquidFreshWaterC & liquidSaltWaterC; // = 0 ... 100
Now, how to test the set (it is just a combined condition underneath, nothing else) – let’s use intersection operator, as scalar value is just a set of one element:
bool isLiquidWater = liquidWaterC & 25; // = true
Union operator provides us with an another set:
Set<int> temperatures = liquidWaterC | 200; // 0 ... 100, 200
The most useful feature is an integration with IEnumerable<T>
. Let’s have:
Int[] tempC = new[] {-100, -10, 0, 10, 100, 200};
We can inersect them:
Enumerable<int> t = temperatures & tempC; // = 0, 10, 100, 200
Please note that non empty Enumerable<T>
is truthy; an empty one is falsy:
bool nonEmpty = t; // true
We can join them, so result will be another Set<T>
:
Set<T> joined = temperatures & tempC; // -100, -10, 0 ... 100, 200
We can even exclude set from enumeration getting an enumeration, or exclude enumeration from set – getting set as a result.
Demo
Let's define Customer
, Order
, Invoice
to calculate discounts (full solution is available online to play with):
class Customer
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
public List<Invoice> Invoices { get; set; }
}
class Order
{
public decimal Total { get; set; }
}
class Invoice
{
public decimal Total { get; set; }
}
Now helpers:
static class Balances
{
public static decimal Invoiced(this Customer customer) =>
customer.Invoices.Sum(i => i.Total);
public static decimal Ordered(this Customer customer) =>
customer.Orders.Sum(o => o.Total);
public static decimal Balance(this Customer customer) =>
customer.Ordered() - customer.Invoiced();
}
Our discount rules are going to be:
var loyal = Set<Customer>(с => с.Invoiced() > 10000);
var debtors = Set<Customer>(c => c.Balance() > 0);
var creditable = Set<Customer>(c => c.Balance() < 5000) & loyal;
var bulk = Set<Customer>(c => c.Orders.Any(o => o.Total > 2000));
// I think this syntax makes total sense
var tenOff = bulk & loyal & !debtors;
var fiveOff = bulk & loyal & creditable - tenOff;
var noOff = !tenOff & !fiveOff;
Let’s test the sets:
foreach (var c in Repository.Customers & fiveOff)
WriteLine($"-5%: {c.Name}");
foreach (var c in Repository.Customers & tenOff)
WriteLine($"-10%: {c.Name}");
foreach (var c in Repository.Customers & noOff)
WriteLine($"0%: {c.Name}");
Library code:
static class Universe
{
public static Set<T> Set<T>() => Set<T>(i => true);
public static Set<T> Set<T>(Predicate<T> predicate) => new Set<T>(predicate);
}
class Set<T>
{
public Set(Predicate<T> predicate)
{
Predicate = predicate;
}
public static Enumerable<T> operator &(Set<T> left, T right) =>
left.Predicate(right) ? new Enumerable<T>(right) : Enumerable<T>.Empty;
public static Enumerable<T> operator &(T left, Set<T> right) =>
right.Predicate(left) ? new Enumerable<T>(left) : Enumerable<T>.Empty;
public static Set<T> operator &(Set<T> left, Set<T> right) =>
new Set<T>(i => left.Predicate(i) && right.Predicate(i));
public static Enumerable<T> operator &(Set<T> left, IEnumerable<T> right) =>
new Enumerable<T>(right.Where(i => left.Predicate(i)));
public static Enumerable<T> operator &(IEnumerable<T> left, Set<T> right) =>
new Enumerable<T>(left.Where(i => right.Predicate(i)));
public static Set<T> operator |(Set<T> left, T right) =>
new Set<T>(i => left.Predicate(i) || right.Equals(i));
public static Set<T> operator |(T left, Set<T> right) =>
new Set<T>(i => left.Equals(i) || right.Predicate(i));
public static Set<T> operator |(Set<T> left, Set<T> right) =>
new Set<T>(i => left.Predicate(i) || right.Predicate(i));
public static Set<T> operator |(Set<T> left, IEnumerable<T> right) =>
new Set<T>(i => left.Predicate(i) || right.Contains(i));
public static Set<T> operator |(IEnumerable<T> left, Set<T> right) =>
new Set<T>(i => left.Contains(i) || right.Predicate(i));
public static Set<T> operator -(Set<T> left, T right) =>
new Set<T>(i => left.Predicate(i) && !right.Equals(i));
public static Set<T> operator -(T left, Set<T> right) =>
new Set<T>(i => left.Equals(i) && !right.Predicate(i));
public static Set<T> operator -(Set<T> left, Set<T> right) =>
new Set<T>(i => left.Predicate(i) && !right.Predicate(i));
public static Set<T> operator -(Set<T> left, IEnumerable<T> right) =>
new Set<T>(i => left.Predicate(i) && !right.Contains(i));
public static Enumerable<T> operator -(IEnumerable<T> left, Set<T> right) =>
new Enumerable<T>(left.Where(i => !right.Predicate(i)));
public static Set<T> operator !(Set<T> set) =>
new Set<T>(i => !set.Predicate(i));
Predicate <T> Predicate { get; }
}
class Enumerable<T> : IEnumerable<T>
{
public static readonly Enumerable<T> Empty = new Enumerable<T>();
public static implicit operator bool(Enumerable<T> intersection) => intersection.Any();
public Enumerable(params T[] items)
{
Items = items;
}
public Enumerable(IEnumerable<T> items)
{
Items = items;
}
public IEnumerator<T> GetEnumerator() => Items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
IEnumerable<T> Items { get; }
}
1 Answer 1
REVISED VERSION
GitHub Repository
==========================================================================
Previously: Countable and uncountable sets in .NET (clean version).
Thesis There is a full featured support of countable sets in .NET: IEnumerable<T>
. What about uncountable sets; sets defined by predicate? How can they be manipulated and interact with IEnumerable<T>
?
What is new? Operators has been unified:
1) Implicit conversion defined:
- from
IEnumerable<T>
toSet<T>
=>Set<T>(i => source.Contains(i))
- from
T value
toSet<T>
=>Set<T>(i == value)
2) Operators redefined to ensure symmetry and simplicity:
// tests an element,
// we can iterate over result or implicitly cast it to Boolean.
Set<T>[T value] => Enumerable<T>
Set<T>[IEnumerable<T> sequence] => Enumerable<T>
// Set manipulations
Set<T> & Set<T> => Set<T>
Set<T> | Set<T> => Set<T>
Set<T> - Set<T> => Set<T> // redundant, but so, so useful
!Set<T> => Set<T>
Solution Let’s introduce two library classes: Universe
and Set
, where Universe
is a factory of Sets
and Sets
are defined by predicate, condition like Func<T, bool>
. Example:
using static Universe;
Set<int> integers = Universe.Set<int>();
Set<int> zero = Universe.Set<int>(i => i == 0);
Set<int> positive = Universe.Set<int>(i => i > 0);
We define some basic calculus on sets:
Set<int> nonPositive = !positive;
Set<int> negative = !positive – zero;
Set<int> nonZero = positive | negative;
Intersection:
Set<int> liquidFreshWaterC = Universe.Set<int>(t => t > 0 && t < 100);
Set<int> liquidSaltWaterC = Universe.Set<int>(t => t > -21.1 && t < 102);
Set<int> liquidWaterC = liquidFreshWaterC & liquidSaltWaterC; // = 0 ... 100
Now, how to test the set (it is just a combined condition underneath, nothing else) – let’s use intersection operator, as scalar value is just a set of one element:
bool isLiquidWater = liquidWaterC[25]; // = true
Actually, tests return Enumerable<T>
, which is truthy; it could be falsy if empty. We can iterate the result, getting 0 or 1 element.
Union operator provides us with an another set:
Set<int> temperatures = liquidWaterC | 200; // 0 ... 100, 200
The most useful feature is an integration with IEnumerable<T>
. Let’s have:
Int[] tempC = new[] {-100, -10, 0, 10, 100, 200};
We can inersect them:
Set<int> t = temperatures & tempC; // = 0, 10, 100, 200
We can join them, so result will be another Set<T>
:
Set<T> joined = temperatures & tempC; // -100, -10, 0 ... 100, 200
We can even exclude set from enumeration getting an enumeration, or exclude enumeration from set – getting set as a result.
Demo Let’s define Customer
, Order
, Invoice
to calculate discounts (full solution is available online to play with):
class Customer
{
public string Name { get; set; }
public List<Order> Orders { get; set; }
public List<Invoice> Invoices { get; set; }
}
class Order
{
public decimal Total { get; set; }
}
class Invoice
{
public decimal Total { get; set; }
}
Now helpers:
static class Balances
{
public static decimal Invoiced(this Customer customer) =>
customer.Invoices.Sum(i => i.Total);
public static decimal Ordered(this Customer customer) =>
customer.Orders.Sum(o => o.Total);
public static decimal Balance(this Customer customer) =>
customer.Ordered() - customer.Invoiced();
}
Our discount rules are going to be:
var loyal = Set<Customer>(с => с.Invoiced() > 10000);
var debtors = Set<Customer>(c => c.Balance() > 0);
var creditable = Set<Customer>(c => c.Balance() < 5000) & loyal;
var bulk = Set<Customer>(c => c.Orders.Any(o => o.Total > 2000));
var tenOff = bulk & loyal & !debtors;
var fiveOff = bulk & loyal & creditable - tenOff;
var noOff = !tenOff & !fiveOff;
Let’s test the sets:
foreach (var c in Repository.Customers.Intersect(fiveOff))
WriteLine($"-5%: {c.Name}");
foreach (var c in Repository.Customers.Intersect(tenOff))
WriteLine($"-10%: {c.Name}");
foreach (var c in Repository.Customers.Intersect(noOff))
WriteLine($"0%: {c.Name}");
Library code:
static class Universe
{
public static Set<T> Set<T>() => Set<T>(i => true);
public static Set<T> Set<T>(Predicate<T> predicate) => new Set<T>(predicate);
public static Enumerable<T> Intersect<T>(this IEnumerable<T> source, Set<T> set) =>
set[source];
}
class Set<T>
{
public static implicit operator Set<T>(T value) =>
new Set<T>(value);
public Set(params T[] values)
: this(values.AsEnumerable())
{
}
public Set(IEnumerable<T> values)
: this(i => values.Contains(i))
{
}
public Set(Predicate<T> predicate)
{
Predicate = predicate;
}
public Enumerable<T> this[T value] =>
Predicate(value) ? new Enumerable<T>(value) : Enumerable<T>.Empty;
public Enumerable<T> this[IEnumerable<T> values] =>
new Enumerable<T>(values.Where(i => Predicate(i)));
public static Set<T> operator &(Set<T> left, Set<T> right) =>
new Set<T>(i => left.Predicate(i) && right.Predicate(i));
public static Set<T> operator &(Set<T> left, IEnumerable<T> right) =>
left & new Set<T>(right);
public static Set<T> operator &(IEnumerable<T> left, Set<T> right) =>
new Set<T>(left) & right;
public static Set<T> operator |(Set<T> left, Set<T> right) =>
new Set<T>(i => left.Predicate(i) || right.Predicate(i));
public static Set<T> operator |(Set<T> left, IEnumerable<T> right) =>
left | new Set<T>(right);
public static Set<T> operator |(IEnumerable<T> left, Set<T> right) =>
new Set<T>(left) | right;
public static Set<T> operator -(Set<T> left, Set<T> right) =>
new Set<T>(i => left.Predicate(i) && !right.Predicate(i));
public static Set<T> operator -(Set<T> left, IEnumerable<T> right) =>
left - new Set<T>(right);
public static Set<T> operator -(IEnumerable<T> left, Set<T> right) =>
new Set<T>(left) - right;
public static Set<T> operator !(Set<T> set) =>
new Set<T>(i => !set.Predicate(i));
Predicate <T> Predicate { get; }
}
class Enumerable<T> : IEnumerable<T>
{
public static readonly Enumerable<T> Empty = new Enumerable<T>();
public static implicit operator bool(Enumerable<T> intersection) => intersection.Any();
public Enumerable(params T[] items)
{
Items = items;
}
public Enumerable(IEnumerable<T> items)
{
Items = items;
}
public IEnumerator<T> GetEnumerator() => Items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
IEnumerable<T> Items { get; }
}
temperatures & tempC;
results in an object of typEnumerable<int>
first, and than in an object of typeSet<T>
\$\endgroup\$Enumerable<T>
... \$\endgroup\$