Skip to content
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

Closed
wants to merge 1 commit into from
Closed

Created Conditional Type #179

wants to merge 1 commit into from

Conversation

ncthbrt
Copy link

@ncthbrt ncthbrt commented Dec 4, 2016

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):

        private Task<Either<Error, Lst<Vehicle>>> GetAllVehicles(RequiredString bearerToken) =>
            $"http://somereallycoolservice.com/api/vehicles"
                .WithHeader("Authorization", bearerToken)
                .WithHeader("Accept", "application/json")
                .AllowAnyHttpStatus()
                .GetAsync()
                .Cond<HttpResponseMessage, Either<Error, Lst<Vehicle>>>
                (
                    condition: y => y.StatusCode == HttpStatusCode.OK,
                    returns: async y => 
                         List<AuthorityAgency>().AddRange(JsonConvert.DeserializeObject<List<AuthorityAgency>>(await y.Content.ReadAsStringAsync()))
                )
                .Else
                (
                    _ => new BadGatewayError(_transportApiOptions.ManagementUrl) as Error
                );

@ncthbrt
Copy link
Author

ncthbrt commented Dec 4, 2016

Note: build failure appears independent of this pull request

@louthy
Copy link
Owner

louthy commented Jan 13, 2017

@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 opt.Match(Some: ..., None: ...) and opt.Some(...).None(...).

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 Cond<A, B> is a delegate that you pass a value to and you get the result of the 'then' or 'else' parts of the expression returned. One benefit of that is that you can pre-build the conditional computations, re-use them, compose them, etc. So for example you could build a 'library of policies' to apply to values-of-a-type, or pass them as arguments to functions for inversion of control.

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 HttpResponseMessage

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 Apply function facilitates the fluent application of the HttpResponseMessage to the DownloadIfValid conditional computation. You could just as easily do this:

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 Cond and Subj

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 Cond is an Option<A>. So even without a conditional predicate the Cond operation can fail (if it sees a null value).

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 Then is just Select or Map:

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 Cond clauses fluently chained. I was unclear if the returns would apply if the first one passed and the second one didn't. I felt this was unsatisfactory. I think most people understand that a series of where clauses are the equivalent of And, so the behaviour from this is obvious (I hope!):

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 All and Any operators:

var cond = Subj<int>().All(x => x > 0, x => x < 100)
                      .Then("In range")
                      .Else("Out of range");

All takes an array of predicates, and all must be true for the Then computation to run.

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");

Any returns true if any of the predicates are true. This is Or behaviour.

One thing you may have noticed is that the Then and Else all take values. They can also take delegates that are only invoked based on the state of the computation. Also any operator that takes a delegate can also return a Task, this applies for Cond, Map, Select, Where, Filter, Any, All, Then, and Else. This immediately changes the type of expression from Cond<A, B> to CondAsync<A, B>; and in the case of Else from Func<A, B> to Func<A, Task<B>>. This allows for a synchronous computation to become asynchronous 'mid-expression'.

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 Then and Else operations are synchronous, but the predicate is asynchronous.

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);

T is defined as Task.FromResult for convenience:

static Task<A> T<A>(A value) => 
    Task.FromResult(value);

What's interesting here is that Any and All operators will run their predicates in parallel if they're all of type Task.

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 conditional-type:

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)

@ncthbrt
Copy link
Author

ncthbrt commented Jan 16, 2017

This is amazing :)
Funnily enough. The class was originally called ConditionalContinuation.
Your implementation does indeed seem more composable.
Thanks for the detailed breakdown of the changes

@NickDarvey
Copy link

That is beautiful! (Thank you both.)
How long till it makes it's way into the NuGet package?

@louthy
Copy link
Owner

louthy commented Jan 16, 2017

@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 Cond.cs and CondAsync.cs are standalone I think (other than you'll need lang-ext in your project for the Task monad and Option references); so if you're super-keen to get this, then I think that you could just copy the files to your project in the short-term and then remove them when released.

Going to close this pull request now.

@louthy
Copy link
Owner

louthy commented Dec 31, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants