Functional programming with C#3

Keywords: C# Programming Java

Preface

Today I chatted with someone about LINQ in C#and found that the LINQ I know seems to be different from the LINQ most people know. Why not?In fact, LINQ can also be used to do functional programming.

Of course, not to write a few Lambdas and use stream s like Java, even called LINQ, but LINQ is something else.

LINQ

In C#, I'm sure you've all seen the following LINQ notation:

IEnumerable<int> EvenNumberFilter(IEnumerable<int> list)
{
    return from c in list where c & 1 == 0 select c;
}

The above code uses LINQ's syntax to filter even numbers in a list.

LINQ is just a tool for manipulating collections. If we want our own types to support LINQ syntax, we need to implement IEnumerable <T> for our types, and then we can do that.

Oh, was that so?That's all I understand...

???Oh, my God, of course not!

Actually LINQ has nothing to do with IEnumerable <T>LINQ is just a set of extension methods. It consists mainly of the following methods:

Method Name Method Description
Where Data filtering
Select/SelectMany Data projection
Join/GroupJoin Data Join
OrderBy/ThenBy/OrderByDescending/ThenByDescending Sorting data
GroupBy Data Grouping
......

The above methods correspond to the LINQ keywords: where, select, join, order by, group...

When the compiler compiles C#code, it converts the LINQ syntax to the syntax of the extended method call, for example:

from c in list where c > 5 select c;

Will be compiled to:

list.Where(c => c > 5).Select(c => c);

Another example is:

from x1 in list1 join x2 in list2 on x1.k equals x2.k into g select g.u;

Will be compiled to:

list1.GroupJoin(list2, x1 => x1.k, x2 => x2.k, (x1, g) => g.u);

Another example is:

from x in list orderby x.k1, x.k2, x.k3;

Will be compiled to:

list.OrderBy(x => x.k1).ThenBy(x => x.k2).ThenBy(x => x.k3);

Again:

from c in list1
from d in list2
select c + d;

Will be compiled to:

list1.SelectMany(c => list2, (c, d) => c + d);

Stop and stop!

In addition, compilers always translate LINQ syntax into method calls before compiling, so long as there is a method with the corresponding name, it does not mean that LINQ syntax can be used (escape)

Then you see if this SelectMany is...

SelectMany is Monad

Oh my God, look at this poor SelectMany, isn't that the bind function Monad needs?

Things are getting more and more interesting.

We inherit the spirit of the previous article and write Maybe<T> again.

Maybe<T>

First, let's write an abstract class, Maybe<T>.

First, we add a Select method to it to select the data in Maybe<T>. If it is T, it returns Just<T> and if Nothing<T>, it returns Nothing<T>.Equivalent to our returns function:

public abstract class Maybe<T>
{
    public abstract Maybe<U> Select<U>(Func<T, Maybe<U>> f);
}

Then we implement our Just and Nothing:

public class Just<T> : Maybe<T>
{
    private readonly T value;
    public Just(T value) { this.value = value; }

    public override Maybe<U> Select<U>(Func<T, Maybe<U>> f) => f(value);
    public override string ToString() => $"Just {value}";
}

public class Nothing<T> : Maybe<T>
{
    public override Maybe<U> Select<U>(Func<T, Maybe<U>> _) => new Nothing<U>();
    public override string ToString() => "Nothing";
}

Then we'll implement bind for Maybe -- that is, we'll add a method called SelectMany to Maybe.

public abstract class Maybe<T>
{
    public abstract Maybe<U> Select<U>(Func<T, Maybe<U>> f);

    public Maybe<V> SelectMany<U, V>(Func<T, Maybe<U>> k, Func<T, U, V> s)
        => Select(x => k(x).Select(y => new Just<V>(s(x, y))));
}

At this point, Maybe<T>is done!What, this??So how?The exciting moment is here!

First, let's create a few Maybe<int>:

var x = new Just<int>(3);
var y = new Just<int>(7);
var z = new Nothing<int>();

Then we use LINQ to calculate x + y, x + z:

var u = from x0 in x from y0 in y select x0 + y0;
var v = from x0 in x from z0 in z select x0 + z0;

Console.WriteLine(u);
Console.WriteLine(v);

Output results:

Just 10
Nothing

Perfect!The LINQ above was compiled to:

var u = x.SelectMany(_ => y, (x0, y0) => x0 + y0);
var v = x.SelectMany(_ => z, (x0, z0) => x0 + z0);

In this case, function k is int -> Maybe <int> and function s is (int, int) -> int, which is an addition function.

We don't care about the parameter of function k, it is used as a selector, we just need to make it produce a Maybe<int>, then add the values of two int s using function s, and wrap the results in a Just<int>.

During this process, if either party produces Nothing, the result of subsequent operations will always be Nothing, because Nothing.Select(...) or Nothing.

A little extension

Let's add another Where to this Maybe<T>

public abstract class Maybe<T>
{
    public abstract Maybe<U> Select<U>(Func<T, Maybe<U>> f);

    public Maybe<V> SelectMany<U, V>(Func<T, Maybe<U>> k, Func<T, U, V> s)
        => Select(x => k(x).Select(y => new Just<V>(s(x, y))));

    public Maybe<U> Where(Func<Maybe<T>, bool> f) => f(this) ? this : new Nothing<T>();
}

Then we can play:

var just = from c in x where true select c;
var nothing = from c in x where false select c;

Console.WriteLine(just);
Console.WriteLine(nothing);

Return Just when the condition is met, otherwise return Nothing.The above code will output:

Just 3
Nothing

Smell inside (escape)

Postnote

Subsequent articles in this series will be written on mortgages, and if C# gets a little angry and adds Discriminated Unions, Higher Kinded Generics, and Type Classes features, we'll continue.

Posted by cuboidgraphix on Sat, 28 Mar 2020 10:24:28 -0700