-
-
Notifications
You must be signed in to change notification settings - Fork 442
Code generation
Language-ext provides a number of code generation features to help make working with the functional paradigm easier in C#.
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.CodeGen" Version="3.3.51"
PrivateAssets="all" />
<PackageReference Include="CodeGeneration.Roslyn.BuildTime"
Version="0.7.5"
PrivateAssets="all" />
<DotNetCliToolReference Include="dotnet-codegen" Version="0.7.5" /></ItemGroup>Obviously, update the
Versionattributes 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.
Example
[Record]
public partial struct Person
{
public readonly string Forename;
public readonly string Surname;
}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
staticmethod calledNewthat constructs the record
- Equality
IEquatable<T>Equals(T rhs)Equals(object rhs)- operator
== - operator
!=
- Ordering
IComparable<T>IComparableCompareTo(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
-
GetObjectDatamethod - You should add
System.ISerializableto 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.ISerializableand 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
nullvalues - Uses
StringBuilderfor optimal performance
-
Withmethod- 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 Surnamewill get a lens field:public static Lens<Person, string> surname
WIP: Create discriminated unions from an interface
Example 1
[Union]
public interface Shape
{
Shape Rectangle(float width, float length);
Shape Circle(float radius);
Shape Prism(float width, float height);
}Example 2
[Union]
public interface Maybe<A>
{
Maybe<A> Just(A value);
Maybe<A> Nothing();
}WIP: Auto-generation of a Reader monad that has a fixed environment
Example 1
[Reader(Env: typeof(IO))]
public partial struct Subsystem<A>
{
}WIP: 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.CodeGenpackage and its dependencies will not be included in your final build - it is purely there to generate the code.
You must however:
- Make the
classpartial - 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 BloggsThis can also be achieved using the Update function:
var person = new Person("Joe", "Bloggs");
var person2 = Person.name.Update(name => name + "l", person); // Joel BloggsThe 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 theWithmethod and the associated lenses.