Functional programming in C#

Josef Starýchfojtů

Who am I ?

Why C# when we have F# ?

  • Lack of (FP) programmers
  • We already have bunch of projects written in C#

What is functional programming ?

Referential transparency

  • When f(5) = 10, then lines 1 and 2 are equivalent
1: 
2: 
var a = f(5);
var a = 10;

What's the benefit ?

  • no suprises on what the function actually does
  • testability
  • no time/state factor => easier reasoning
  • parallelization
  • caching

FP vs OOP

  • hard to define (pure functions vs polymorphism ??)
  • techniques, patterns and so are usually thought to be used within some paradigm
  • let's see those commonly used within FP (and yes, you can use them in OOP as well)

C# as funcional language

  • delegates
  • lambda functions
  • LINQ
  • System.Collections.Immutable
  • pattern matching (switch expression)

C# as more funcional language

Concepts, techniques, patterns

Expressions over statements

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
int result;

if (something) 
{  
    result = 1;
}
else 
{
    result = 2;
}

return result;

Expressions over statements

1: 
return something ? 1 : 2;

Expressions over statements

  • Statements don't return values
  • Statements usually mutate stuff
  • Statements are hard to compose

Expressions over statements

  • We want to get rid of this
1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
int result;
try {
    result = ComputeValue();
} catch (SomeException e) {
    Log(e);
    result = -1;
}
return result;

Immutability

