Skip to content

Code generation

Paul Louth edited this page Feb 13, 2020 · 47 revisions

Language-ext provides a number of code generation features to help make working with the functional paradigm easier in C#.

Setup

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.4.0"
                    OutputItemType="CodeGenerationRoslynPlugin"
                    PrivateAssets="all" />
  <PackageReference Include="CodeGeneration.Roslyn.BuildTime" 
                    Version="0.7.5-alpha" 
                    PrivateAssets="all" />
  <DotNetCliToolReference Include="dotnet-codegen" Version="0.7.5-alpha" /></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.

Record / product-types

'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 called New 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
  • 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.
  • 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 union / sum-types

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 an abstract class)
      • operator != (if the union-type is an abstract class)
    • Ordering
      • IComparable<T>
      • IComparable
      • CompareTo(T rhs)
      • CompareTo(object rhs)
      • operator < (if the union-type is an abstract class)
      • operator <= (if the union-type is an abstract class)
      • operator > (if the union-type is an abstract class)
      • operator >= (if the union-type is an abstract class)
    • Hash-code calculation
      • GetHashCode() which uses the [FNV-1a hash algorithm]
  • 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.
  • 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 called New 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
    • 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.
    • 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

Reader monad

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.

RWS monad

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>
{
}

Transformation of immutable types

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);

[With]

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;
    }
}

Transformation of nested immutable types with Lenses

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);

[WithLens]

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 the With method and the associated lenses.

Clone this wiki locally