-
I am new to FP and I have been following article series on paullouth.com and some of the discusions in here 1, 2. Since the final code that use monad transformers look elegant I have dicide to give it a try and create a simple CRUD api using this library and following the principles I learned as closely as possible. But I feel like I am still missing somehthing. Here is what I have done. Since I need Db I tried to create a wrapper around EF that is using public record Db<A>(ReaderT<DbEnv, IO, A> RunDb) : K<Db, A>
{
public Db<B> Select<B>(Func<A, B> m) => this.Kind().Select(m).As();
public Db<C> SelectMany<B, C>(Func<A, K<Db, B>> b, Func<A, B, C> p) => this.Kind().SelectMany(b, p).As();
public Db<C> SelectMany<B, C>(Func<A, K<IO, B>> b, Func<A, B, C> p) => this.Kind().SelectMany(b, p).As();
} with this trait implementation: public abstract partial class Db : Monad<Db>, Readable<Db, DbEnv>
{
public static K<Db, B> Map<A, B>(Func<A, B> mapFunc, K<Db, A> db)
=> new Db<B>(db.As().RunDb.Map(mapFunc).As());
public static K<Db, A> Pure<A>(A value)
=> new Db<A>(ReaderT<DbEnv, IO, A>.Pure(value));
public static K<Db, B> Apply<A, B>(K<Db, Func<A, B>> applyFunc, K<Db, A> db)
=> new Db<B>(applyFunc.As().RunDb.Apply(db.As().RunDb));
public static K<Db, B> Bind<A, B>(K<Db, A> db, Func<A, K<Db, B>> bindFunc)
=> new Db<B>(db.Run().Bind(x => bindFunc(x).Run()).As());
public static K<Db, A> Asks<A>(Func<DbEnv, A> f)
=> new Db<A>(ReaderT.asks<IO, A, DbEnv>(f));
public static K<Db, A> Local<A>(Func<DbEnv, DbEnv> f, K<Db, A> db)
=> new Db<A>(ReaderT.local(f, db.Run().As()));
public static K<Db, A> LiftIO<A>(IO<A> ma)
=> new Db<A>(ReaderT.liftIO<DbEnv, IO, A>(ma));
} And some other convinience functions: public abstract partial class Db
{
public static Db<A> Lift<A>(IO<A> io)
=> new(ReaderT<DbEnv, IO, A>.LiftIO(io).As());
public static Db<DbEnv> Ask()
=> Readable.ask<Db, DbEnv>().As();
public static Db<Context> Ctx<Context>()
where Context : DbContext
=> from env in Ask()
select (Context)env.DbContext;
public static Db<Unit> Save() =>
from env in Ask()
from _ in liftIO(() => env.DbContext.SaveChangesAsync())
select unit;
}
public static class DbExtensions
{
public static Db<A> As<A>(this K<Db, A> kind)
=> (Db<A>)kind;
public static K<ReaderT<DbEnv, IO>, A> Run<A>(this K<Db, A> db)
=> db.As().RunDb;
public static IO<A> Run<A>(this K<Db, A> db, DbEnv env)
=> db.Run().Run(env).As();
public static Db<A> ToDb<A>(this ValueTask<A> task)
=> Db.Lift(liftIO(async () => await task));
public static Db<A> ToDb<A>(this Task<A> task)
=> Db.Lift(liftIO(() => task));
} This allows me to write code like this: public static class ProductRepository
{
public static Db<List<Product>> List() =>
from ctx in Db.Ctx<AppDbContext>()
from ps in ctx.Products.ToListAsync().ToDb()
select ps;
public static Db<Unit> Add(Product product) =>
from ctx in Db.Ctx<AppDbContext>()
from _ in ctx.AddAsync(product).ToDb()
select unit;
} And use it like this: var listProducts =
from products in ProductRepository.List()
from _ in Printer.PrintProducts(products)
select _; So far so good. Everything looks clear and I can run The problem I have starts to appear when I want to implement public static Db<Option<Product>> Get(int id) =>
from ctx in Db.Ctx<AppDbContext>()
from p in liftIO(async () => Optional(await ctx.Products.FindAsync(id)))
select p; And now the usage is a bit ruined: var getProduct =
from product in ProductRepository.Get(1)
from _ in Printer.PrintProduct(product) // won't work since `product` is `Option<Product>` and not `Product`
select _; The solution I can think of is to create another monad transformer stack |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 8 replies
-
You can leverage the fact that public abstract partial class Db :
Monad<Db>,
Readable<Db, DbEnv>,
Fallible<Db>
{
public static K<Db, A> Fail<A>(Error error) =>
Lift(IO.fail<A>(error));
public static K<Db, A> Catch<A>(K<Db, A> fa, Func<Error, bool> Predicate, Func<Error, K<Db, A>> Fail) =>
Ask().Bind(env => fa.MapIO(io => io.Catch(Predicate, e => Fail(e).Run(env))));
public static K<Db, B> MapIO<A, B>(K<Db, A> ma, Func<IO<A>, IO<B>> f) =>
new Db<B>(new ReaderT<DbEnv, IO, B>(env => f(ma.Run(env))));
public static K<Db, IO<A>> ToIO<A>(K<Db, A> ma) =>
ma.MapIO(IO.pure); So, you can see above I added I also provided overrides for new Db<A>(
new ReaderT<DbEnv, IO, A>(
env => fa.Run(env).Catch(Predicate, e => Fail(e).As().RunDb.Run(env)))); But the other benefit is that now Then add the operators to public static Db<A> operator |(Db<A> ma, Db<A> mb) =>
ma.Catch(mb).As();
public static Db<A> operator |(Db<A> ma, CatchM<Error, Db, A> mb) =>
ma.Catch(mb).As();
public static Db<A> operator |(Db<A> ma, Pure<A> mb) =>
ma.Catch(mb).As();
public static Db<A> operator |(Db<A> ma, Error mb) =>
ma.Catch(mb).As(); These will allow you to write So, failure now can be either exceptional (the underlying calls to EF throw exceptions), or manually raised using: I hope that helps. |
Beta Was this translation helpful? Give feedback.
-
I understand what louthy says like this: you don't need to use custom Here's a working example of how it could be done (BEWARE that I'm learning this library and FP as well 😄, so something may be overdone or misused): using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using LanguageExt;
using LanguageExt.Common;
using LanguageExt.Traits;
using Microsoft.EntityFrameworkCore;
using static LanguageExt.Prelude;
AppDbContext dbCtx = new();
dbCtx.AddFakeData();
DbEnv env = new(dbCtx);
// Test printing
var listProducts =
from ps in ProductRepository.list()
from _ in Printer.printProducts(ps)
select unit;
await listProducts.Run(env).RunAsync();
// Test when not found
var getProduct =
(from p in ProductRepository.get(999) // use non existing id so it fails
from _ in Printer.printProduct(p)
select unit)
| @catch(DbError.NotFound, err => Db.LiftIO(Printer.error(err))) // catch specific errors if you need to handle them in some custom way
| @catch(err => Db.LiftIO(Printer.error(err))); // or catch any errors
await getProduct.Run(env).RunAsync();
// Test adding with invalid properties
var addInvalidProduct =
(from _0 in ProductRepository.add(new Product() { Id = -1, Name = null! })
from _1 in Db.Save()
select unit)
.MapFail(DbError.AddFailed)
| @catch(err => Db.LiftIO(Printer.error(err))); // catch any errors
await addInvalidProduct.Run(env).RunAsync();
// Test adding with valid properties
var addValidProduct =
(from _0 in ProductRepository.add(new Product() { Id = 6, Name = "My product" })
from _1 in Db.Save()
from _2 in Printer.info("Added successfully")
select unit)
| @catch(err => Db.LiftIO(Printer.error(err))); // catch any errors
await addValidProduct.Run(env).RunAsync();
// Modules
static class ProductRepository
{
public static K<Db, List<Product>> list() =>
from ctx in Db.Ctx<AppDbContext>()
from ps in liftIO(async () => await ctx.Products.ToListAsync())
select ps;
public static K<Db, Product> get(int id) => // no need to use Db<Option<Product>> to short-circuit on error
from ctx in Db.Ctx<AppDbContext>()
from p in Db.LiftIO(ctx.Products.FindAsync(id))
from _ in guard(notnull(p), DbError.NotFound) // check if product is not null; otherwise, provide the error and stop the computation
select p;
public static K<Db, Unit> add(Product product) => // no need to use Db<Validation<Product>> to harvest the errors
from p in Db.Lift(ProductValidation.validate(product))
from ctx in Db.Ctx<AppDbContext>()
from addRes in Db.LiftIO(ctx.AddAsync(p))
select unit;
}
static class DbError
{
public enum Code
{
NotFound = 1,
AddFailed = 2
}
public static readonly Error NotFound =
Error.New((int)Code.NotFound, "Not found");
public static Error AddFailed(Error e) =>
Error.New((int)Code.AddFailed, $"Failed to add: {e.Message}");
}
static class ProductValidation
{
public static Validation<Error, Product> validate(Product p) =>
fun<Product, Product, Product>((_, _) => p)
.Map(validateId(p))
.Apply(validateName(p))
.As();
public static Validation<Error, Product> validateId(Product p) =>
p.Id > 0
? Pure(p)
: Fail(ProductValidationError.InvalidId);
public static Validation<Error, Product> validateName(Product p) =>
!string.IsNullOrEmpty(p.Name)
? Pure(p)
: Fail(ProductValidationError.InvalidName);
}
static class ProductValidationError
{
public enum Code
{
InvalidId = 10,
InvalidName = 20
}
public static readonly Error InvalidId =
Error.New((int)Code.InvalidId, "Id should be >0");
public static readonly Error InvalidName =
Error.New((int)Code.InvalidName, "Name should not be null or empty");
}
static class Printer
{
public static IO<Unit> info(string msg) =>
liftIO(() =>
{
Console.WriteLine(msg);
return Task.FromResult(unit);
});
public static IO<Unit> error(Error e) =>
liftIO(() =>
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(e.Message);
Console.ForegroundColor = ConsoleColor.Gray;
return Task.FromResult(unit);
});
public static IO<Unit> printProduct(Product product) =>
liftIO(() =>
{
Console.WriteLine($"{product.Id}: {product.Name}"); // using built-in Console directly as I'm busy
return Task.FromResult(unit);
});
public static IO<Unit> printProducts(List<Product> products) =>
products.AsIterable().Traverse(printProduct).IgnoreF().As();
}
// Data access
class Product
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public string Name { get; set; }
};
class AppDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseInMemoryDatabase("test");
}
}
static class AppDbContextExtensions
{
public static void AddFakeData(this AppDbContext dbCtx)
{
dbCtx.AddRange(Enumerable.Range(0, 5).Select(id => new Product()
{
Id = id,
Name = $"Product #{id}"
}));
dbCtx.SaveChanges();
}
}
// Db monad related
record DbEnv(AppDbContext DbContext);
record Db<A>(ReaderT<DbEnv, IO, A> RunDb) : K<Db, A>
{
public Db<B> Select<B>(Func<A, B> m) => this.Kind().Select(m).As();
public Db<C> SelectMany<B, C>(Func<A, K<Db, B>> b, Func<A, B, C> p) =>
this.Kind().SelectMany(b, p).As();
public Db<C> SelectMany<B, C>(Func<A, K<IO, B>> b, Func<A, B, C> p) =>
this.Kind().SelectMany(b, p).As();
public static Db<A> operator |(Db<A> ma, Db<A> mb) =>
ma.Catch(mb).As();
public static Db<A> operator |(Db<A> ma, CatchM<Error, Db, A> mb) =>
ma.Catch(mb).As();
public static Db<A> operator |(Db<A> ma, Pure<A> mb) =>
ma.Catch(mb).As();
public static Db<A> operator |(Db<A> ma, Error mb) =>
ma.Catch(mb).As();
}
static class DbExtensions
{
public static Db<A> As<A>(this K<Db, A> kind) =>
(Db<A>)kind;
public static K<ReaderT<DbEnv, IO>, A> Run<A>(this K<Db, A> db) =>
db.As().RunDb;
public static IO<A> Run<A>(this K<Db, A> db, DbEnv env) =>
db.Run().Run(env).As();
public static Db<A> MapFail<A>(this K<Db, A> db, Func<Error, Error> f) =>
db.Catch(f).As();
}
abstract partial class Db : Monad<Db>, Readable<Db, DbEnv>, Fallible<Db>
{
public static K<Db, B> Map<A, B>(Func<A, B> mapFunc, K<Db, A> db) =>
new Db<B>(db.As().RunDb.Map(mapFunc).As());
public static K<Db, A> Pure<A>(A value) =>
new Db<A>(ReaderT<DbEnv, IO, A>.Pure(value));
public static K<Db, B> Apply<A, B>(K<Db, Func<A, B>> applyFunc, K<Db, A> db) =>
new Db<B>(applyFunc.As().RunDb.Apply(db.As().RunDb));
public static K<Db, B> Bind<A, B>(K<Db, A> db, Func<A, K<Db, B>> bindFunc) =>
new Db<B>(db.Run().Bind(x => bindFunc(x).Run()).As());
public static K<Db, A> Asks<A>(Func<DbEnv, A> f) =>
new Db<A>(ReaderT.asks<IO, A, DbEnv>(f));
public static K<Db, A> Local<A>(Func<DbEnv, DbEnv> f, K<Db, A> db) =>
new Db<A>(ReaderT.local(f, db.Run().As()));
public static K<Db, A> Fail<A>(Error error) =>
new Db<A>(ReaderT.liftIO<DbEnv, IO, A>(error));
public static K<Db, A> Catch<A>(K<Db, A> fa, Func<Error, bool> Predicate, Func<Error, K<Db, A>> Fail) =>
from env in Readable.ask<Db, DbEnv>()
from res in fa.As()
.RunDb
.runReader(env)
.Catch(Predicate, x => Fail(x).As().RunDb.runReader(env))
select res;
// WARNING: personally I'm very not sure about some of the ways of lifting below
public static K<Db, A> Lift<A>(Validation<Error, A> ma) =>
new Db<A>(
ReaderT<DbEnv, IO, A>.Lift(
ma.Match(
Succ: IO.pure,
Fail: IO.fail<A>)));
public static K<Db, A> LiftIO<A>(ValueTask<A> f) =>
new Db<A>(
ReaderT.liftIO<DbEnv, IO, A>(
IO.liftAsync(async envIO => await f.ConfigureAwait(false))));
public static K<Db, A> LiftIO<A>(Task<A> f) =>
new Db<A>(
ReaderT.liftIO<DbEnv, IO, A>(
IO.liftAsync(async envIO => await f.ConfigureAwait(false))));
public static K<Db, A> LiftIO<A>(IO<A> ma) =>
new Db<A>(
ReaderT.liftIO<DbEnv, IO, A>(ma));
public static K<Db, A> Lift<A>(Option<A> ma) =>
new Db<A>(
ReaderT.liftIO<DbEnv, IO, A>(IO.lift(ma.ToFin())));
}
abstract partial class Db
{
public static Db<DbEnv> Ask() =>
Readable.ask<Db, DbEnv>().As();
public static Db<Context> Ctx<Context>() where Context : DbContext =>
Ask().Select(env => (env.DbContext as Context)!);
public static Db<Unit> Save() =>
from env in Ask()
from _ in liftIO(() => env.DbContext.SaveChangesAsync())
select unit;
} And the output:
|
Beta Was this translation helpful? Give feedback.
I understand what louthy says like this: you don't need to use custom
OptDb<A>
orOptValidDb<A>
types, but instead you can use IO's capabilities and existing traits for fail/alt values (in your case, Fallible is enough). So, instead of usingDb<Option<Product>>
when trying to query an item, you may return a specific error and catch it on the caller's side; and instead of using `Db<Validation<Error, Option>> when trying to update an item, you may return just specific Errors errors indicating what went wrong. And yes, you can not only "fast-worward" (I guess you mean "short-circuit") the computation, but also accumulate errors.By the way, your thoughts on making custom subtypes of db opera…