- Published on
Map, don't ask
- Authors
- Name
- Damian Płaza
- @raimeyuu
No, it is not an YAMT - Yet Another Monad Tutorial.
There will be no burritos, boxes...Or maybe there will be?
Finally, functional programming
Let's imagine we attended a conference when someone was glorifying functional programming and all benefits it brings - purity, no poverity in the world, wealth to all the people, etc.
It sounded pretty impressive and someone from our team (yes, this person attended the conference too!) decided to grab one of the packages, from the ecosystem we "live in", that brings "functional powers".
(Yes dear Reader, there's a tiny assumption that we don't work with a functional-first language, but please bear with me)
Let's say we work with .NET (avid Reader might spot that .NET = C#) and we can use CSharpFunctionalExtensions
as our package of choice.
We check the docs and what we see? Maybe
type.
Barbaric nulls
Can you feel this power too?
No more nulls!
No more problems!
No more poverty!
Let's imagine we took one of our command handlers that worked with a repository in order to try to get active user with given identifier and then deactive that user.
Deactivating has a consequence on permissions which we need to take care of too.
Fortunately, we don't have huge traffic and it should happen immediately - meaning that we can't "distribute" handling this workflow.
Yes, no asynchronous processing this time.
Before "functionalifying", dull, not functional and not impressive code looked as follows:
public class DeactivateUserCommandHandler
{
public DeactivateUserCommandHandler(
ActiveUsersRepository activeUsers,
GroupPermissionsRepository groupPermissions
)
{
_activeUsers = activeUsers;
_groupPermissions = groupPermissions;
}
public async Task Handle(DeactivateUserCommand command)
{
var userId = command.UserId;
var activeUser = await _activeUsers.GetBy(userId);
if(activeUser is null)
{
throw new CannotDeactivateUser(userId);
}
activeUser.Deactivate();
var userGroupPermissions = await _groupPermissions.GetFor(userId);
if(userGroupPermissions is null)
{
throw new CannotDeactivateUser($"Cannot remove group permissions for user {userId}");
}
userGroupPermissions.Remove();
await _activeUsers.Save(activeUser);
await _groupPermissions.Save(userGroupPermissions);
}
}
This null
- pathetic.
Good that we have functional programming to take care of that pesky, barbaric null!
Finally, monads!
Imagine that we added a new overloaded method in our ActiveUsersRepository
that expresses the effect of "missingness" of an active user.
public interface ActiveUsersRepository
{
// ...other "old" methods
public Task<Maybe<ActiveUser>> GetBy(UserId userId);
}
Now we are talking, our method is honest and tells the full story! (does it?)
Let's make proper adjustments.
public class DeactivateUserCommandHandler : IHandle<DeactivateUserCommand>
{
public DeactivateUserCommandHandler(
ActiveUsersRepository activeUsers,
GroupPermissionsRepository groupPermissions
)
{
_activeUsers = activeUsers;
_groupPermissions = groupPermissions;
}
public async Task Handle(DeactivateUserCommand command)
{
var userId = command.UserId;
var activeUser = await _activeUsers.GetBy(userId);
if(activeUser.HasNoValue) // 👈️ changed code!
{
throw new CannotDeactivateUser(userId);
}
activeUser.Value.Deactivate(); // 👈️ changed code!
var userGroupPermissions = await _groupPermissions.GetFor(userId);
if(userGroupPermissions is null) // ❌ here aren't that functional yet 😥
{
throw new CannotDeactivateUser($"Cannot remove group permissions for user {userId}");
}
userGroupPermissions.Remove();
await _activeUsers.Save(activeUser.Value); // 👈️ changed code!
await _groupPermissions.Save(userGroupPermissions);
}
}
So much functional programming!
Lazy, anemic folks
I hope you already know it, dear Reader, that I like to talk to parts of the code I wrote, as if they were actors playing their roles on the scene. (you don't know what I am talking about? Go ahead and check Conversation-Driven Design) or I, interface)
Should we talk to DeactivateUserCommandHandler
?
Interesting, isn't it?
In this little therapy session, DeactivateUserCommandHandler
revealed interesting observation - it is quite overloaded.
It should take care of ensuring that the process of deactivation runs smoothly, one step after another, but now it seems that it has too much responsibilities.
Ok, even if we did hardcore functional programming, let's get back to the roots.
Let's turn our sight onto objects that collaborate and talk to each other in order to perform some activities.
Tell, don't ask
"In the old and good days", objects were fully responsible for themselves (at least that's what our parents ought to say to us).
In our tiny tale, DeactivateUserCommandHandler
know intimate and spicy details of summoned collaborators and needs to deal with them explicitly (is it a bad or a good thing?).
Those collaborators might not exist (checks for "missingness") and then it needs to react appriopriately (throwing exceptions).
So our handler asks about the state ("do you exist?") and then makes the decision.
Could we do something about it? Re-assign responsibilities so that DeactivateUserCommandHandler
knows less?
Let's "just" do some wishful thinking "coding" and write down how we would imagine this collaboration could look like.
Just so you know, dear Reader, I will drop the usage of newly added method:
public interface ActiveUsersRepository
{
// ...other "old" methods
public Task<Maybe<ActiveUser>> GetBy(UserId userId);
}
In favor of "old", barbaric "object-oriented" one, so it looks as follows:
public class DeactivateUserCommandHandler : IHandle<DeactivateUserCommand>
{
public DeactivateUserCommandHandler(
ActiveUsersRepository activeUsers,
GroupPermissionsRepository groupPermissions
)
{
_activeUsers = activeUsers;
_groupPermissions = groupPermissions;
}
public async Task Handle(DeactivateUserCommand command)
{
var userId = command.UserId;
var activeUser = await _activeUsers.GetBy(userId);
activeUser.Deactivate(); // 👈️ Look, no ifs!
var userGroupPermissions = await _groupPermissions.GetFor(userId);
userGroupPermissions.Remove();
await _activeUsers.Save(activeUser); // 👈️ Look, no ifs!
await _groupPermissions.Save(userGroupPermissions);
}
}
Ifs?
All experienced and smart people will probably immediately notice that "there are no ifs" is a complete lie.
And yes - that's true - it is a lie.
They didn't disappear, just changed places ("Complexity does not disappear, it gets moved", remember?).
In our tiny tale's problem area, there's clearly a concept of an "active user", so let's inspect that.
What we know about it? Well, there's a clear responsibility: be able to get itself deativated.
Let's represent that capability:
public interface ICanBeDeactivated
{
public void Deactivate();
}
Now comes the abstract concept of an user that is regarded as active.
public abstract class ActiveUser(Guid ActiveUserId) : ICanBeDeactivated
{
protected Guid ActiveUserId = ActiveUserId;
public abstract void Deactivate();
}
And finally - let's make this "concept" responsible for itself by making it the owner of its "missingness" variant:
public class NonExistingActiveUser(Guid ActiveUserId) : ActiveUser(ActiveUserId)
{
public override void Deactivate()
{
throw new CannotDeactivateUser(ActiveUserId);
}
}
Ok, now the "happy path":
public class ExistingActiveUser(Guid ActiveUserId, bool IsActive) : ActiveUser(ActiveUserId)
{
private bool _isActive = IsActive;
public override void Deactivate()
{
if(!_isActive)
throw new CannotDeactivateUser($"Cannot deactivate already deactivated user with id {ActiveUserId}");
_isActive = false;
}
}
As you can see, dear Reader, we modelled two variants, or I should probably say, two cases of an active user.
In one of the cases, there's no existing active user with a given id, whereas the another one represents the existing one.
How does this changed our design?
We are "telling" this abstract concept what we want it to do, without asking.
Each of its "aspects" clearly knows what to do.
But hey, there's a strong requirement that this user groups permissions need to be removed as it gets deativated.
Shouldn't we design the interface to enforce that?
Need-Driven Design
Let's alter deactivating capability so that it expresses such requirement:
public interface ICanBeDeactivated
{
public void Deactivate(IRemoveUserGroupPermissions andContinueWith);
}
For now, we're going to omit strangely named argument.
We're focusing on (yet) non-existing capability but we clearly know what we want to achieve.
What will be the usage?
Let's focus on that, rather than thinking how one could implement it.
public class DeactivateUserCommandHandler : IHandle<DeactivateUserCommand>
{
public DeactivateUserCommandHandler(
ActiveUsersRepository activeUsers,
GroupPermissionsRepository groupPermissions
)
{
_activeUsers = activeUsers;
_groupPermissions = groupPermissions;
}
public async Task Handle(DeactivateUserCommand command)
{
var userId = command.UserId;
var activeUser = await _activeUsers.GetBy(userId);
await activeUser.Deactivate(
andContinueWith: new UserGroupPermissionsRemoval(_groupPermissions) // 👈️ here's what has changed!
);
await _activeUsers.Save(activeUser);
}
}
It looks strangely suspicious, isn't it?
An object without any "asynchronicity" suddenly got "awaited".
Smell-ish, right?
We can start thinking about our new collaborator - UserGroupPermissionsRemoval
.
Based on its usage, I believe we can figure out how it might work:
// 👇 a new responsibility!
public interface IRemoveUserGroupPermissions
{
Task RemoveFor(Guid userId);
}
// 👇 a new collaborator that has this responsibility
public class UserGroupPermissionsRemoval(GroupPermissionsRepository GroupPermissions) : IRemoveUserGroupPermissions
{
public async Task RemoveFor(Guid userId)
{
var userGroupPermissions = await GroupPermissions.GetFor(userId);
userGroupPermissions.Remove();
await GroupPermissions.Save(userGroupPermissions);
}
}
As we changed the responsiblity for deactivating oneself, we need to ensure a specific aspect of the abstract concept - active user - also aligns to the new design!
public class ExistingActiveUser(Guid ActiveUserId, bool IsActive) : ActiveUser(ActiveUserId)
{
private bool _isActive = IsActive;
public override async Task Deactivate(IRemoveUserGroupPermissions userGroupPermissionsRemoval)
{
if(!_isActive)
throw new CannotDeactivateUser($"Cannot deactivate already deactivated user with id {ActiveUserId}");
_isActive = false;
await userGroupPermissionsRemoval.RemoveFor(ActiveUserId);
}
}
We will omit the details of how does UserGroupPermissions
concept looks and works internally - one can derive the implementation based on what we did.
Functional programming?
I hear you dear Reader - we started with hardcore functional programming by introducing CSharpFunctionalExtensions
and suddenly we wrote some objects - come on, what is that - an amateur hour?!
The collaborating objects showed one interesting attribute - the composition.
DeactivateUserCommandHandler
composed their powers together without controlling each and every "step" of each collaborator.
Instead of "orchestrating" lazy and anemic objects, we excelled their skills to the maximum of their ownership.
Before, the syntax haven't changed that much as we procedurally checked for "the missingness", either by using is null
or HasNoValue
.
Let's try to do the same with Maybe
type - DeactivateUserCommandHandler
should focus on composition rather than asking for its "internals"!
NOTE: if you are sensitive to ugly/unreadable code, either omit the next lines and rush to summary or please be forgiving.
public class DeactivateUserCommandHandler : IHandle<DeactivateUserCommand>
{
public DeactivateUserCommandHandler(
ActiveUsersRepository activeUsers,
GroupPermissionsRepository groupPermissions
)
{
_activeUsers = activeUsers;
_groupPermissions = groupPermissions;
}
public async Task Handle(DeactivateUserCommand command)
{
var userId = command.UserId;
(await _activeUsers
.GetBy(userId))
.Map(activeUser => {
activeUser.Deactivate();
return activeUser;
})
.ToResult($"Cannot deactivate the user with id {userId}")
.Bind(async (activeUser) =>
(await _groupPermissions
.GetFor(userId))
.Map(userGroupPermissions => {
userGroupPermissions.Remove();
return (activeUser, userGroupPermissions);
})
.ToResult($"Cannot remove group permissions for user {userId}")
)
.Match(
onSuccess: async (activeUser, userGroupPermissions) => {
await _activeUsers.Save(activeUser);
await _groupPermissions.Save(userGroupPermissions);
},
onFailure: async (error) => {
throw new CannotDeactivateUser(error);
}
);
}
}
(note dear Reader that this might not compile without additional type annotations - C# hasn't that well type inference when combined with CSharpFunctionalExtensions
)
I firmly believe that if you got "Ok, what the hell just happened?" reaction - you're probably a healthy human being.
We went from "procedural missingness checking" into "tell, don't ask"-ish way of handling it by passing behavior "inside" - to Maybe
and to Result
, later in the processing.
Of course, functional programming is "all about expressions" and this "design" expresses that.
Everything gets transformed along the way - from Maybe
to Result
- which summons a behemoth full of unreadable piece of code.
But please, don't "blame" functional programming.
To fully benefit from "sequential" processing via "monadic comprehension", that reduces boilerplate quite much, we would need to employ Monadic Comprehension Syntax via LINQ in C# presented by Oleksii.
It would probably looks something like this:
NOTE #1: it's based on the blog post mentioned above, so feel encouraged to read it first!
NOTE #2: the syntax below is how I imagine it could look like - sounds like a nice exercise to implement, isn't it?
public class DeactivateUserCommandHandler : IHandle<DeactivateUserCommand>
{
public DeactivateUserCommandHandler(
ActiveUsersRepository activeUsers,
GroupPermissionsRepository groupPermissions
)
{
_activeUsers = activeUsers;
_groupPermissions = groupPermissions;
}
public async Task Handle(DeactivateUserCommand command)
{
var deactivationResult =
await (
from activeUser in activeUsers.GetBy(userId)
let deactivatedUser = activeUser.Deactivate()
from userGroupPermissions in groupPermissions.GetFor(userId)
let newUserGroupPermissions = userGroupPermissions.Remove()
let savingDeactivatedUser = activeUsers.Save(deactivatedUser)
let savingUserGroupPermissions = groupPermissions.Save(newUserGroupPermissions)
select (Result.Combine(savingDeactivatedUser, savingUserGroupPermissions))
)
deactivationResult.Match(
onSuccess: () => {},
onFailure: (error) => {
throw new CannotDeactivateUser(error);
}
);
}
}
Functional "tell, don't ask"?
Even though this was a tiny, toy example, we explored an important trait of designing using "tell, don't ask" approach - focusing on "what" and passing behavior around.
Expressing effects like "missingness" (via Maybe
) or "branching (with erroneous path)" (via Result
) might require thinking in terms of "passing the behavior in", rather then trying to "peek what's inside".
We saw something similar in representing abstract concepts like an active user, covering two cases that existed in our tiny tale's "knowledge area".
Either functional or object oriented - we tried our best to manifest certain aspects, like missingness.
When working with Maybe
, instead of "peeking inside", we can "tell" it to perform a mapping (a transformation) by passing it a behavior that will be used if there's something inside.
We want to keep the intimate details of "the packaging" and not obsessively have the control of everything.
We often talk about "primitive obsession", maybe we should start talking about "control obsession"?
Don't ask the effect, tell it about transformations
As authors of GOOS wrote:
We have objects sending each other messages, so what do they say? Our experience is that the calling object should describe what it wants in terms of the role that its neighbor plays, and let the called object decide how to make that happen. This is commonly known as the “Tell, Don’t Ask” style or, more formally, the Law of Demeter.
Objects make their decisions based only on the information they hold internally or that which came with the triggering message; they avoid navigating to other objects to make things happen.
Maybe
or Result
did something similar - they "made decisions" based on the information they held internally.
We didn't want to "obsessively control" them - we trusted them that they "are" certain structures, having certain attributes and based on those motivations we could just describe transformations, and "let them do the rest of the job".
And of course - they are not "objects", so to speak. We are not "sending messages to them", but what about behaviors?
Let's jump back to GOOS:
We find that by applying “Tell, Don’t Ask” consistently, we end up with a coding style where we tend to pass behavior (in the form of callbacks) into the system instead of pulling values up through the stack.
Well, callbacks.
Passing functions here and there.
Treating them as regular values - we know that, right?
What is it all about?
It is all about composition.
"Ifs do not compose", one could say.
As with applying "non existing" aspect of an ActiveUser
abstract concept (in the form of NonExistingActiveUser
), which turned out to be a Null Object pattern, using Maybe
and/or Result
helped us by moving "ifs" somewhere else.
It gave us powers to compose functions together, working with "effects" explicitly.
Instead of procedurally control everything, everytime, we could "trust" the promises provided by the "algebraic structures" that Maybe
and Result
types are.
Even though objects and monads are different, they tend to provide solutions to similar set of problems.
For object, this magic happened if we started treating objects as units of behavior, rather than "structures".
Those solutions tend to orbit around passing behavior in and not focusing on the structure too much.
And such approaches favor composition (of functions or of objects) too.
Suddenly, when we employ "wishful" designing, which in fact might be understood as a declarative way of representing intensions and providing promises, the holy grail of composition naturally emerges either from the collaboration of objects or from the set of effectful transformations.
So next time dear Reader, either tell your objects to do their job or transform your effects into new ones.
But don't ask.
Well, at least most of the times.