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

Documentation for Cond & Subj #494

Closed
TonyHernandezAtMS opened this issue Oct 6, 2018 · 4 comments
Closed

Documentation for Cond & Subj #494

TonyHernandezAtMS opened this issue Oct 6, 2018 · 4 comments

Comments

@TonyHernandezAtMS
Copy link

TonyHernandezAtMS commented Oct 6, 2018

Copied this from another thread. Would be awesome to see this in the wiki.

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)

Originally posted by @louthy in #179 (comment)

@louthy
Copy link
Owner

louthy commented Oct 8, 2018

Yes, good point. It would need some rewording here and there.

@SeanFarrow
Copy link
Contributor

@TonyHernandezAtMS Was this ever added to the wiki as this is a really good explanation.
Also, where was the http client that allowed fluent adition of headers obtained from?
Thanks,
Sean.

@louthy
Copy link
Owner

louthy commented Dec 31, 2019

@louthy louthy closed this as completed Dec 31, 2019
@SeanFarrow
Copy link
Contributor

Would it be worth adding the other functions such as WithHeader?
Or, linking to the functional http client used.

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

No branches or pull requests

3 participants