Reputation: 1514
From this amazing course: Applying Functional Principles in C#
I am trying to apply domain drive design, functional principles and railway oriented programming approaches.
Can someone help me to concise those lines of code below?.
I know that I need to create some Result of T extension methods, I tried but I am not able to get them work.
What the pseudo code does is...
(We do this operation before hitting the database for update operations, if we got an invalid EmailAddress, has no sense to hit the database at all)
(The railway gets broken due to the necessity to have available the variable emailAddress later on :( so sad!!. Even worst... a failed EmailAddress Result entity wasn't checked, and it should be)
(The railway gets broken once more due to the necessity to have available the variable playerResult later on :( so sad!!. Even worst... a failed to return Result Player entity wasn't checked, and it should be)
(The railway gets broken once more :( so sad!!. Even worst... a failure to add the EmailAddress to the player collection wasn't checked, and it should)
Here below the code lines reduced for brevity
var emailAddress = Email.Create("[email protected]")
.OnSuccess(email => EmailAddress.Create(email, default));
var playerResult = await emailAddress.OnSuccess(e => repository.GetAsync("336978e9-837a-4e8d-6b82-08d6347fe6b6")).ToResult(""));
var wasAdded = playerResult.OnSuccess(p => p.AddEmailAddress(emailAddress.Value));
var wasSaved = await wasAdded.OnSuccess(a => unitOfWork.SaveChangesAsync());
var message = wasSaved
.Ensure(r => r > 0, "No record were saved")
.OnBoth(result => result.IsSuccess ? "Ok" : result.Error);
Here below the signature of the methods
Email.Create -> public static Result<Email> Create(string email)
EmailAddress.Create -> public static Result<EmailAddress> Create(Email mailAddress, string mailAddressInstructions)
GetAsync -> public Task<Maybe<Player>> GetAsync(string id)
AddEmailAddress -> public Result<bool> AddEmailAddress(EmailAddress emailAddress)
SaveChangesAsync -> public async Task<int> SaveChangesAsync()
I performed some unit test and the code is working but as far as you can see is very far to be railway oriented.
Thanks in advance.
Upvotes: 2
Views: 5956
Reputation: 457302
Personally, I prefer using local variables; I think it's more maintainable. This is particularly true when dealing with non-functional designs such as Unit of Work. However, what you want to do is possible using functional programming constructs.
There's a couple of things you need to use to remove your local variables. First, you need a bind
for your monad; that's what allows you to unwrap multiple values and then map them to a new value. The second is tuples.
I find it useful to have my domain types themselves not know anything about functional types. So there's just regular methods and whatnot, not returning Result<T>
types:
private static Task<Player> RepositoryGetAsync(string id) => Task.FromResult(new Player());
private static Task<int> RepositorySaveChangesAsync() => Task.FromResult(0);
public sealed class Player
{
public bool AddEmailAddress(EmailAddress address) => true;
}
public sealed class Email
{
public Email(string address) => Address = address ?? throw new ArgumentNullException(nameof(address));
public string Address { get; }
}
public sealed class EmailAddress
{
public static EmailAddress Create(Email address, int value) => new EmailAddress();
}
Here's the first version of your code - using my own Try<T>
type, as I'm not sure which Maybe<T>
and Result<T>
types you're using. My Try<T>
type supports SelectMany
so that it can be used in a multiple-from
clause to unwrap multiple instances:
static async Task Main(string[] args)
{
var emailAddress = Try.Create(() => new Email("[email protected]"))
.Map(email => EmailAddress.Create(email, default));
var playerResult = await Try.Create(() => RepositoryGetAsync("336978e9-837a-4e8d-6b82-08d6347fe6b6"));
var wasAdded = from address in emailAddress
from player in playerResult
select player.AddEmailAddress(address);
var wasSaved = await wasAdded.Map(_ => RepositorySaveChangesAsync());
if (wasSaved.Value == 0)
throw new Exception("No records were saved");
}
If we start using tuples, we can then combine a couple of variables. The syntax is a bit awkward (e.g., deconstructing lambda arguments), but it's doable:
static async Task Main(string[] args)
{
var emailAddressAndPlayerResult = await Try.Create(() => new Email("[email protected]"))
.Map(email => EmailAddress.Create(email, default))
.Map(async emailAddress => (emailAddress, await RepositoryGetAsync("336978e9-837a-4e8d-6b82-08d6347fe6b6")));
var wasAdded = emailAddressAndPlayerResult.Map(((EmailAddress Address, Player Player) v) => v.Player.AddEmailAddress(v.Address));
var wasSaved = await wasAdded.Map(_ => RepositorySaveChangesAsync());
if (wasSaved.Value == 0)
throw new Exception("No records were saved");
}
Once we have tuples in the mix, the rest of the variables fold up nicely. The only awkward part left is that await
usually requires parentheses. E.g., this code is the same as above:
static async Task Main(string[] args)
{
var wasAdded =
(
await Try.Create(() => new Email("[email protected]"))
.Map(email => EmailAddress.Create(email, default))
.Map(async emailAddress => (emailAddress, await RepositoryGetAsync("336978e9-837a-4e8d-6b82-08d6347fe6b6")))
)
.Map(((EmailAddress Address, Player Player) v) => v.Player.AddEmailAddress(v.Address));
var wasSaved = await wasAdded.Map(_ => RepositorySaveChangesAsync());
if (wasSaved.Value == 0)
throw new Exception("No records were saved");
}
And now as we combine these variables, you can see how the await
expressions in particular create "bumps" in the road. Ugly, but doable. To remove the last variable:
static async Task Main(string[] args)
{
var wasSaved =
await
(
await Try.Create(() => new Email("[email protected]"))
.Map(email => EmailAddress.Create(email, default))
.Map(async emailAddress => (emailAddress, await RepositoryGetAsync("336978e9-837a-4e8d-6b82-08d6347fe6b6")))
)
.Map(((EmailAddress Address, Player Player) v) => v.Player.AddEmailAddress(v.Address))
.Map(_ => RepositorySaveChangesAsync());
if (wasSaved.Value == 0)
throw new Exception("No records were saved");
}
To reiterate what I stated at the beginning, it's possible to do this, but IMO it's ugly and less maintainable. The bottom line is that C# is an imperative language and not a functional language. Adopting some aspects of functional languages in parts of your code can make it more elegant; trying to force all your code to be fully functional is a recipe for unmaintainability.
Upvotes: 4