-
-
Couldn't load subscription status.
- Fork 442
Code generation
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.
WIP: Create records from simple fields or property lists
Example 1
[Record]
public partial struct Person
{
public readonly string Forename;
public readonly 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.