1: 
2: 
var person = new Person(...);
PersonPrinter.Print(person);
  • How do we know that Print did not change person ? (Anybody with a reference can harm us)
  • We don't want any mutable suprises
  • We desire thread-safetiness
  • If it wants to change the person, it has to return a new one and tell us
  • May even be faster than mutating (we don't have to do deep copies)

Immutability

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
public sealed class Person
{
    public Person(PersonName name, MailAddress email)
    {
        Name = name;
        Email = email;
    }

    public PersonName Name { get; }
    public MailAddress Email { get; }

    public Person WithEmail(MailAddress email) =>
        new Person(Name, email);
}
  • Now we know it is safe to pass it as an argument

Algebraic data types

  • "Composition over Inheritance" - OOP Programmer
  • Composable data types
  • Product (N-ary tuple) = AND
  • Coproduct = OR

Product

1: 
2: 
3: 
4: 
5: 
6: 
7: 
public sealed class Point : Product2<int, int>
{
    public Point(int x, int y): base(x, y) {}

    public int X => ProductValue1;
    public int Y => ProductValue2;
}
  • N-ary tuple
  • Basically same as System.Tuple
  • Product1 is useful for type aliasing

Coproduct

1: 
2: 
var reader = new ParagraphReader();
var result = reader.Read(); 
  • How to distinguish between line, paragraph end, document end ?
  • What is the return type ? null for end or "" ?

Coproduct

1: 
2: 
3: 
4: 
5: 
6: 
public sealed class ReaderResult : Coproduct3<Line, ParagraphEnd, DocumentEnd>
{
    public ReaderResult(Line line): base(line) {}
    public ReaderResult(ParagraphEnd paragraphEnd): base(paragraphEnd) {}
    public ReaderResult(DocumentEnd documentEnd): base(documentEnd) {}
}
  • Think of it as non-extensible single level inheritance in one place
  • Forces composition
  • Easier to add behavior, no nead to bloat the class with virtual methods
  • Great for domain modeling, once you know them, you see them everywhere (we will revisit that)

Pattern matching

  • Switch expression
  • Exhaustivenes
1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
match (x, y) with
| (5, _) -> ...
| (_ 12) -> ...
| _ -> ...

match list with
| (head, tail) -> ...
| [x, y, z] -> ...
| _ -> ...

Coproduct + pattern matching example

1: 
2: 
3: 
4: 
5: 
6: 
7: 
var reader = new ParagraphReader();
var result = reader.Read();
var foo = result.Match(
    line => ProcessLine(line),
    paragraphEnd => EndParagraph(),
    end => End()
);
  • If new type of result is added, this won't compile

F# sneak peak

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
type ReaderResult = 
    | Line of string
    | ParagraphEnd
    | DocumentEnd

let f = 
    match read () with
    | Line l -> process l
    | ParagraphEnd -> ...
    | DocumentEnd -> ...

Type safety

Type safety - domain modeling

1: 
2: 
3: 
4: 
5: 
6: 
public sealed class Job
{
    public JobState State { get; } // Pending | InProgress | Finished
    public Percentage Progress { get; } 
    public Json Result { get; }
}
  • What is in Result when it is not finished ?
  • Does it make sense to have progress in pending and finished ? (I have to validate it etc.)

Type safety - domain modeling

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
public sealed class PendingJob {}
public sealed class InProgressJob 
{
    public Percentage Progress { get; }
}
public sealed class FinishedJob 
{
    public Json Result { get; }
}

public sealed class Job : Coproduct3<PendingJob, InProgressJob, FinishedJob>
{
    ...
}

Type safety - domain modeling

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
public sealed class JobState : Coproduct3<PendingJob, InProgressJob, FinishedJob>
{
    ...
}

public sealed class Job
{
    public JobState State { get; }
    public NonEmptyString Name { get; }
    ...
}

Type safety - code deduplication

1: 
2: 
3: 
static Date Create(int y, int m, int d);

static Calendar GetCalendarFor(int m);

Type safety - code deduplication

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
static Date Create(int y, int m, int d)
{
    if (IsNotValidMonth(m)) throw new InvalidMonthException();
    ...
}

static Calendar GetCalendarFor(int m)
{
    if (IsNotValidMonth(m)) throw new InvalidMonthException();
    ...
}

Type safety - code deduplication

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
public sealed class Month : Product1<int>
{
    private Month(int value): base(value) {}

    public static Month Create(int value)
    {
        // Exceptions are not friends with FP, but we will work on that later.
        if (IsNotValidMonth(m)) throw new InvalidMonthException();
        return new Month(value);
    }
}
  • It also keeps the information in one place (domain modeling)

Type safety - code deduplication

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
static Date Create(Year y, Month m, Day d)
{
    ...
}

static Calendar GetCalendarFor(Month m)
{
    ...
}

Type safety - honest signature

1: 
static Person Get(string id)
  • What will this do when person is not found ?

Type safety - honest signature

1: 
2: 
3: 
static Person Get(string id)

static IOption<Person> Get(Guid id)
  • Enforcing invariants on the type level (Guid instead of string and exception)
  • Caller over callee (Caller decides)

Special types - Unit

1: 
2: 
3: 
4: 
5: 
6: 
Func<A> vs Action
Task<T> vs Task

Action<int> -> Func<Unit, int> 
Action -> Func<Unit, Unit>
Task -> Task<Unit>
  • Single value

Special types - Nothing

1: 
2: 
3: 
4: 
public Nothing Foo()
{
    throw new Exception();
}
  • No value at all, cannot be created
  • Its semantic is not supported by C#

Option

1: 
2: 
3: 
4: 
5: 
public T F(int? input, int divider)
{
    var x = input?.Value + 5;
    return x is null || divider == 0 ? 42 : x.Value / divider;
}
  • Noisy code
  • We can forget to check for 0

Option

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
public T F(int? input, int divider)
{
    if (input is null || divider == 0)
    {
        return 42;
    }
    
    return (input.Value + 5) / divider;
}
  • We have to think about all cases, which we won't (cause we forget)

Option

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
IOption<int> ~ int?
IOption<T> = Coproduct2<T, Unit>

public IOption<int> Div(int a, int b) =>
    b.Match(
        0, _ => Option.Empty<int>(),
        _ => Option.Create(a / b)
    );

public int F(IOption<int> input, int divider) =>
    input
        .Map(v => v + 5) // similar to '?.', but much more powerful
        .FlatMap(v => Div(v, divider))
        .GetOrElse(42);
  • Now we have it on one place and therefore never forgot
  • It is clear what we want to do
  • The information is visible in function signature

Option - Wait, you have seen it

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
public IEnumerable<int> SomeF(int a, int b)
{
    yield return a;
    yield return b;
}

public IEnumerable<int> F(IEnumerable<int> inputs, int x) =>
    inputs
        .Select(v => v + 5) // Map
        .SelectMany(v => SomeF(v, x)); // FlatMap

Option - LINQ

1: 
2: 
3: 
4: 
5: 
6: 
7: 
public IOption<int> Add(IOption<int> a, IOption<int> b) =>
    a.FlatMap(valueA => b.Map(valueB => valueA + valueB))

public IOption<int> Add(IOption<int> a, IOption<int> b) =>
    from valueA in a
    from valueB in b
    select valueA + valueB

Error handling

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
public Account SignUp(Credentials creds);
public Email CreateSignUpEmail(Account account);

try 
{
    var account = SignUp(email, password);
    var email = CreateSignUpEmail(account);
    ..
}
catch (AccountAlreadyExistsException)
{
}
catch (InvalidEmailException)
{
}
  • Only comments tell me what to do and we all know how that ends
  • We don't want statements

Error handling

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
ITry<A, E> = Coproduct2<A, E>

public enum SignUpError
{
    AccountAlreadyExists,
    InvalidEmail,
    PasswordTooShort,
    NoConnection
}

public ITry<Account, SignUpError> SignUp(Credentials creds);
public Email CreateSignUpEmail(Account account);

var email = SignUp(email, password)
    .Map(SendEmail);
  • If I need some error data, I can make the enum coproduct
  • Now I cannot forget
  • I have the error strongly typed (Exceptions can also be like that, but they are usually not)

Error handling

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
public ITry<Account, SignUpError> SignUp(Credentials creds);
public ITry<Email, EmailError> CreateSignUpEmail(Account acc);

var email = SignUp(email, password)
    .MapError(CreateCommonError)
    .FlatMap(a => CreateSignUpEmail(a).MapError(CreateCommonError));

public ITry<Account> SignUp(string email, string password);

var email = SignUp(email, password)
    .FlatMap(CreateSignUpEmail);
  • Weakly typed variant
1: 
ITry<A> = Coproduct2<A, IEnumerable<Exception>>

Validation

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
public Person Create(string name, string email)
{
    var exceptions = new List<Exception>();
    if(!TryParseName(name, out var stronglyTypedName))
    {
        exceptions.Add(new InvalidNameException());
    }

    if(!TryParseEmail(email, out var stronglyTypedEmail))
    {
        exceptions.Add(new InvalidEmailException());
    }

    return exceptions.IsEmpty ?
        ? new Person(stronglyTypedName, stronglyTypedEmail)
        : throw new AggregateException(exceptions);
}
  • Duplicates the logic

Validation

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
public ITry<Person, Error[]> Create(string name, string email) =>
    Try.Aggregate(
        CreateName(name),
        CreateEmail(email)
        (n, e) => new Person(n, e)
    );

private ITry<Email, Error[]> CreateEmail(string email) => ...
private ITry<Name, Error[]> CreateName(string name) =>
    name.IsNullOrEmpty
        ? Try.Error<Name, Error[]>(Error.NameCannotBeEmpty)
        : Try.Success<Name, Error[]>(new Name(name));

Example

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
public Person GetById(Guid id);
public void DoSomething(Person person);

var person = PersonRepository.GeById(id);
if (person is null)
    return HttpNotFound();

try 
{
    PersonService.DoSomething(person);
}
catch (SomePersonException e)
{
    return HttpBadRequest();
}
return HttpOk();

More functional example

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
public IOption<Person> GetById(Guid id);
public ITry<Unit> DoSomething(Person person);

var person = PersonRepository.GeById(id);
var result = person.Map(p => PersonService.DoSomething(p));
return result.Match(
    some => some.Match(
            success => HttpOk(),
            error => HttpBadRequest()
        ),
    _ => HttpNotFound()
);

From top to bottom

  • So how do these techniques evolve into whole functional app ?
  • Functional application is just a bunch of function in modules combined to bigger ones.
  • But real world apps have side effects, what to do ?
  • Try to limit them.
  • Simple way: unpure-pure-unpure sandwitch. (Not always applicable)
1: 
2: 
3: 
var data = FetchData();
var result = PureFunction(data);
var probablyUnit = Save(result);

Advanced IO

1: 
2: 
3: 
4: 
5: 
6: 
7: 
public IO<Recipe> GetRecipe(Environment environment, Guid id);
public IO<User> GetUser(Environment environment, Guid id);

public Something Foo(Guid id, Guid otherId) =>
    from recipe in GetRecipe(environment, id);
    from user in GetRecipe(environment, userId);
    return DoSomething(recipe, user);
  • Makes explicit that it is IO
  • Should be still as high as possible in code
  • callee cannot perform the IO, caller always knows about it!

Reader

1: 
2: 
3: 
4: 
5: 
6: 
7: 
public Reader<Environment, Recipe> GetRecipe(Guid id);
public Reader<Environment, User> GetUser(Guid id);

public Something Foo(Guid id, Guid otherId) =>
    from recipe in GetRecipe(id)
    from user in GetUser(otherId)
    select DoSomething(recipe, user);

Reader to the next level

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
public IO<E, Something> Foo(Guid id, Guid otherId)
    where E: IRecipeProvider, IUserProvider 
{
    return
        from recipe in GetRecipe(id)
        from user in GetUser(otherId)
        select DoSomething(recipe, user);
}

Alternative IO - Free Monad

  • Program as data structure
  • Data structure gets interpreted, we are building the execution tree

    [lang=cs] public Program Foo(Guid id, Guid otherId) { return from recipe in GetRecipe(id) from user in GetUser(otherId) select DoSomething(recipe, user); }

Links

Thanks for your attention