Skip to content

Code generation

Paul Louth edited this page Oct 17, 2024 · 47 revisions

[DEPRECATED]

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

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.

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

Free monads

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.

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.