A lecture about type expressiveness using the Either monad in C#. It's a discussion about the problem that arises from a lack of a built-in mechanism that allows you to express that a function is likely to fail. We implement a solution to this problem and see how it allows us to write production-ready code.
14. The case - model a function that performs an HTTP
request
15. Details
- HTTP has 62 status codes but only 10 of them mean success
- The chance of receiving an error is 84%
therefore
- The caller should be signaled unambiguously that errors are likely to
occur
- It should be easy for the caller to handle the potential errors
19. Requirements
1. It should be able to contain 2 other types - one that is going to be
returned if the operation went successfully and another one if it failed
a. In the HTTP request case, we would like one type of response if the server returned a
status code of 2XX and another one if it failed
2. It should never be null
a. It makes no sense for a type that signals a likely error to be null as that will lead to even
more errors
20. public struct Either<T, TException>
{
...
public bool HasValue { get; }
...
public static Either<T, TException> Success(T value) =>
new Either<T, TException>(value);
public static Either<T, TException> Error(TException exception) =>
new Either<T, TException>(exception);
}
29. // Perform a request
// If the request is successful, perform another request
// If the consequent request is successful, perform another request
// ...
30. // Perform a request
// If the request is successful, perform another request
// If the consequent request is successful, perform another request
// If the consequent request is successful, perform another request
31. // Perform a request
// If the request operation is successful, perform another request operation
// If the consequent request operation is successful, perform another request
operation
// If the consequent request operation is successful, perform another request
operation
32. public struct Either<T, TException>
{
public Either<TResult, TException> Map<TResult>(Func<T, TResult> mapping) =>
Match(
some: value => Either.Success<TResult, TException>(mapping(value)),
none: exception => Either.Error<TResult, TException>(exception)
);
}
33. var responseResult = HttpRequest(...);
responseResult.Map(response =>
{
var anotherResponseResult = HttpRequest(... response.Data);
anotherResponseResult.Map(anotherResponse =>
{
var thirdResponseResult = HttpRequest(... anotherResponse.Data);
thirdResponseResult.Map(thirdResponse =>
{
})
})
})
36. The type of
HttpRequest(...).Map(response =>
HttpRequest(... response.Data).Map(anotherResponse =>
HttpRequest(... anotherResponse.Data).Map(thirdResponse =>
HttpRequest(... thirdResponse.Data))))
is
41. public struct Either<T, TException>
{
public Either<TResult, TException> Map<TResult>(Func<T, TResult> mapping) =>
Match(
some: value => Either.Success<TResult, TException>(mapping(value)),
none: exception => Either.Error<TResult, TException>(exception)
);
}
42. public struct Either<T, TException>
{
public Either<TResult, TException> FlatMap<TResult>(Func<T, Either<TResult, TException>> mapping) =>
Match(
some: mapping, // We skip wrapping the value into an Either
none: exception => Either.Error<TResult, TException>(exception)
);
}
49. // function ProcessDocument userId, category, file
// 1. check if the user is authorized
// 2. store the file into the cloud
// 3. store database log with unique key
// 4. send the unique key to an external service for post-processing
53. ● Dev Adventures .NET Core template (link)
● A real world API using Either (link)
● A CLI tool for managing your music collection (link)
● A sample application using DDD and event-sourcing with complete
integration tests coverage (link)
● Real life examples of Either in C# (link)
Ask what’s wrong.
You don’t know if this returns null or throws an exception if it can’t parse.
No way to aggregate errors.
(You can put an “Errors” property in the RowData class but is basically fucking with the single responsibility principle)
Let’s say it throws an exception
Represents row data but has an Error property?
Ask why did this situation occur...
The answer is because the library you were using lacks expressiveness.
Most of you write in compiled languages, and using types allows you to leverage the compiler as much as possible.
This results in compiler error
How are we going to transform this, and aren’t we going to lose information?
We’re only going to have one error or one result. The nesting is meaningless.
We’re only going to have one error or one result. The nesting is meaningless.
This now works.
To this
A user uploads a file with a category.
The server checks if the user has rights for this category.
The server stores the file into some cloud storage (AWS S3 for example).
The server takes the cloud storage id and stores it into a database with a newly generated unique key.
The unique key is then sent to an external service.
Each function is looked upon as a separate operation, and the process function is going to be simply a composition of these operations.
It’s much easier to avoid bugs and structure your thinking when you have clearly defined “goals” of what an operation should do.
Note on integration testing: it’s very easy to reason about this thing as having at least one test per line.