-
Hey everyone, I'm still relatively new to functional programming so please go easy on me if this is a stupid question, but I'm struggling a bit to make some code that I have composable and I'm often finding myself writing redundant bits of code where I often have to Match pre-maturely rather than binding all my functions together and dealing with errors at the boundary of my application. Take this code for instance: public static Eff<Either<Error, JobExecutionData>> GetExecutionAsync(BatchJobDbContext context, BatchJobName name, DateOnly executionDate) =>
liftEff(async () =>
Optional(await context.JobExecutions.FindAsync(name.Value, executionDate))
.ToEither(Error.New($"Cannot find execution named {name.Value} on date {executionDate}"))
)
.Bind(eitherEntity => eitherEntity.Match(
Left: err => err,
Right: entity => GetAllStatusChangesAsync(context, entity.Name, entity.ExecutionDate).Map(statusChanges =>
JobExecutionEntity.ToDomain(entity, statusChanges)
)
));
public static Eff<Seq<JobStatusChange>> GetAllStatusChangesAsync(BatchJobDbContext context, string jobName, DateOnly dateKey)
=> liftEff(async ()
=> Optional(await context.JobStatusChanges.Where(s => s.JobName == jobName && s.Time.DateOnly() == dateKey).ToListAsync())
.Map(list => list.ToSeq())
.Bind(entities => entities.Map(entity =>
JobStatusChangeEntity.ToDomain(entity).ToOption()
).Somes())
).Map(lst => lst.ToSeq());
public record JobStatusChangeEntity
{
public required string JobName { get; init; }
public required DateTime Time { get; init; }
public string? FromStatus { get; init; }
public required string ToStatus { get; init; }
public static Either<Error, JobStatusChangeEntity> FromDomain(JobStatusChange domain) =>
Right(new JobStatusChangeEntity {
JobName = domain.JobName.Value,
Time = domain.Time,
FromStatus = domain.From.Match(
Some: from => from.ToString(),
None: () => null!
),
ToStatus = domain.To.ToString()
});
public static Either<Error, JobStatusChange> ToDomain(JobStatusChangeEntity entity) =>
from jobName in BatchJobName.Create(entity.JobName)
let fromStatus = JobStatus.Parser().Parse(entity.FromStatus ?? string.Empty).ToOption()
from toStatus in JobStatus.Parser().Parse(entity.ToStatus).ToEither(Error.New)
from jobStatusChange in JobStatusChange.Create(
jobName,
entity.Time,
fromStatus,
toStatus
)
select jobStatusChange;
}
public record JobExecutionEntity
{
public required string Name { get; init; }
public required DateOnly ExecutionDate { get; init; }
public required TimeOnly ExecutionTime { get; init; }
public static Either<Error, JobExecutionEntity> FromDomain(JobExecutionData domain) =>
new JobExecutionEntity {
Name = domain.JobName.Value,
ExecutionDate = domain.ExecutionTime.DateOnly(),
ExecutionTime = domain.ExecutionTime.TimeOnly()
};
public static Either<Error, JobExecutionData> ToDomain(JobExecutionEntity entity, Seq<JobStatusChange> jobStatusChanges) =>
JobExecutionData.Create(entity.Name, new DateTime(entity.ExecutionDate, entity.ExecutionTime), jobStatusChanges); I'm dealing with EntityFramework at this point in my codebase, and want to bind my effect operations together and deal with them at my controller level so that all side-effects occur at the boundary of my application. As you can see in the GetExecutionAsync function (and probably elsewhere lol), there is some redundant code, particularly this bit: eitherEntity.Match(
Left: err => err,
Right: entity => GetAllStatusChangesAsync(context, entity.Name,
... To my understanding, instead of using public static Eff<Either<Error, JobExecutionData>> GetExecutionAsync2(BatchJobDbContext context, BatchJobName name, DateOnly executionDate)
=> liftEff(async () =>
Optional(await context.JobExecutions.FindAsync(name.Value, executionDate))
.ToEither(Error.New($"Cannot find execution named {name.Value} on date {executionDate}"))
)
.Bind(eitherEntity => eitherEntity.Bind(entity =>
GetAllStatusChangesAsync(context, entity.Name, entity.ExecutionDate).Map(statusChanges =>
JobExecutionEntity.ToDomain(entity, statusChanges)
)
));
Based on the code that I've seen in Samples, my code looks a lot more sloppy, however in many cases I haven't been able to get from .. in .. syntax working. There are some examples in my code that I'm happy with, but this is an area of my code that has really been bothering me. I've been able to do similar stuff to this in Haskell and the type-inference system has done a lot of the heavy-lifting for me, but doing this in CSharp has been a thorn in my side for quite a while. Does anyone have any ideas what I might be doing wrong? I'm very much open to criticism here and ultimately want to get better at this. I'm happy to provide more context if needed. Thanks in advance. Kyle |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 1 reply
-
Hi Kyle, I'm also relatively new to this library and still wrapping my head around some of the deeper patterns, but from what I’ve understood so far I think I can help clarify what’s going on here. In your second example:
The outer .Bind is working on Eff, so your lambda needs to return an Eff. You can match on the Either inside the Eff context and promote the result with Fail() or continue with another Eff:
This way, everything stays inside Eff and you're handling both paths cleanly. |
Beta Was this translation helpful? Give feedback.
-
liftEff(async () =>
Optional(await context.JobExecutions.FindAsync(name.Value, executionDate))
.ToEither(Error.New($"Cannot find execution named {name.Value} on date {executionDate}"))
)
.Bind(eitherEntity => eitherEntity.Bind(entity =>
GetAllStatusChangesAsync(context, entity.Name, entity.ExecutionDate).Map(statusChanges =>
JobExecutionEntity.ToDomain(entity, statusChanges)
) Here you wrap a
static Eff<JobExecutionData> GetExecutionAsync(BatchJobDbContext context, BatchJobName name, DateOnly executionDate) =>
// Lift IO call
from jobExecutionEntity in liftEff(async () => await context.JobExecutions.FindAsync(name.Value, executionDate))
// Check that entity is not null; otherwise shortcurcuit with an error
from _ in guard(jobExecutionEntity is null, Error.New($"Cannot find execution named {name.Value} on date {executionDate}"))
from statusChanges in GetAllStatusChangesAsync(context, jobExecutionEntity.Name, jobExecutionEntity.ExecutionDate)
// Create a domain with helper function that returns Fin<JobExecutionData>
from domain in JobExecutionEntity.ToDomain(jobExecutionEntity, statusChanges).ToEff()
select domain;
static Eff<Seq<JobStatusChange>> GetAllStatusChangesAsync(BatchJobDbContext context, string jobName, DateOnly dateKey) =>
from changes in liftEff(async () =>
await context.JobStatusChanges
.Where(s => s.JobName == jobName && s.Time.DateOnly() == dateKey)
.ToListAsync())
// Create Iterable<T> from List<T> to get its capabilities, then traverse and apply `JobStatusChangeEntity.ToDomain`
// function to each element resulting in an Iterable<T> with only elements that successfully was transformed to domain model.
// As() and ToEff() are just helper functions to get outer monad Fin<Iterable<T>> and convert it to Eff<T> so
// it has same type in the entire linq expression.
from domains in changes.AsIterable().Traverse(JobStatusChangeEntity.ToDomain).As().ToEff()
select domains.ToSeq();
static Eff<Unit> UsageExample(BatchJobDbContext context) =>
from executionData in GetExecutionAsync(new BatchJobName("Test"), DateTime.Now.DateOnly())
| @catch(err => Log.LogInfo(err.Message)) // just an abstract example of logging error when it was not found
select unit; There may be more changes to improve code readability, such as bringing a runtime for I feel I've suggested it to you already some time ago, but louthy has a good series of articles for diving in FP with language-ext v5: https://paullouth.com/ |
Beta Was this translation helpful? Give feedback.
-
Slightly unrelated, but of possible interest to most people here; although https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_errors.html It's reassuring to know that LanguageExt's usage of Scala's Sorry if this is derailing the overall discussion too much! |
Beta Was this translation helpful? Give feedback.
Eff<Either<Error, JobExecutionData>>
is redundant,Eff<T>
is already usingFin<T>
which is basically anEither<Error, T>
.Option
,Either
andEff
). If you are using v4, there are some built in functions to transform, say,Option
toEff
. If you are using v5, there are monad transformers that you can use, which simplifies working with different stacks even more and giving you a way to build you own domain monad.