-
-
Notifications
You must be signed in to change notification settings - Fork 422
Code generation
Language-ext provides a number of code generation features to help make working with the functional paradigm easier in C#.
"Code is a liability but also an asset, if we reduce the code whilst maintaining the same functionality, we reduce the liabilities and keep the assets" - Paul Louth
- Setup
- Records / Product-types
- Discriminated unions / Sum-types
Free
monadsReader
monadRWS
monad- Transformation of Immutable Types
- Transformation of nested immutable types with Lenses
To use the code-generation features of language-ext (which are totally optional by the way), then you must include the LanguageExt.CodeGen package into your project.
To make the reference build and design time only (i.e. your project doesn't gain an additional dependencies because of the code-generator), open up your csproj
and set the PrivateAssets
attribute to all
:
<ItemGroup>
<PackageReference Include="LanguageExt.Core" Version="3.4.10" />
<PackageReference Include="LanguageExt.CodeGen" Version="3.4.10"
PrivateAssets="all" />
<PackageReference Include="CodeGeneration.Roslyn.BuildTime"
Version="0.6.1"
PrivateAssets="all" />
<DotNetCliToolReference Include="dotnet-codegen" Version="0.6.1" />
</ItemGroup>
Obviously, update the
Version
attributes to the appropriate values. Also note that you will probably need the latest VS2019+ for this to work. Even early versions of VS2019 seem to have problems.
'Records' are pure data-types that are usually immutable. They contain either readonly
fields or properties with { get; }
accessors-only. A record acts like a value - like DateTime
in the .NET Framework. And because of that can have equality, ordering, and hash-code implementations provided automatically. The code-generation will work with either struct
or class
types.
Example
[Record]
public partial struct Person
{
public readonly string Forename;
public readonly string Surname;
}
Click here to see the generated code
The [Record]
code-generator provides the following features:
- Construction / Deconstructor
- A constructor that takes all of the fields/properties and sets them
- A deconstructor that allows for easy access to the individual fields/properties of the record
- A
static
method calledNew
that constructs the record
- Equality
IEquatable<T>
Equals(T rhs)
Equals(object rhs)
- operator
==
- operator
!=
- Ordering
IComparable<T>
IComparable
CompareTo(T rhs)
CompareTo(object rhs)
- operator
<
- operator
<=
- operator
>
- operator
>=
- Hash-code calculation
-
GetHashCode()
which uses the FNV-1a hash algorithm
-
- Serialisation
- Adds the
[System.Serializable]
attribute - Serialisation constructor
-
GetObjectData
method - You should add
System.ISerializable
to leverage this- The reason it's not added by default is it doesn't play nice with Json.NET, so if you use Json.NET don't derive from
System.ISerializable
and everything will just work.
- The reason it's not added by default is it doesn't play nice with Json.NET, so if you use Json.NET don't derive from
- Adds the
-
ToString
- Provides a default implementation that shows the record-name followed by the field name/value pairs.
- Gracefully handles
null
values - Uses
StringBuilder
for optimal performance
-
With
method- Allows for transformation (generation of a new record based on the old one) by provision of just the fields/properties you wish to transform.
- i.e.
person.With(Surname: "Smith")
- Lenses
- Provides lower-case variants to the fields/properties that are lenses
- Lenses allow composition of property/field transformation so that nested immutable types can be transformed easily
- So the field
public readonly string Surname
will get a lens field:public static Lens<Person, string> surname
Discriminated unions/sum-types are like enums in that they can be in one on N states (often called cases). Except each case can have attributes like a Record. They contain either readonly
fields or properties with { get; }
accessors-only. A case acts like a value - like DateTime
in the .NET Framework. And because of that can have equality, ordering, and hash-code implementations provided automatically.
The type of the [Union]
can either be an interface
or an abstract class
. If an abstract class
is used then the type will gain operators for equality and ordering.
Example 1
[Union]
public interface Shape
{
Shape Rectangle(float width, float length);
Shape Circle(float radius);
Shape Prism(float width, float height);
}
// you can use C# pattern matching like F#
public double GetArea(Shape shape)
=> shape switch
{
Rectangle rec => rec.Length * rec.Width,
Circle circle => 2 * Math.PI * circle.Radius,
_ => throw new NotImplementedException()
};
Example 2
[Union]
public interface Maybe<A>
{
Maybe<A> Just(A value);
Maybe<A> Nothing();
}
Click here to see the generated code
Example 3
[Union]
public abstract partial class Shape<NumA, A> where NumA : struct, Num<A>
{
public abstract Shape<NumA, A> Rectangle(A width, A length);
public abstract Shape<NumA, A> Circle(A radius);
public abstract Shape<NumA, A> Prism(A width, A height);
}
Click here to see the generated code
The [Union]
code-generator provides the following features:
- Addition of extra members to the union-type
- Equality
IEquatable<T>
Equals(T rhs)
Equals(object rhs)
- operator
==
(if the union-type is anabstract class
) - operator
!=
(if the union-type is anabstract class
)
- Ordering
IComparable<T>
IComparable
CompareTo(T rhs)
CompareTo(object rhs)
- operator
<
(if the union-type is anabstract class
) - operator
<=
(if the union-type is anabstract class
) - operator
>
(if the union-type is anabstract class
) - operator
>=
(if the union-type is anabstract class
)
- Hash-code calculation
-
GetHashCode()
which uses the [FNV-1a hash algorithm]
-
- Equality
- Provision of a
static
type which provides factory functions for generating the cases- If the union type has generic arguments then the
static
factory type will have the same name without the generic parameters - If the union type doesn't have generic arguments thene the
static
factory type will be called*Con
where the*
is the name of the union type.
- If the union type has generic arguments then the
- Provision of one case-type for each method in the union-type. The case-type will have the following:
- A class that derives from the union-type
- Construction / Deconstructor
- A constructor that takes all of the fields/properties and sets them
- A deconstructor that allows for easy access to the individual fields/properties of the case
- A
static
method calledNew
that constructs the case - Equality
IEquatable<T>
Equals(T rhs)
Equals(object rhs)
- operator
==
- operator
!=
- Ordering
IComparable<T>
IComparable
CompareTo(T rhs)
CompareTo(object rhs)
- operator
<
- operator
<=
- operator
>
- operator
>=
- Hash-code calculation
-
GetHashCode()
which uses the FNV-1a hash algorithm
-
- Serialisation
- Adds the
[System.Serializable]
attribute - Serialisation constructor
-
GetObjectData
method - You should add
System.ISerializable
to leverage this- The reason it's not added by default is it doesn't play nice with Json.NET, so if you use Json.NET don't derive from
System.ISerializable
and everything will just work.
- The reason it's not added by default is it doesn't play nice with Json.NET, so if you use Json.NET don't derive from
- Adds the
-
ToString
- Provides a default implementation that shows the case-name followed by the field name/value pairs.
- Gracefully handles
null
values - Uses
StringBuilder
for optimal performance
-
With
method- Allows for transformation (generation of a new case based on the old one) by provision of just the fields/properties you wish to transform.
- i.e.
person.With(Surname: "Smith")
- Lenses
- Provides lower-case variants to the fields/properties that are lenses
- Lenses allow composition of property/field transformation so that nested immutable types can be transformed easily
- So the field
public readonly string Surname
will get a lens field:public static Lens<Person, string> surname
Free monads allow the programmer to take a functor and turn it into a monad for free.
The [Free]
code-gen attribute provides this functionality in C#.
Below, is a the classic example of a Maybe
type (also known as Option
, here we're using the Haskell naming parlance to avoid confusion with the language-ext type).
[Free]
public interface Maybe<A>
{
[Pure] A Just(A value);
[Pure] A Nothing();
public static Maybe<B> Map<B>(Maybe<A> ma, Func<A, B> f) => ma switch
{
Just<A>(var x) => Maybe.Just(f(x)),
_ => Maybe.Nothing<B>()
};
}
Click here to see the generated code
The Maybe<A>
type can then be used as a monad:
var ma = Maybe.Just(10);
var mb = Maybe.Just(20);
var mn = Maybe.Nothing<int>();
var r1 = from a in ma
from b in mb
select a + b; // Just(30)
var r2 = from a in ma
from b in mb
from _ in mn
select a + b; // Nothing
And so, in 11 lines of code, we have created a Maybe
monad that captures the short-cutting behaviour of Nothing
.
But, actually, it's possible to do this in fewer lines of code:
[Free]
public interface Maybe<A>
{
[Pure] A Just(A value);
[Pure] A Nothing();
}
If you don't need to capture bespoke rules in the Map
function, the code-gen will build it for you.
A monad, a functor, and a discriminated union in 6 lines of code. Nice.
As with the discriminated-unions, [Free]
types allow for deconstructing the values when pattern-maching:
var txt = ma switch
{
Just<int> (var x) => $"Value is {x}",
_ => "No value"
};
The type 'behind' a free monad (in Haskell or Scala for example) usually has one of two cases:
Pure
Free
Pure
is what we've used so far, and that's why Just
and Nothing
had the Pure
attribute before them:
[Pure] A Just(A value);
[Pure] A Nothing();
They can be considered terminal values. i.e. just raw data, nothing else. The code generated works in exactly the same way as the common types in language-ext, like Option
, Either
, etc. However, if the [Pure]
attribute is left off the method-declaration then we gain an extra field in the generated case type: Next
.
Next
is a Func<*, M<A>>
- the *
will be the return type of the method-declaration.
For example:
[Free]
public interface FreeIO<T>
{
[Pure] T Pure(T value);
[Pure] T Fail(Error error);
string ReadAllText(string path);
Unit WriteAllText(string path, string text);
}
Click here to see the generated code
If we look at the generated code for the ReadAllText
case (which doesn't have a [Pure]
attribute), then we see that the return type of string
has now been injected into this additional Next
function which is provided as the last argument.
public sealed class ReadAllText<T> : FreeIO<T>, System.IEquata...
{
public readonly string Path;
public readonly System.Func<string, FreeIO<T>> Next;
public ReadAllText(string Path, System.Func<string, FreeIO<T>> Next)
{
this.Path = Path;
this.Next = Next;
}
Why is all this important? Well, it allows for actions to be chained together into a continuations style structure. This is useful for building a sequence of actions, very handy for building DSLs.
var dsl = new ReadAllText<Unit>("I:\\temp\\test.txt",
txt => new WriteAllText<Unit>("I:\\temp\\test2.txt", txt,
_ => new Pure<Unit>(unit)));
You should be able to see now why the [Pure]
types are terminal values. They are used at the end of the chain of continuations to signify a result.
But that's all quite ugly, so we can leverage the monadic aspect of the type:
var dsl = from t in FreeIO.ReadAllText("I:\\temp\\test.txt")
from _ in FreeIO.WriteAllText("I:\\temp\\test2.txt", t)
select unit;
The continuation itself doesn't do anything, it's just a pure data-structure representing the actions of the DSL. And so, we need an interpreter to run it (which you write). This is a simple example:
public static Either<Error, A> Interpret<A>(FreeIO<A> ma) => ma switch
{
Pure<A> (var value) => value,
Fail<A> (var error) => error,
ReadAllText<A> (var path, var next) => Interpret(next(Read(path))),
WriteAllText<A> (var path, var text, var next) => Interpret(next(Write(path, text))),
};
static string Read(string path) =>
File.ReadAllText(path);
static Unit Write(string path, string text)
{
File.WriteAllText(path, text);
return unit;
}
We can then run it by passing it the FreeIO<A>
value:
var result = Interpret(dsl);
Notice how the result type of the interpreter is Either
. We can use any result type we like, for example we could make the interpreter asynchronous:
public static async Task<A> InterpretAsync<A>(FreeIO<A> ma) => ma switch
{
Pure<A> (var value) => value,
Fail<A> (var error) => await Task.FromException<A>(error),
ReadAllText<A> (var path, var next) => await InterpretAsync(next(await File.ReadAllTextAsync(path))),
WriteAllText<A> (var path, var text, var next) => await InterpretAsync(next(await File.WriteAllTextAsync(path, text).ToUnit())),
};
Which can be run in a similar way, but asynchronously:
var res = await InterpretAsync(dsl);
And so, the implementation of the interpreter is up to you. It can also take extra arguments so that state can be carried through the operations. In fact it's very easy to use the interpreter to bury all the messy stuff of your application (the IO, maybe some ugly state management, etc.) in one place. This then allows the code itself (that works with the free-monad) to be referentialy transparent.
Another trick is to create a mock interpreter for unit-testing code that uses IO without having to ever do real IO. The logic gets tested, which is what is often the most important aspect of unit testing, but not real IO occurs. The arguments to the interpreter can be the mocked state.
Some caveats though:
- The recursive nature of the interpreter means large operations could blow the stack. This can be dealt with using a functional co-routines/trampolining trick, but that's beyond the scope of this doc.
- Although it's the perfect abstraction for IO, it does come with some additional performance costs. Generating the DSL before interpreting it is obviously not as efficient as directly calling the IO functions.
Caveats aside, the free-monad allows for complete abstraction from side-effects, and makes all operations pure. This is incredibly powerful.
A Reader monad is a monad that carries with it an environment. The environment is used to carry some external state through the monadic computation. This state can either be values or functions. This is often how settings are injected into a pure computation or even how dependency-injection is done in the functional world (if the state contains functions).
The [Reader(Env)]
code-gen wraps up the language-ext built-in Reader
monad. So, instead of having to type Reader<Env, A>
the Env
can be baked-in to the wrapper. This makes using the type much easier.
The code-gen also looks for methods in the Env
type and then adds them as regular methods to the new wrapper type. This makes working with injected functionality much, much simpler.
Example 1
[Reader(Env: typeof(IO))]
public partial struct Subsystem<A>
{
}
Click here to see the generated code
This example injects all the IO functionality into the Subsystem<A>
type.
Here's an example IO
type:
public interface IO
{
Seq<string> ReadAllLines(string fileName);
Unit WriteAllLines(string fileName, Seq<string> lines);
Person ReadFromDB();
int Zero { get; }
}
This can then be used in a LINQ expression directly:
var comp = from ze in Subsystem.Zero
from ls in Subsystem.ReadAllLines("c:/test.txt")
from _ in Subsystem.WriteAllLines("c:/test-copy.txt", ls)
select ls.Count;
And run with an injected implementation, which could be the real IO methods or mocked ones:
var res = comp.Run(new RealIO()).IfFail(0);
And so the [Reader]
code-gen is your own personal monad builder that leverages the existing power of the Reader
monad built into language-ext.
DOC-WIP: Document Reader/Writer/State monad generator
Example 1
[RWS(WriterMonoid: typeof(MSeq<string>),
Env: typeof(IO),
State: typeof(Person),
Constructor: "Pure",
Fail: "Error" )]
public partial struct Subsys<T>
{
}
If you're writing functional code you should treat your types as values. Which means they should be immutable. One common way to do this is to use readonly
fields and provide a With
function for mutation. i.e.
public class A
{
public readonly X X;
public readonly Y Y;
public A(X x, Y y)
{
X = x;
Y = y;
}
public A With(X X = null, Y Y = null) =>
new A(
X ?? this.X,
Y ?? this.Y
);
}
Then transformation can be achieved by using the named arguments feature of C# thus:
val = val.With(X: x);
val = val.With(Y: y);
val = val.With(X: x, Y: y);
It can be quite tedious to write the With
function however. And so, if you include the LanguageExt.CodeGen
nu-get package in your solution you gain the ability to use the [With]
attribtue on a type. This will build the With
method for you.
NOTE: The
LanguageExt.CodeGen
package and its dependencies will not be included in your final build - it is purely there to generate the code.
You must however:
- Make the
class
partial
- Have a constructor that takes the fields in the order they are in the type
- The names of the arguments should be the same as the field, but with the first character lower-case
i.e.
[With]
public partial class A
{
public readonly X X;
public readonly Y Y;
public A(X x, Y y)
{
X = x;
Y = y;
}
}
One of the problems with immutable types is trying to transform something nested deep in several data structures. This often requires a lot of nested With
methods, which are not very pretty or easy to use.
Enter the Lens<A, B>
type.
Lenses encapsulate the getter and setter of a field in an immutable data structure and are composable:
[With]
public partial class Person
{
public readonly string Name;
public readonly string Surname;
public Person(string name, string surname)
{
Name = name;
Surname = surname;
}
public static Lens<Person, string> name =>
Lens<Person, string>.New(
Get: p => p.Name,
Set: x => p => p.With(Name: x));
public static Lens<Person, string> surname =>
Lens<Person, string>.New(
Get: p => p.Surname,
Set: x => p => p.With(Surname: x));
}
This allows direct transformation of the value:
var person = new Person("Joe", "Bloggs");
var name = Person.name.Get(person);
var person2 = Person.name.Set(name + "l", person); // Joel Bloggs
This can also be achieved using the Update
function:
var person = new Person("Joe", "Bloggs");
var person2 = Person.name.Update(name => name + "l", person); // Joel Bloggs
The power of lenses really becomes apparent when using nested immutable types, because lenses can be composed. So, let's first create a Role
type which will be used with the Person
type to represent an employee's job title and salary:
[With]
public partial class Role
{
public readonly string Title;
public readonly int Salary;
public Role(string title, int salary)
{
Title = title;
Salary = salary;
}
public static Lens<Role, string> title =>
Lens<Role, string>.New(
Get: p => p.Title,
Set: x => p => p.With(Title: x));
public static Lens<Role, int> salary =>
Lens<Role, int>.New(
Get: p => p.Salary,
Set: x => p => p.With(Salary: x));
}
[With]
public partial class Person
{
public readonly string Name;
public readonly string Surname;
public readonly Role Role;
public Person(string name, string surname, Role role)
{
Name = name;
Surname = surname;
Role = role;
}
public static Lens<Person, string> name =>
Lens<Person, string>.New(
Get: p => p.Name,
Set: x => p => p.With(Name: x));
public static Lens<Person, string> surname =>
Lens<Person, string>.New(
Get: p => p.Surname,
Set: x => p => p.With(Surname: x));
public static Lens<Person, Role> role =>
Lens<Person, Role>.New(
Get: p => p.Role,
Set: x => p => p.With(Role: x));
}
We can now compose the lenses within the types to access the nested fields:
var cto = new Person("Joe", "Bloggs", new Role("CTO", 150000));
var personSalary = lens(Person.role, Role.salary);
var cto2 = personSalary.Set(170000, cto);
Typing the lens fields out every time is even more tedious than writing the With
function, and so there is code generation for that too: using the [WithLens]
attribute. Next, we'll use some of the built-in lenses in the Map
type to access and mutate a Appt
type within a map:
[WithLens]
public partial class Person : Record<Person>
{
public readonly string Name;
public readonly string Surname;
public readonly Map<int, Appt> Appts;
public Person(string name, string surname, Map<int, Appt> appts)
{
Name = name;
Surname = surname;
Appts = appts;
}
}
[WithLens]
public partial class Appt : Record<Appt>
{
public readonly int Id;
public readonly DateTime StartDate;
public readonly ApptState State;
public Appt(int id, DateTime startDate, ApptState state)
{
Id = id;
StartDate = startDate;
State = state;
}
}
public enum ApptState
{
NotArrived,
Arrived,
DNA,
Cancelled
}
So, here we have a Person
with a map of Appt
types. And we want to update an appointment state to be Arrived
:
// Generate a Person with three Appts in a Map
var person = new Person("Paul", "Louth", Map(
(1, new Appt(1, DateTime.Parse("1/1/2010"), ApptState.NotArrived)),
(2, new Appt(2, DateTime.Parse("2/1/2010"), ApptState.NotArrived)),
(3, new Appt(3, DateTime.Parse("3/1/2010"), ApptState.NotArrived))));
// Local function for composing a new lens from 3 other lenses
Lens<Person, ApptState> setState(int id) =>
lens(Person.appts, Map<int, Appt>.item(id), Appt.state);
// Transform
var person2 = setState(2).Set(ApptState.Arrived, person);
Notice the local-function which takes an ID and uses that with the item
lens in the Map
type to mutate an Appt
. Very powerful stuff.
There are a number of useful lenses in the collection types that can do common things like mutate by index, head, tail, last, etc.
NOTE:
[WithLens]
and[With]
are not necessary when using[Union]
or[Record]
as those code-gen attributes auto-generate theWith
method and the associated lenses.