-
-
Notifications
You must be signed in to change notification settings - Fork 423
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
Created Conditional Type #179
Conversation
Note: build failure appears independent of this pull request |
@ncthbrt Hi Nick, sorry for the slow response. I've been investigating some different approaches to this. Mainly because the example code above gave me some concerns. Namely the mixture of fluent and named-parameters. It's a confused style I think, and jars with the existing So I started thinking about it a bit more, and I remembered an answer I'd given on StackOverflow a while back about 'functional IF' statements. They key difference with yours is that that Here's your example above. But note I have broken out the 'then' and the 'else' part of the operation to make it a touch easier to read: static async Task<Either<Error, Lst<Vehicle>>> DownloadVehicles(HttpResponseMessage resp) =>
Right<Error, Lst<Vehicle>>(List<Vehicle>().AddRange(JsonConvert.DeserializeObject<List<Vehicle>>(await resp.Content.ReadAsStringAsync())));
static Either<Error, Lst<Vehicle>> BadGatewayError = new Error(); This is the stand-alone conditional computation. It pre-builds the expression that results in a delegate that you can apply to any value of type static readonly Func<HttpResponseMessage, Task<Either<Error, Lst<Vehicle>>>> DownloadIfValid =
Cond<HttpResponseMessage>(y => y.StatusCode == HttpStatusCode.OK)
.Then(DownloadVehicles)
.Else(BadGatewayError); This is it being used: private Task<Either<Error, Lst<Vehicle>>> GetAllVehicles(RequiredString bearerToken) =>
$"http://somereallycoolservice.com/api/vehicles"
.WithHeader("Authorization", bearerToken)
.WithHeader("Accept", "application/json")
.AllowAnyHttpStatus()
.GetAsync()
.Apply(DownloadIfValid); The private Task<Either<Error, Lst<Vehicle>>> GetAllVehicles(RequiredString bearerToken) =>
DownloadIfValid(
$"http://somereallycoolservice.com/api/vehicles"
.WithHeader("Authorization", bearerToken)
.WithHeader("Accept", "application/json")
.AllowAnyHttpStatus()
.GetAsync()); Here's a much simpler example: var cond = Cond<int>(x => x == 4)
.Then(true)
.Else(false); That can be run like so: bool result = cond(4); // True
bool result = cond(0); // False Or, bool result = 4.Apply(cond); // True
bool result = 0.Apply(cond); // False Because of the compositional nature of the type, you can create many variants. Even one that doesn't have a predicate: // This says the 'subject' of the operation is a string
var subj = Subj<string>();
// To turn it into something that can be run you must close with Else
var cond = subj.Else("no value");
// Run
var a = cond(null); // "no value"
var b = cond("Hello, World"); // "Hello, World" To understand why that is, take a look at the implementation of public delegate Option<B> Cond<A, B>(A input);
public static Cond<A, A> Cond<A>(Func<A, bool> pred) =>
input =>
pred(input)
? Optional(input)
: None;
public static Cond<A, A> Subj<A>() =>
input =>
Optional(input); The 'return type' for If I followed the example above with integers, then it's impossible for it to fail: var cond = Subj<int>().Else(Int32.Max);
var a = cond(0); // 0
var b = cond(1); // 1
... Once you have a 'subject' for testing, then you can use some of the more advanced operators: var cond = Subj<int>().Where(x => x >0 && x < 100)
.Then("In range")
.Else("Out of range");
var a = cond(0); // "Out of range"
var b = cond(100); // "Out of range"
var c = cond(50); // "In range" When you look at it like this, you may realise that var cond = Subj<int>().Where(x => x >0 && x < 100)
.Select("In range")
.Else("Out of range"); And can be LINQified: var cond = (from x in Subj<int>()
where x > 0 && x < 100
select "In range").Else("Out of range");
var a = cond(0); // "Out of range"
var b = cond(100); // "Out of range"
var c = cond(50); // "In range" Another problem I found when looking at your original is I couldn't tell what would happen if I used multiple var cond = Subj<int>().Where(x => x > 0)
.Where(x => x < 100)
.Then("In range")
.Else("Out of range"); But to make it even easier I have added var cond = Subj<int>().All(x => x > 0, x => x < 100)
.Then("In range")
.Else("Out of range");
var vowels = Subj<char>().Map(Char.ToLower)
.Any(x => x == 'a', x => x == 'e', x => x == 'i', x => x == 'o', x => x == 'u')
.Then("Is a vowel")
.Else("Is a consonant");
One thing you may have noticed is that the Once the computation is asynchronous, you can't go back to being synchronous. However synchronous operations can be done on the result of an asynchronous operation. i.e. Task<int> GetIntegerValue(int x) => Task.FromResult(x);
var cond = Cond<int>(async x => (await GetIntegerValue(x)) > 0).Then(x => 1).Else(x => 0);
var a = await cond(100); // 1
var b = await cond(-100); // 0 In the above examples the You can use any combination of synchronous and asynchronous and it will 'just work'. Here are some async tests from the unit-tests: var cond1 = Cond<int>(x => T(x == 4)).Then(true).Else(false);
var cond2 = Subj<int>().Where(x => T(x == 4)).Then(true).Else(false);
var cond3 = Subj<int>().Any(x => T(x == 4), x => T(x > 4)).Then(true).Else(false);
var cond4 = Subj<int>().All(x => T(x > 0), x => T(x < 10)).Then(true).Else(false);
var cond5 = Subj<int>().Where(x => T(x == 4)).Then(true).Else(false);
var cond6 = Subj<int>().Any(x => T(x == 4), x => T(x > 4)).Then(_ => T(true)).Else(_ => false);
var cond7 = Subj<int>().Any(x => T(x == 4), x => T(x > 4)).Then(_ => T(true)).Else(_ => T(false));
var cond8 = Subj<int>().Any(x => T(x == 4), x => T(x > 4)).Then(T(true)).Else(false);
var cond9 = Subj<int>().Any(x => T(x == 4), x => T(x > 4)).Then(T(true)).Else(T(false));
var condA = Cond<int>(x => x == 4).Then(_ => T(true)).Else(false);
var condB = Cond<int>(x => x == 4).Then(true).Else(_ => T(false));
var condC = Cond<int>(x => x == 4).Then(_ => T(true)).Else(_ => false);
static Task<A> T<A>(A value) =>
Task.FromResult(value); What's interesting here is that I feel overall this is a stronger solution, which is more composable, causes less memory allocation, and has a more elegant API. But because I don't use this day-in/day-out and it seems you have been, I'd be interested to know your views on this. I have implemented it in a separate branch called Please let me know your thoughts. (after writing all this, I have a sneaky suspicion that I've just re-invented the continuation monad! - so perhaps a name-change is coming. I've gotten a bit too close to it to judge) |
This is amazing :) |
That is beautiful! (Thank you both.) |
@ncthbrt @NickDarvey Thanks for the kind words :) @NickDarvey Not sure when I'll get this released at the moment. I've just started the process of migrating from xproj/csproj/project.json to the new csproj format (in VS2017 RC). And predictably auto-migration doesn't work, and there seems to be some problems with per-framework references (or the lack of them). So it could be a while until I sort this out fully. The two files Going to close this pull request now. |
The conditional type is in the spirit of the lisp construct. I created it because I'm combining language-ext with a lot of other fluent libraries and it is quite handy to evaluate a chain of conditions inline without having to use if/else.
For example (lightly edited from production code):