What is the best practice to pass global like values, like correlationId, between methods #1362
Replies: 2 comments 2 replies
-
Changing the right type of
If every method returns Basically the above is the (nice) LINQ version of
Method signature for example:
As a general rule of thumb you probably don't want |
Beta Was this translation helpful? Give feedback.
-
@Andras-Csanyi there are a number of quite 'big' questions there. I think it's important to take a step back and remember the motivation for doing FP in C# (or any language). My reasons were that the large OO project I was running had hit a complexity limit. Not so much for the computer, but for our minds. I think OOP is OK at a small scale, but hidden mutable state without proper compositional capabilities, and a lack of declarativity, becomes quite difficult for the human mind to keep track of. This leads to bugs, more difficult refactoring, slower moving teams, etc. This is only getting worse as our OO programs get bigger and more complex. The grey-matter between our ears stays the same but our programs get more complex. The fact we've had to invent AI to help us write code shows we're struggling. Pure Functional Programming discards the notion of mutable-state, welcomes in true composition, and treats everything like a mathematical expression, so that we can use the power of mathematical reasoning and proofs to give our weak and feeble brains a chance of understanding. So, those are the critical elements that we need to work with:
Everything naturally falls out of these guidelines. What happens to your code is that it becomes more robust, easier to maintain, it doesn't bit-rot in the same way, and generally you get to a point where you can 'see' the structure of your code in your mind's eye with much more clarity. And also, everything composes in the same way as terms in a mathematical expression. The 'structure' point above is actually quite profound once you get comfortable with pure-FP. Because you start to see structure everywhere. Not just layers, or services, but between types (natural transformations, functors, etc.) I won't lie and tell you that you will get this straight-away, nobody does, especially if you're coming from an OO world. It takes a little time for your brain to switch into pure-mathematical-expression mode. But, when it does, and it will, you will have that "aha!" moment and it will all become very easy. Much easier than OO. But it does take time. Just as with anything you will need to make your own mistakes and learn the best way to do it. The guidelines are the key checklist to keep with you.
On to your questions:
I know of many private projects, but not many public, so the best place to start is with the Samples. Some sample projects are older than others however, so a good starting point would be:
@StefanBertels answer is correct by the way, but the one thing you will want to consider as you get better at FP is the fact that you're mixing IO and pure code without any declaration of that. IO is one of the 'big problem' issues that makes code unreliable. The ultimate goal is to make our function signatures talk to us and to put IO in a box where we know what it's doing and when it's doing it. A function that returns To get the absolute maximum out of this approach you'll want to wrap your IO up. Either using the Here's an example from my new startup's code. I am writing my own layer for FoundationDB. FoundationDB is a toolkit for building databases, so needs a lot of simple functions. So, I wrapped those up in a public record Db<A>(StateT<DbEnv, IO, A> runDB) : K<Db, A>
{
public IO<(A Value, DbEnv Env)> Run(DbEnv env) =>
runDB.Run(env).As();
public K<Db, B> MapIO<B>(Func<IO<A>, IO<B>> f) =>
from e in Db.dbEnv
from r in f(runDB.Run(e).Map(vs => vs.Value).As())
select r;
public K<Db, A> IfFail(Func<Error, Db<A>> OnFail) =>
from e in Db.dbEnv
from r in MapIO(io => io.As()
.Match(Succ: x => IO.Pure((Value: x, Env: e)),
Fail: x => OnFail(x).Run(e)).Flatten())
from _ in Db.put(r.Env)
select r.Value;
public static Db<A> LiftIO(IO<A> ma) =>
new (StateT<DbEnv, IO, A>.LiftIO(ma));
public static Db<A> operator |(Db<A> ma, Db<A> mb) =>
ma.IfFail(_ => mb).As();
public Db<B> Map<B>(Func<A, B> f) =>
this.Kind().Map(f).As();
public Db<B> Bind<B>(Func<A, Db<B>> f) =>
this.Kind().Bind(f).As();
public Db<C> SelectMany<B, C>(Func<A, Db<B>> bind, Func<A, B, C> project) =>
this.Kind().SelectMany(bind, project).As();
public Db<C> SelectMany<B, C>(Func<A, IO<B>> bind, Func<A, B, C> project) =>
this.Kind().SelectMany(bind, project).As();
public static implicit operator Db<A>(Pure<A> ma) =>
Db.pure(ma.Value).As();
public static implicit operator Db<A>(Fail<Error> ma) =>
Db.fail<A>(ma.Value);
public static implicit operator Db<A>(Fail<string> ma) =>
Db.fail<A>(ma.Value);
public static implicit operator Db<A>(Error ma) =>
Db.fail<A>(ma);
} It wraps up the I then implement the public partial class Db : Monad<Db>, Stateful<Db, DbEnv>
{
static K<Db, B> Monad<Db>.Bind<A, B>(K<Db, A> ma, Func<A, K<Db, B>> f) =>
new Db<B>(ma.As().runDB.Bind(x => f(x).As().runDB));
static K<Db, B> Functor<Db>.Map<A, B>(Func<A, B> f, K<Db, A> ma) =>
new Db<B>(ma.As().runDB.Map(f));
static K<Db, A> Applicative<Db>.Pure<A>(A value) =>
new Db<A>(StateT<DbEnv, IO, A>.Pure(value));
static K<Db, B> Applicative<Db>.Apply<A, B>(K<Db, Func<A, B>> mf, K<Db, A> ma) =>
new Db<B>(mf.As().runDB.Apply(ma.As().runDB).As());
static K<Db, Unit> Stateful<Db, DbEnv>.Put(DbEnv value) =>
new Db<Unit>(StateT.put<IO, DbEnv>(value).As());
static K<Db, Unit> Stateful<Db, DbEnv>.Modify(Func<DbEnv, DbEnv> f) =>
new Db<Unit>(StateT.modify<IO, DbEnv>(f).As());
static K<Db, A> Stateful<Db, DbEnv>.Gets<A>(Func<DbEnv, A> f) =>
new Db<A>(StateT.gets<IO, DbEnv, A>(f).As());
static K<Db, A> MonadIO<Db>.LiftIO<A>(IO<A> ma) =>
new Db<A> (StateT<DbEnv, IO, A>.LiftIO(ma));
public static K<Db, IO<A>> ToIO<A>(K<Db, A> ma) =>
throw new NotImplementedException(); // TODO
} Then I create a 'module' of lightweight functions that interact with the database: public partial class Db
{
/// <summary>
/// Fail constructor
/// </summary>
public static Db<A> pure<A>(A value) =>
new (StateT<DbEnv, IO, A>.Pure(value));
/// <summary>
/// Fail constructor
/// </summary>
public static Db<A> fail<A>(string error) =>
fail<A>((Error)error);
/// <summary>
/// Fail constructor
/// </summary>
public static Db<A> fail<A>(Error error) =>
new (StateT<DbEnv, IO, A>.Lift(IO.fail<A>(error)));
/// <summary>
/// Get the database environment
/// </summary>
public static readonly Db<DbEnv> dbEnv =
Stateful.get<Db, DbEnv>().As();
/// <summary>
/// Map the database environment
/// </summary>
internal static Db<A> gets<A>(Func<DbEnv, A> f) =>
Stateful.gets<Db, DbEnv, A>(f).As();
/// <summary>
/// Put the database environment
/// </summary>
public static Db<Unit> put(DbEnv env) =>
Stateful.put<Db, DbEnv>(env).As();
/// <summary>
/// lift an IO operation onto the monad
/// </summary>
public static Db<A> liftIO<A>(IO<A> ma) =>
new (StateT<DbEnv, IO, A>.LiftIO(ma));
/// <summary>
/// lift an IO operation onto the monad
/// </summary>
internal static Db<A> liftIO<A>(Func<DbEnv, EnvIO, A> f) =>
dbEnv.Bind(env => IO.lift(envIO => f(env, envIO))).As();
/// <summary>
/// lift an IO operation onto the monad
/// </summary>
internal static Db<A> liftIO<A>(Func<EnvIO, A> f) =>
MonadIO.liftIO<Db, A>(IO.lift(f)).As();
/// <summary>
/// lift an IO operation onto the monad
/// </summary>
internal static Db<A> liftIO<A>(Func<DbEnv, EnvIO, Task<A>> f) =>
dbEnv.Bind(env => IO.liftAsync(async envIO => await f(env, envIO).ConfigureAwait(false))).As();
/// <summary>
/// lift an IO operation onto the monad
/// </summary>
internal static Db<A> liftIO<A>(Func<EnvIO, Task<A>> f) =>
MonadIO.liftIO<Db, A>(IO.liftAsync(async e => await f(e).ConfigureAwait(false))).As();
/// <summary>
/// Get the Foundation DB version
/// </summary>
static Db<Unit> fdbVersion =
liftIO(_ =>
{
Fdb.Start(710);
return unit;
}).Memo();
/// <summary>
/// Shutdown the database connection
/// </summary>
/// <remarks>
/// Only shutdown if you're quitting the application
/// </remarks>
public static Db<Unit> shutdown =
liftIO(_ =>
{
Fdb.Stop();
return unit;
});
/// <summary>
/// Connect to the database
/// </summary>
/// <remarks>
/// This is memoised, so it only ever connects *once*. So, never `shutdown` unless you're quitting.
/// </remarks>
public static readonly Db<Unit> connect =
(from ver in fdbVersion
from env in dbEnv
from con in liftIO(async envIO =>
await (env.ClusterFile.IsSome
? Fdb.OpenAsync(new FdbConnectionOptions { ClusterFile = env.ClusterFile.ValueUnsafe()! }, envIO.Token)
.ConfigureAwait(false)
: Fdb.OpenAsync(envIO.Token).ConfigureAwait(false)))
from res in put(env with { Database = Some(con) })
select unit)
.Memo();
/// <summary>
/// Access the database from the monad state
/// </summary>
static readonly Db<IFdbDatabase> db =
gets<IFdbDatabase>(e => e.Database.IsSome
? e.Database.ValueUnsafe()!
: throw new IOException("no connection to database, make sure you call `connect` first"));
/// <summary>
/// Access the transaction from the monad state
/// </summary>
static readonly Db<IFdbTransaction> transaction =
gets<IFdbTransaction>(e => e.Transaction.IsSome && e.Transaction.ValueUnsafe() is IFdbTransaction t
? t
: throw new IOException("not in a readWrite transaction"));
/// <summary>
/// Access the read-only transaction from the monad state
/// </summary>
static readonly Db<IFdbReadOnlyTransaction> readOnlyTransaction =
gets<IFdbReadOnlyTransaction>(e => e.Transaction.IsSome
? e.Transaction.ValueUnsafe()!
: throw new IOException("not in a readOnly transaction"));
/// <summary>
/// Set the subspace we're working in
/// </summary>
public static Db<A> subspace<A>(string leaf, K<Db, A> ma) =>
subspace(FdbPath.Relative(leaf), ma);
/// <summary>
/// Set the subspace we're working in
/// </summary>
public static Db<A> subspace<A>(FdbPath path, K<Db, A> ma) =>
(from t in readOnlyTransaction
from r in liftIO(async (env, eio) =>
{
var provider = env.Provider.ValueUnsafe() ??
throw new IOException("no provider make sure you call `subspace` within a transaction");
var subspace = env.Subspace.IsSome
? t is IFdbTransaction mutableTrans1
? await env.Subspace.ValueUnsafe().CreateOrOpenAsync(mutableTrans1, path)
: await env.Subspace.ValueUnsafe().OpenAsync(t, path)
: t is IFdbTransaction mutableTrans2
? await provider.Root.CreateOrOpenAsync(mutableTrans2, path)
: await provider.Root.OpenAsync(t, path);
var (v, _) = ma.As().Run(env with { Subspace = Some(subspace) }).Run(eio);
return v;
})
select r)
.As();
/// <summary>
/// Read only transaction
/// </summary>
public static Db<A> readOnly<A>(FdbPath subspacePath, K<Db, A> ma) =>
readOnly(subspace(subspacePath, ma));
/// <summary>
/// Read only transaction
/// </summary>
public static Db<A> readOnly<A>(K<Db, A> ma) =>
connect.Bind(
_ =>
liftIO(async (env, eio) =>
{
var db = env.Database.ValueUnsafe() ??
throw new IOException(
"no connection to database, make sure you call `connect` first");
var p = db.AsDatabaseProvider(eio.Token);
var (v, _) = await p.ReadAsync(
t => ma.As().Run(env with { Transaction = Some(t), Provider = Some(p) }).Run(eio).AsTask(),
eio.Token);
return v;
})).As();
/// <summary>
/// Read-write transaction
/// </summary>
public static Db<A> readWrite<A>(FdbPath subspacePath, K<Db, A> ma) =>
readWrite(subspace(subspacePath, ma));
/// <summary>
/// Read-write transaction
/// </summary>
public static Db<A> readWrite<A>(K<Db, A> ma) =>
connect.Bind(
_ => liftIO(async (env, eio) =>
{
var db = env.Database.ValueUnsafe() ??
throw new IOException(
"no connection to database, make sure you call `connect` first");
var p = db.AsDatabaseProvider(eio.Token);
var (v, _) = await p.ReadWriteAsync(
t => ma.As()
.Run(env with
{
Transaction = Some<IFdbReadOnlyTransaction>(t),
Provider = Some(p)
}).Run(eio).AsTask(),
eio.Token);
return v;
})).As();
/// <summary>
/// get a value from the database
/// </summary>
public static Db<Slice> get(string name) =>
liftIO(async (env, _) =>
{
var t = env.Transaction.IsSome
? env.Transaction.ValueUnsafe()!
: throw new IOException("not in a readOnly transaction");
var k = env.MakeKeySlice(name);
var s = await t.GetAsync(k);
return s == Slice.Nil
? throw new KeyNotFound(k.ToStringUtf8() ?? "[null]")
: s;
});
/// <summary>
/// get a value from the database
/// </summary>
public static Db<Slice> get<ID>(string name, ID id) =>
liftIO(async (env, _) =>
{
var t = env.Transaction.IsSome
? env.Transaction.ValueUnsafe()!
: throw new IOException("not in a readOnly transaction");
var k = env.MakeKeySlice(name, id);
var s = await t.GetAsync(k);
return s == Slice.Nil
? throw new KeyNotFound(k.ToStringUtf8() ?? "[null]")
: s;
});
/// <summary>
/// get a value from the database
/// </summary>
public static Db<Slice> get<ID1, ID2>(string name, ID1 id1, ID2 id2) =>
liftIO(async (env, _) =>
{
var t = env.Transaction.IsSome
? env.Transaction.ValueUnsafe()!
: throw new IOException("not in a readOnly transaction");
var k = env.MakeKeySlice(name, id1, id2);
var s = await t.GetAsync(k);
return s == Slice.Nil
? throw new KeyNotFound(k.ToStringUtf8() ?? "[null]")
: s;
});
/// <summary>
/// Set a value in the database
/// </summary>
public static Db<Unit> set<A>(string name, A value) =>
liftIO((env, _) =>
{
var t = env.Transaction.ValueUnsafe() as IFdbTransaction ?? throw new IOException("not in a write transaction");
t.Set(env.MakeKeySlice(name), value.AsSlice());
return unit;
});
/// <summary>
/// Set a value in the database
/// </summary>
public static Db<Unit> set<ID1, A>(string name, ID1 id1, A value) =>
liftIO((env, _) =>
{
var t = env.Transaction.ValueUnsafe() as IFdbTransaction ?? throw new IOException("not in a write transaction");
t.Set(env.MakeKeySlice(name, id1), value.AsSlice());
return unit;
});
/// <summary>
/// Set a value in the database
/// </summary>
public static Db<Unit> set<ID1, ID2, A>(string name, ID1 id1, ID2 id2, A value) =>
liftIO((env, _) =>
{
var t = env.Transaction.ValueUnsafe() as IFdbTransaction ?? throw new IOException("not in a write transaction");
t.Set(env.MakeKeySlice(name, id1, id2), value.AsSlice());
return unit;
});
public static Db<uint> increment32(string name) =>
liftIO(async (env, _) =>
{
var t = env.Transaction.ValueUnsafe() as IFdbTransaction ?? throw new IOException("not in a write transaction");
var k = env.MakeKeySlice(name);
t.AtomicIncrement32(k);
var v = await t.GetAsync(k);
return v.As<uint>().ValueUnsafe();
});
public static Db<uint> increment32<ID1>(string name, ID1 id1) =>
liftIO(async (env, _) =>
{
var t = env.Transaction.ValueUnsafe() as IFdbTransaction ?? throw new IOException("not in a write transaction");
var k = env.MakeKeySlice(name, id1);
t.AtomicIncrement32(k);
var v = await t.GetAsync(k);
return v.As<uint>().ValueUnsafe();
});
public static Db<uint> increment32<ID1, ID2>(string name, ID1 id1, ID2 id2) =>
liftIO(async (env, _) =>
{
var t = env.Transaction.ValueUnsafe() as IFdbTransaction ?? throw new IOException("not in a write transaction");
var k = env.MakeKeySlice(name, id1, id2);
t.AtomicIncrement32(k);
var v = await t.GetAsync(k);
return v.As<uint>().ValueUnsafe();
});
public static Db<ulong> increment64(string name) =>
liftIO(async (env, _) =>
{
var t = env.Transaction.ValueUnsafe() as IFdbTransaction ?? throw new IOException("not in a write transaction");
var k = env.MakeKeySlice(name);
t.AtomicIncrement64(k);
var v = await t.GetAsync(k);
return v.As<ulong>().ValueUnsafe();
});
public static Db<ulong> increment64<ID1>(string name, ID1 id1) =>
liftIO(async (env, _) =>
{
var t = env.Transaction.ValueUnsafe() as IFdbTransaction ?? throw new IOException("not in a write transaction");
var k = env.MakeKeySlice(name, id1);
t.AtomicIncrement64(k);
var v = await t.GetAsync(k);
return v.As<ulong>().ValueUnsafe();
});
public static Db<ulong> increment64<ID1, ID2>(string name, ID1 id1, ID2 id2) =>
liftIO(async (env, _) =>
{
var t = env.Transaction.ValueUnsafe() as IFdbTransaction ?? throw new IOException("not in a write transaction");
var k = env.MakeKeySlice(name, id1, id2);
t.AtomicIncrement64(k);
var v = await t.GetAsync(k);
return v.As<ulong>().ValueUnsafe();
});
} This wraps up calls to the With all of that wrapped up into its own type, I now have an abstraction away from my database that:
The key is that I've wrapped up all the messy IO stuff into something declarative. Every time I interact with the database I work with the The other thing is that every interaction with the database is now composable. So, I can then write functions like these (below) which just return more public class Workspace
{
public static Db<WorkspaceId> registerPerson(EmailAddress email) =>
// Generate a workspace ID
from w in Db.liftIO(MakeId.generate)
// Create an index from the credential to the workspace and mark the
// index as an authenticated owner
from c in Db.subspace(Subspaces.CredentialWorkspaceIndex,
Db.set(email.To(), w, new CredentialWorkspaceIndex(true, true)))
// Make a /w/<id>/s entry that specifies where the spec data used to compile
// the workspace is (folder in this case).
from _ in Db.subspace(Subspaces.Workspace,
Db.subspace(w,
Db.subspace(WorkspaceSubspaces.Spec, makeFolder(w))))
select WorkspaceId.From(w);
public static Db<Unit> makeFolder(string wid) =>
from _1 in Db.set("type", "folder")
from _2 in Db.set("repo", $"/{wid}")
select unit;
} NOTE: how To structure an application you might make several of these types that wrap up domains/sub-systems within your app (UI, Services, APIs, etc.) For example, I have an public record Api<A>(Free<ApiDsl, A> runApi) : K<Api, A>
{
... That DSL encapsulates access to the public abstract record ApiDsl<A> : K<ApiDsl, A>;
public record ApiFail<A>(Error Error) : ApiDsl<A>;
public record ApiDb<A>(Db<A> Action) : ApiDsl<A>;
public record ApiService<A>(Service<A> Action) : ApiDsl<A>; With that I can write my API behaviours that access underlying databases or services: public class Registration
{
/// <summary>
/// Create the user in an invalid state
/// Create the email verification token
/// Email the verification token
/// </summary>
/// <param name="email">User email address</param>
/// <param name="phone">Phone number</param>
/// <param name="password">Hashed and salted password</param>
/// <returns>Unit</returns>
public static Api<Unit> register(EmailAddress email, PhoneNumber phone, Password password) =>
from id in Api.readWrite(from _ in Credential.createUser(email, phone, password)
from t in Verification.generateEmailVerifyId(email)
select t)
from _ in Api.lift(Email.sendVerifyEmail(email, id))
select unit;
/// <summary>
/// Verify the user account using the verification ID (make its `Valid` property `true`)
/// </summary>
/// <param name="verifyId">Verification ID</param>
/// <returns>Unit</returns>
public static Api<Unit> verifyEmail(VerificationId verifyId) =>
from wid in Api.readWrite(from e in Verification.emailFromVerifyId(verifyId)
from _ in Credential.makeUserValid(e)
from w in DAT.Workspace.registerPerson(e)
select w)
from _ in Api.lift(Clone.personWorkspaceFolder(wid))
select unit;
} This automatically manages: security, state, configuration, resource-tracking, DB access, IO, etc. All without having to manually pass values around as arguments. The only arguments you see to functions are the ones needed by the functions themselves. That means it's easy to refactor and restructure based on the changing needs of your application and it usually means a reduction of boilerplate, making everything more declarative.
This is obviously taking the idea as far as it can go. And I certainly wouldn't go all in on this approach until you're comfortable with the guidelines and the motivations. It takes a while for this to all sink in, just remember that passing If you're really interested in seeing how far this can go, then it's worth starting my 'Higher Kinds in C# with language-ext' series, it introduces many of the concepts I list above and talks a little about the philosophy too. I wish I could shortcut the learning, but believe me, that "aha!" moment will come! 👍 |
Beta Was this translation helpful? Give feedback.
-
Hi All,
I am very new in the functional world and it takes a lot of mental energy to wrap my head around things, but I am trying to progress.
I have two questions.
The first one is can you recommend projects written in functional C#? I'd like to take a look at them and making myself more familiar with this type of programming.
The second question is below.
Imagine the situation where you want to log all operation failure in your business process and these log entries are connected by correlationId, when you debug this value helps you to see the sequence of events.
The business process has the following steps:
In imperative would look like this:
What is the good practice in functional world to do this? What I did is I created a context object and all the phases has its own entry in it (input, mapped, saved) and the whole is part of
Either
looks like the below example. Changing the right type between methods didn't feel right.Beta Was this translation helpful? Give feedback.
All reactions