-
-
Notifications
You must be signed in to change notification settings - Fork 424
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New Library - Wraps repetitive code with LanguageExt #844
Comments
Hi Greg, IMHO: I think a single helper library is problematic because it might result in "endless scope" and incompleteness. If there is a general feature which seems missing in If you solved a specific thing like e.g. interoperability with Azure SDK I suggest to create a helper package specifically for that use case and make them available independently of each other. (Probably you should check with Paul about naming it Third way IMHO is to try to add features in other libraries (via PR's) that make it easier to wire functional / LanguageExt code into it -- reducing the need to have overhead code like wrappers. Many things are already supported by LanguageExt out of the box like turning exception throwing code into Finally I myself often end up with a collection of extension methods that I just "copy & paste" between solutions (buying the disadvantages of ignoring DRY principle). I still think this is a valid way to learn whether the code is universal enough to be a candidate for some own separate library or a PR to the core library. Just my 2 cents. |
@StefanBertels Thanks for the feedback, thats exactly what I was looking for and many of your concerns I've been thinking about as well.
100% agree. Completeness is generally the first thing I've been pushing toward, but I hadn't thought of your other two points:
I probably haven't put enough thought into it, but for something like the HttpClient I don't know how you avoid adding new types, maybe extension methods?? Seem like there may be challenges there. Also - how to handle
@louthy - Any thoughts on the subject or a namespace convention that could be used to wrap |
This overlaps somewhat with what I'm trying to achieve with the new The To give you an example, I recently used the new effect monads in a internal project and needed something that converts text to a /// <summary>
/// Interface that specifies the behaviour of a JSON IO sub-system
/// </summary>
public interface JsonIO
{
/// <summary>
/// Convert a string to a Json
/// </summary>
Json Convert(string json);
}
/// <summary>
/// Trait that tells a runtime it has the capacity to deal with JSON
/// </summary>
/// <typeparam name="RT">Runtime</typeparam>
public interface HasJson<RT>
{
Eff<RT, JsonIO> Json { get; }
} The /// <summary>
/// JSON union
/// </summary>
[Union]
public interface Json
{
Json Record(HashMap<string, Json> Fields);
Json StringValue(string Value);
Json IntValue(int Value);
Json LongValue(long Value);
Json DoubleValue(double Value);
Json BoolValue(bool Value);
Json DateValue(DateTime Value);
Json GuidValue(Guid Value);
Json NoValue();
Json Array(Seq<Json> Values);
} There then needs to be a 'live' implementation of the public struct LiveJsonIO : JsonIO
{
public static readonly JsonIO Default = new LiveJsonIO();
public Json Convert(string value) =>
Walk(JsonConvert.DeserializeObject(value));
Json Walk(object obj) =>
obj switch
{
null => new NoValue(),
JArray jarr => WalkArray(jarr),
JObject jobj => WalkObject(jobj),
JValue jval => WalkValue(jval),
_ => throw new NotImplementedException()
};
Json WalkValue(JValue val) =>
val.Type switch
{
JTokenType.Boolean => new BoolValue((bool)val),
JTokenType.Date => new DateValue((DateTime)val),
JTokenType.Float => new DoubleValue((double)val),
JTokenType.Guid => new GuidValue((Guid)val),
JTokenType.Integer => WalkInt((long)val),
JTokenType.String => new StringValue((string)val),
_ => new NoValue()
};
Json WalkInt(long val) =>
val < (long) Int32.MinValue || val > (long) Int32.MaxValue
? JsonCon.LongValue(val)
: JsonCon.IntValue((int) val);
Json WalkArray(JArray jarr) =>
new Array(jarr.Map(Walk).ToSeq().Strict());
Json WalkObject(IDictionary<string, JToken> jobj) =>
new Record(toHashMap(
jobj.AsEnumerable()
.Map(pair => (pair.Key, Walk(pair.Value)))));
} Access to a generic /// <summary>
/// JSON IO
/// </summary>
public static class JsonEff
{
/// <summary>
/// Convert a string to a `Json`
/// </summary>
public static Eff<RT, Json> convert<RT>(string json) where RT : struct, HasJson<RT> =>
default(RT).Json.Map(j => j.Convert(json));
} That constrains And finally, the project will need a runtime that will make all effects and IO available: /// <summary>
/// IO runtime
/// </summary>
public struct Runtime : HasCancel<Runtime>, HasJson<Runtime>, HasFile<Runtime>
{
readonly CancellationTokenSource cancellationTokenSource;
public CancellationToken CancellationToken { get; }
/// <summary>
/// Construct a runtime
/// </summary>
/// <param name="cancellationTokenSource"></param>
public Runtime(CancellationTokenSource cancellationTokenSource) =>
(this.cancellationTokenSource, CancellationToken) = (cancellationTokenSource, cancellationTokenSource.Token);
/// <summary>
/// Cancellation token source
/// </summary>
public Eff<Runtime, CancellationTokenSource> CancellationTokenSource =>
SuccessEff(cancellationTokenSource);
/// <summary>
/// Make a new cancellation toke
/// </summary>
public Runtime LocalCancel =>
new Runtime(new CancellationTokenSource());
/// <summary>
/// JSON serialisation provider
/// </summary>
public Eff<Runtime, JsonIO> Json =>
SuccessEff(LiveJsonIO.Default);
/// <summary>
/// Text encoding
/// </summary>
public Eff<Runtime, Encoding> Encoding =>
SuccessEff(System.Text.Encoding.Default);
/// <summary>
/// File IO
/// </summary>
public Aff<Runtime, FileIO> FileAff =>
SuccessAff(LanguageExt.LiveIO.FileIO.Default);
/// <summary>
/// File IO
/// </summary>
public Eff<Runtime, FileIO> FileEff =>
SuccessEff(LanguageExt.LiveIO.FileIO.Default);
} You can see the traits in the inheritance list: If you look at couple of functions that use this: static Aff<RT, Unit> Do<RT>(string path, string rootClass)
where RT : struct, HasCancel<RT>, HasJson<RT>, HasFile<RT> =>
from tx in IO.File.readAllText<RT>(path)
from js in JsonEff.convert<RT>(JsonEff.many(tx))
from sc in EffMaybe(() => Generate(js, rootClass))
from _1 in IO.File.writeAllText<RT>(Path.ChangeExtension(path, "cs"), sc)
from _2 in IO.File.writeAllText<RT>(Path.ChangeExtension(path, "csproj"), MakeProjectFile(path))
select unit;
static Fin<string> Generate(Json json, string rootClass) =>
from tys in Infer.Run(json, rootClass, default)
from src in Generator.Run(tys)
select src; You can see that These are the BCL methods I've done so far, that will grow as I build up to v4 of lang-ext (and so will cover the
To get back to the point of this request. I would expect that for anything outside of the BCL that is IO or a side-effect of any kind, or might require application wide injection (like my JSON example), that separate libraries would be created that would provide the traits needed to compose runtimes (like In terms of name-spacing, I'm replacing |
@louthy Holy shit. Just spent the night working through your examples. I heard about Effects in 2018 from John DeGoes in Scala and probably used them in Purescript... but damn, I never understood them until now. It reminds me of learning Dependency Injection... seemed complicated but provided IoC. This'll take a day to grok it - but without the shitty magic of DI !!!! This is also awesome because I've been working on a data and logic layer to an API... and I'm pretty sure this solves all the issues I've been trying to deal with trying to do FP. I'll play with this in that project and get back to you. The only concern (initially) is that it seems like you have to buy in fully to Eff/Aff + Runtime, instead of just pulling it in when needed, which may makes a whole project require it and training FP nubes is even harder, but seems like the tradeoff is worth it. Here's an even simpler version using using System;
using System.Linq;
using LanguageExt;
using LanguageExt.Common;
using static LanguageExt.Prelude;
using LanguageExtCommon;
using Xunit;
namespace LanguageExt.HotDamn
{
// specifies the behaviour of IO sub-system
public interface PwdIO
{
string Pwd();
}
// Trait
public interface HasPwd<RT>
{
Eff<RT, PwdIO> EffPwd { get; }
}
public struct LivePwdIO : PwdIO
{
public static readonly PwdIO Default = new LivePwdIO();
public string Pwd() =>
System.IO.Directory.GetCurrentDirectory();
// throw new Exception("AHHHHHHHHH!!!");
}
public static class PwdEff
{
public static Eff<RT, string> pwd<RT>() where RT : struct, HasPwd<RT> =>
default(RT).EffPwd.Map(p => p.Pwd());
}
public struct Runtime2 : HasPwd<Runtime2>
{
public Eff<Runtime2, PwdIO> EffPwd =>
SuccessEff(LivePwdIO.Default);
}
public class PwdEffTests
{
static Eff<RT, string> DoThePwd<RT>()
where RT : struct, HasPwd<RT> =>
from pwd in PwdEff.pwd<RT>()
select pwd;
[Fact]
public void Test()
{
var env = new Runtime2();
var a = DoThePwd<Runtime2>();
var b = a.RunIO(env);
Assert.True(b.IsSucc);
Assert.Equal(FinSucc("/Users/.../project/test/bin/Debug/netcoreapp3.1"), b.First());
}
}
} |
Yes, this overlaps with ZIO in Scala, and the
In some ways that's the point. It's to be opinionated about dealing with the world's side-effects. It's been a long running theme on this repo where I get questions about how to do IO and deal with state and side-effects. This is one way, but it will be the most supported and optimised way. It is of-course possible to just call In Haskell, for example, there's your IO code and then there's your pure-code. Very much Haskell engineers try to push the IO to the edges so it doesn't pollute their nice pure code. This is kind-of the point of the It can also be used to completely limit what a subsystem can do. Here we dictate the capabilities of the various layers within our enterprise application. This groups the traits together: // UI layer capabilities
public interface HasUI<RT> : HasCancel<RT>, HasJson<RT>, HasWeb<RT>
{
}
// Business logic layer capabilities
public interface HasBusLayer<RT> : HasCancel<RT>, HasFile<RT>, HasMessaging<RT>
{
}
// Data layer capabilities
public interface HasDataLayer<RT> : HasCancel<RT>, HasDatabase<RT>, HasMessaging<RT>
{
} We can then still just define one runtime, but use the layer traits instead public struct Runtime : HasUI<Runtime>, HasBusLayer<Runtime>, HasDataLayer<Runtime>
{
...
} That means the constraints are then easier to work with (if slightly less declarative): static Aff<RT, Unit> Do<RT>(string path, string rootClass)
where RT : struct, HasDataLayer<RT>=>
...; |
Love it, sounds great to me. Looks like I'll be spending the morning playing with this, but three more questions at your convenience:
If you have any more sample code/projects you could dump my way, I may put a project together to illustrate this and get some feedback?? - and submit as a PR?? |
That's the way. But, in theory, an application only ever needs one call to
The I started a massive refactor of my
I'm midway through this refactor, so I wouldn't try running it (or even compiling it), but it's a useful reference. |
Update: Sample project started, initially with the data layer. Very much a work in progress. Everything interesting is in
|
Some ideas to show how you could do the config different: public struct DataLayerRuntime : HasCancel<DataLayerRuntime>, HasSqlDb<DataLayerRuntime>
{
readonly CancellationTokenSource cancellationTokenSource;
readonly string connectionString;
public CancellationToken CancellationToken { get; }
public static DataLayerRuntime New(string connectionString) =>
new DataLayerRuntime(connectionString, new CancellationTokenSource());
DataLayerRuntime(string connectionString, CancellationTokenSource cancellationTokenSource) =>
(this.connectionString, this.cancellationTokenSource, CancellationToken) =
(connectionString, cancellationTokenSource, cancellationTokenSource.Token);
public DataLayerRuntime LocalCancel =>
new DataLayerRuntime(connectionString, new CancellationTokenSource());
public Eff<DataLayerRuntime, CancellationTokenSource> CancellationTokenSource =>
Eff<DataLayerRuntime, CancellationTokenSource>(env => env.cancellationTokenSource);
public Aff<DataLayerRuntime, SqlDbIO> AffSqlDb =>
EffSqlDb.ToAsync();
public Eff<DataLayerRuntime, SqlDbIO> EffSqlDb =>
Eff<DataLayerRuntime, SqlDbIO>(env => LiveSqlDbIO.New(env.connectionString));
} This means the global environment is part of your runtime and is automatically threaded through the system. The global atoms are valid too (although they do make future assumptions for you, that there will only ever be one configuration needed). A better way to access the configurations is to realise that reading a global value is also a side-effect, and so instead of public static class ConfigurationStore
{
static readonly Atom<Option<Configuration>> configMap = Atom(Option<Configuration>.None);
public static Eff<Unit> SetConfig(Configuration config) =>
Eff(() => ignore(configMap.Swap(_ => config)));
static Eff<A> NotInitialised<A>() =>
FailEff<A>(Error.New("Configuration not initialised"));
public static Eff<string> ConnectionString =>
configMap.Value
.Map(c => c.ConnectionString)
.Match(SuccessEff, NotInitialised<string>);
}
Then the runtime access to the public Aff<DataLayerRuntime, SqlDbIO> AffSqlDb =>
from conn in ConfigurationStore.ConnectionString
select LiveSqlDbIO.New(conn);
public Eff<DataLayerRuntime, SqlDbIO> EffSqlDb =>
from conn in ConfigurationStore.ConnectionString
select LiveSqlDbIO.New(conn); Or alternatively, use public Aff<DataLayerRuntime, SqlDbIO> AffSqlDb =>
ConfigurationStore.ConnectionString.Bind(LiveSqlDbIO.New);
public Eff<DataLayerRuntime, SqlDbIO> EffSqlDb =>
ConfigurationStore.ConnectionString.Bind(LiveSqlDbIO.New); |
This is an alternative approach to the public static class SqlDbAff<RT>
where RT : struct, HasSqlDb<RT>
{
public static Eff<RT, string> pwd =>
default(RT).SqlDb.Map(p => p.Pwd());
public static Aff<RT, T> querySingle<T>(string query, object param) =>
default(RT).SqlDb.MapAsync(p => p.QuerySingle<T>(query, param));
} This can have some benefits if you ever want to fix the using static SqlDbAff<YourRuntime>; It can also make usage of the functions a little clearer and use of other generic arguments easier. |
Fantastic! I can't decide whether I like adding the connection string from the constructor or global state. Conceptually passing it via the constructor removes any dependencies, which I like. But the last several small jobs I've used a pattern to pull EnvVars and add them to a Configuration object which is fine being global. Representing the config access as an Effect made sense too, I think I was going that direction and got stuck. Exposing the ConnectionString explicitly is really nice too, hadn't thought about that. I guess my next step is one of two things: to try and compose two runtimes, or add an API and see how to get the runtime stitched in. Does it make any sense to put AspNet (the API system) into a Runtime?? Seems like it would be a pain/not feasible. Or is there a way we can represent/handle its Effects? Or do we just have it call into Runtimes ... via DI?? eek. As always, thanks for all the help. |
Obviously it depends entirely on how much you want to buy into a system like this, but side-effects are:
I don't use ASP.NET Core on a regular basis, so I don't know what's involved there. But an abstraction layer on-top of any system like that will always give you options further down the track. My goal is to wrap up all the BCL, so I guess that gives an idea of the potential for a The other thing to note is that you don't have to use the public static class File
{
public static Eff<string> readAllText(string path) =>
Eff(() => System.IO.File.ReadAllText(path));
}
public static class Math
{
public static Eff<double> divide(double x, double y) =>
EffMaybe(() => y == 0
? FinFail<double>(Error.New("Divide by zero"))
: FinSucc(x / y ));
} That doesn't have the dependency injection benefits, but allows you to easily lift behaviour into the effect system - you could create a trait for those later. You could even do this: public static class File
{
public static Func<string, Eff<string>> readAllText =
path =>
Eff(() => System.IO.File.ReadAllText(path));
}
public static class Math
{
public static Func<double, double, Eff<double>> divide =
(x, y) =>
EffMaybe(() => y == 0
? FinFail<double>(Error.New("Divide by zero"))
: FinSucc(x / y));
} Which would allow for the static functions to be set (in unit tests for example). Not exactly pretty, but avoids the need for runtime traits. The use of the runtime environment to store config is valuable because it allows you to switch it depending on context. This is an example of a runtime that allows a user to be picked by ID, which can then be used to put the rest of the computation in the context of the user. [Record]
public class UserEnv
{
public readonly string Name;
public readonly string ConnectionString;
}
[Record]
public class Env
{
public readonly HashMap<int, UserEnv> Users;
public readonly Option<UserEnv> User;
}
public interface HasUsers<RT>
{
Fin<RT> SetCurrentUser(int id);
Eff<RT, UserEnv> CurrentUser { get; }
Eff<RT, Unit> AssertUserExists(int id);
}
public struct Runtime : HasCancel<Runtime>, HasUsers<Runtime>
{
readonly CancellationTokenSource cancellationTokenSource;
readonly Env env;
public CancellationToken CancellationToken { get; }
public static Runtime New(Env env) =>
new Runtime(env, new CancellationTokenSource());
Runtime(Env env, CancellationTokenSource cancellationTokenSource) =>
(this.env, this.cancellationTokenSource, CancellationToken) =
(env, cancellationTokenSource, cancellationTokenSource.Token);
public Runtime LocalCancel =>
new Runtime(env, new CancellationTokenSource());
public Eff<Runtime, CancellationTokenSource> CancellationTokenSource =>
Eff<Runtime, CancellationTokenSource>(env => env.cancellationTokenSource);
public Fin<Runtime> SetCurrentUser(int id) =>
env.Users.ContainsKey(id)
? FinSucc(new Runtime(env.With(User: env.Users[id])))
: FinFail<Runtime>(Error.New("Invalid user ID"));
public Eff<Runtime, UserEnv> CurrentUser =>
EffMaybe<Runtime, UserEnv>(
env =>
env.env.User.Match(
Some: FinSucc,
None: FinFail<UserEnv>(Error.New("Current user not set"))));
public Eff<Runtime, Unit> AssertUserExists(int id) =>
EffMaybe<Runtime, Unit>(
rt => rt.env.Users.ContainsKey(id)
? FinSucc(unit)
: FinFail<Unit>(Error.New("User doesn't exist")));
}
public static class Users<RT> where RT : struct, HasCancel<RT>, HasUsers<RT>
{
public static Aff<RT, A> withUser<A>(int id, Aff<RT, A> ma) =>
from _ in default(RT).AssertUserExists(id)
from r in localAff<RT, RT, A>(env => env.SetCurrentUser(id).IfFail(env), ma)
select r;
public static Eff<RT, A> withUser<A>(int id, Eff<RT, A> ma) =>
from _ in default(RT).AssertUserExists(id)
from r in localEff<RT, RT, A>(env => env.SetCurrentUser(id).IfFail(env), ma)
select r;
public static Eff<RT, UserEnv> user =>
from env in runtime<RT>()
from usr in env.CurrentUser
select usr;
public static Eff<RT, string> userName =>
from usr in user
select usr.Name;
public static Eff<RT, string> userConnectingString =>
from usr in user
select usr.ConnectionString;
} If we define an operation that expects a user to be set: var userOperation = from name in Users<Runtime>.userName
from conn in Users<Runtime>.userConnectingString
select (name, conn); Then it will only work if it's been wrapped in something that has picked the user: var res = Users<Runtime>.withUser(123, userOperation); So you could imagine a whole request/response loop having its context set. Including details of the request, details of the logged-in user, if any, etc. If there isn't an available user, it automatically fails gracefully, which could have security benefits. But the code itself does't care where or how that is managed, it becomes entirely part of the built-in behaviour of the monadic computation. In the
|
Do you use something other than ASPNet? Or is most your work just back end without much API. Just wondering. That gives me a ton to think about. That explanation is helpful - I definitely still don't fully grok the implications but that helps and more implementation will too. The user example was a perfect illustration because I'm working on a Shopping Cart/ECommerce platform right now which all revolves around a user. I'm going to take that and pull it into the example app as the business layer to see if I can better understand what you discussed. Regarding the BCL work, last night I worked on seeing how an Environmental Variables interface would work. In the project that kicked off this conversation, I used the Validation applicative to grab a set of environmental variables, potentially parse them (int, bool, Url), and put them into a
|
Our system is 16 years old, so it was built on the original ASP.NET. But very early on I stripped it back to the bare minimum and built a bespoke type-safe UI system on-top of it. And so, day-to-day, we don't see ASP.NET front and centre.
There's a more elegant example in the If you look at /// <summary>
/// Access to the echo environment
/// </summary>
public EchoEnv EchoEnv { get; }
/// <summary>
/// Set the SystemName
/// </summary>
public RedisRuntime SetEchoEnv(EchoEnv echoEnv) =>
new RedisRuntime(
source,
echoEnv);
/// <summary>
/// Use a local environment
/// </summary>
/// <remarks>This is used as the echo system steps into the scope of various processes to set the context
/// for those processes</remarks>
public RedisRuntime LocalEchoEnv(EchoEnv echoEnv) =>
new RedisRuntime(
source,
echoEnv); Then in public class EchoEnv
{
public readonly static EchoEnv Default = new EchoEnv(default, default, default, default);
/// <summary>
/// Current system
/// </summary>
public readonly SystemName SystemName;
/// <summary>
/// Current session ID
/// </summary>
internal readonly Option<SessionId> SessionId;
/// <summary>
/// True if we're currently in a message-loop
/// </summary>
internal readonly bool InMessageLoop;
/// <summary>
/// Current request
/// </summary>
internal readonly Option<ActorRequestContext> Request;
/// <summary>
/// Ctor
/// </summary>
internal EchoEnv(
SystemName systemName,
Option<SessionId> sessionId,
bool inMessageLoop,
Option<ActorRequestContext> request) =>
(SystemName, SessionId, InMessageLoop, Request) =
(systemName, sessionId, inMessageLoop, request);
/// <summary>
/// Constructor function
/// </summary>
public static EchoEnv New(SystemName systemName) =>
new EchoEnv(systemName, None, false, None);
/// <summary>
/// Set the request
/// </summary>
internal EchoEnv WithRequest(ActorRequestContext request) =>
new EchoEnv(SystemName, SessionId, true, request);
/// <summary>
/// Set the session
/// </summary>
internal EchoEnv WithSession(SessionId sid) =>
new EchoEnv(SystemName, sid, InMessageLoop, Request);
/// <summary>
/// Set the session
/// </summary>
internal EchoEnv WithSystem(SystemName system) =>
new EchoEnv(system, SessionId, InMessageLoop, Request);
} Which is then made visible in the public interface HasEcho<RT> : HasCancel<RT>, HasEncoding<RT>, HasTime<RT>, HasFile<RT>, HasSerialisation<RT>
where RT :
struct,
HasCancel<RT>,
HasEncoding<RT>,
HasTime<RT>,
HasFile<RT>,
HasSerialisation<RT>
{
/// <summary>
/// Access to the echo environment
/// </summary>
EchoEnv EchoEnv { get; }
/// <summary>
/// Access to the echo IO
/// </summary>
Aff<RT, EchoIO> EchoAff { get; }
/// <summary>
/// Access to the echo IO
/// </summary>
Eff<RT, EchoIO> EchoEff { get; }
/// <summary>
/// Use a local environment
/// </summary>
/// <remarks>This is used as the echo system steps into the scope of various processes to set the context
/// for those processes</remarks>
RT LocalEchoEnv(EchoEnv echoEnv);
} Then these functions in public static Aff<RT, A> localSystem<A>(SystemName system, Aff<RT, A> ma) =>
localAff<RT, RT, A>(env => env.LocalEchoEnv(env.EchoEnv.WithSystem(system)), ma);
public static Eff<RT, A> localSystem<A>(SystemName system, Eff<RT, A> ma) =>
localEff<RT, RT, A>(env => env.LocalEchoEnv(env.EchoEnv.WithSystem(system)), ma);
public static Aff<RT, A> localSystem<A>(ProcessId pid, Aff<RT, A> ma) =>
localSystem(pid.System, ma);
public static Eff<RT, A> localSystem<A>(ProcessId pid, Eff<RT, A> ma) =>
localSystem(pid.System, ma);
public static Aff<RT, A> localSystem<A>(ActorSystem system, Aff<RT, A> ma) =>
localAff<RT, RT, A>(env => env.LocalEchoEnv(env.EchoEnv.WithSystem(system.SystemName)), ma);
public static Eff<RT, A> localSystem<A>(ActorSystem system, Eff<RT, A> ma) =>
localEff<RT, RT, A>(env => env.LocalEchoEnv(env.EchoEnv.WithSystem(system.SystemName)), ma);
public static Aff<RT, A> localContext<A>(ActorRequestContext requestContext, Aff<RT, A> ma) =>
localAff<RT, RT, A>(env => env.LocalEchoEnv(env.EchoEnv.WithRequest(requestContext)), ma);
public static Eff<RT, A> localContext<A>(ActorRequestContext requestContext, Eff<RT, A> ma) =>
localEff<RT, RT, A>(env => env.LocalEchoEnv(env.EchoEnv.WithRequest(requestContext)), ma); That then means static functions can be called when in-context, to get things like the current request: public static Eff<RT, ActorRequestContext> Request =>
from e in EchoEnv
from r in e.Request.Match(SuccessEff, FailEff<ActorRequestContext>(Error.New("Not in a message loop")))
select r; As you can see it can also gracefully fail when called out-of-context. Remember it's an 'environment', not a 'state'. i.e. it works more like the |
With regards to the validation question, and whether multiple errors could be carried, and applicative behaviour added: the short answer is no, it won't support that. The longer answer is I want to keep the effect system as lightweight and optimal as possible, so adding lots of bells and whistles compromises that. The single error type is a bit of a compromise, for sure, but there's ways of adding your own extensions to get around that: public static Aff<Env, D> Apply<Env, A, B, C, D>(this (Aff<Env, A>, Aff<Env, B>, Aff<Env, C>) ms, Func<A, B, C, D> apply)
where Env : struct, HasCancel<Env> =>
AffMaybe<Env, D>(async env =>
{
var t1 = ms.Item1.RunIO(env).AsTask();
var t2 = ms.Item2.RunIO(env).AsTask();
var t3 = ms.Item3.RunIO(env).AsTask();
var tasks = new Task[] {t1, t2, t3};
await Task.WhenAll(tasks);
return (t1.Result.ToValid(), t2.Result.ToValid(), t3.Result.ToValid())
.Apply(apply)
.Match(Succ: FinSucc, Fail: ErrorAggregate<D>);
});
static Validation<Error, A> ToValid<A>(this Fin<A> ma) =>
ma.Match(Succ: Success<Error, A>, Fail: Fail<Error, A>);
static Fin<A> ErrorAggregate<A>(Seq<Error> errs) =>
FinFail<A>(Error.New(new AggregateException(errs.Map(e => (Exception)e)))); The That should give you the behaviour you want without anything additional in the core |
Conceptually I get the 'env' idea, but I spent a day or two implementing some things and failed pretty hard. So I backed off and I wanted to get something very basic working. So the goal was to implement a simple http call to the Github API and deserialize the response. I created 3 IO layers: Json, HttpClient, and Github API. (Sorry, the code is really ugly right now with all the experimentation)
To get it working initially, Also, in Any other thoughts or changes would be very appreciated. |
I think it just clicked with this line of code:
The runtime can have config and that config can be exposed via 'traits'/interfaces on the runtime. The static function knows nothing about the runtime, but can access the traits defined on the I also got rid of all the DI style crap. And realized that the |
Yep. The constraints on NOTE: The trait model isn't totally needed, you could just have a runtime deriving from regular interfaces (rather than the So, the trait system is to provide that indirection. It facilitates the plug-in approach, but also has a nice side-effect of having names like
It's a new way of thinking for the C# world (and therefore I will need to document the hell out of this); so kudos for pushing through with limited support! This fits with the shapes/roles/concepts investigation going on in the |
@louthy I implemented the |
Hi,
|
Rather than review this in-place, I think (if you're up for helping in general - and anyone else), I have setup a new project to manage the progress. I haven't done a full dig into the scope of what's going to be wrapped, but Some complexities come around working with streams. I'm working on a resource management system for the
Obviously for your own code it's up to you. I think I'd prefer to stick with
Yes. It's very much trying to leverage the toolkit of language-ext. Internally it can call
Exceptions are caught automatically and wrapped in an |
Alright, I created a PR for EnvironementIO. I haven't gone back to return HashMap's and did some work on the Option/nullables that'll need some changes. We can discuss the details further in there. The pipes look interesting, once I get my head wrapped around this I'd like to spend some time understanding it better. I took a stab at an HttpClient (partial) implementation but quickly got stuck primarily because the
My guess is that something like the Edit: Note this is out of date since its actively being worked on and changes pushed. Look at main to see the most recent version. |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
Hi Paul,
Recently I've joined a company as tech lead and am rebuilding a platform with C#. Obviously LanguageExt is playing a centrall roll in the code base.
After the 5th small service I found I was re-writing/copy-pasting a large portion of the code like: wrapping
GetEnvironmentalVariable
with Validation, wrapping HttpClient, JSON and Yaml deserialization, and a paging in REST APIs.So I took all the common code and aggregated it into: LanguageExtCommon. We've now used it for the last couple weeks and cut out a ton of repetitive code in our services.
Its still very much in alpha, but I'd image we'll add things like database wrappers (maybe as separate nuget's), Azure SDK, who knows...
Anyways, I just wanted to share this and get any feedback, suggestions, or ideas you may have. Hell, if there's a better name for the project I'm open to it!
https://github.com/gregberns/LanguageExtCommon
cc @StefanBertels (since you seem very active the last couple years)
The text was updated successfully, but these errors were encountered: