promises-aplus / promises-spec

An open standard for sound, interoperable JavaScript promises—by implementers, for implementers.
https://promisesaplus.com/
Creative Commons Zero v1.0 Universal
1.84k stars 171 forks source link

Implementing promises-aplus for C# and having some questions. #201

Closed joscarras closed 9 years ago

joscarras commented 9 years ago

Hello guys,

First of all I know that this spec is for JS, but since its very nice, I wanted to implement it (as close as possible) for C# as a learning process and then use it in the future if I decide to work full time with C#. Of course when I finish I will publish it to git hub so others can benefit from it as well as long as I feel confident that its well implemented and productive.

Now since C# is a strongly type language there are some areas of the spec that do not apply and some others that probably should not apply because of the nature of a strongly type language, and I need your help and thoughts so I can implemented this as close as possible to this spec.

2.2.1.1 If onFulfilled is not a function, it must be ignored. 2.2.1.2 If onRejected is not a function, it must be ignored.

This can easily be implemented, however I feel that in the context of a strongly typed language this should raise an exception because its probably an error on the developers part. Can you provide the reasoning behind this just in case I am not seeing the full picture? Do you think this should be implemented, or do you think its better to raise in invalid argument exception or even better a compile error?

2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].

This is not easy for a beginner. Its something that I might be able to do in the future when I get stronger but for now I am skipping it.

2.2.7.1 If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure Resolve(promise2, x).

Here I have the opportunity to know before hand if a function returns or not and if it does what the type is. What should I do if the user adds a function that simple does not return? Should I resolved the promise with 2.2.7.3 If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1. and 2.2.74 If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1. or create a NULL resolve / reject type and pass that?

In my personal opinion the first is a more semantically correct for a strongly type language, and if I do want the resolve / reject values to change, I can always pass a function that returns, get its value and continue the promise resolution with the new value; also with the first the user has the opportunity to do both, however with the second the user does not get the first as a choice and might even surprise him if his result value gets mysteriously consumed. What do you guys think?

Here however I have another problem. The spec specifies with 2.2.7.1 that If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure, however since this is a strongly type language the resolve and reject types are different (can be the same if the user wants to but most likely will be different), so I can't pass directly the onRejected value to the resolution function with out first casting the reject value to a resolve type.

I can introduce an interface and have the user supply the casting method, however since I know the return type of the onRejected I can resolve the promise if its a resolve type or reject the promise if its a reject type, however this breaks the part of the spec.

So what do you think is better? Force the user provide the casting method with an interface or get the result type of the onRejected and depending on that type resolve or reject the promise. (If you are wondering what will I do if the user specifies both types being the same, then I follow the spec and simple resolve the promise)

2.3.3 Otherwise, if x is an object or function, 2.3.3.1 Let then be x.then. [3.5]

This full sections is actually close to impossible to do efficiently. I have to use reflection and a bunch of very very nasty stuff that no beginner should do, so for now I am skipping it. When I finish all the above and study the spec here very well, i will probably come with more questions and thoughts.

Hope you guys have some time to help me out! Thank again a lot for your help and this great spec!

bergus commented 9 years ago

2.2.1.1 If onFulfilled is not a function, it must be ignored. 2.2.1.2 If onRejected is not a function, it must be ignored.

This can easily be implemented, however I feel that in the context of a strongly typed language this should raise an exception because its probably an error on the developers part. Can you provide the reasoning behind this just in case I am not seeing the full picture?

There is indeed a useful application: passing nothing instead of a function. then should be able to be called with only a onFulfilled callback ("chain"), only a onRejected callback ("catch") or both ("bichain"). It could be called with neither as well, which is more like an identity function, but might have some meaning for cancellation semantics.

I don't know C#, but this could be either implemented by overloading the then method or by making the arguments optional (nullable?). Indeed, passing in arbitrary values that are no functions is not a design goal, and some implementations do log warnings in that case. You won't need it.

2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].

This is not easy for a beginner.

Forget about that "platform code" terminology (it's controversial in JS as well). This just says that the callback must, in every case, be invoked asynchronously, and not from within then (on the stack) even if the promise is already fulfilled. Again, I don't know what concurrency primitives your language is based on, but this could be some kind of multithreading situation.

What should I do if the user adds a function that simple does not return? Should I resolved the promise with the same value as promise1?

No, you should indeed resolve it with NULL (or whatever value/type your language uses to represent the absence of a value, in JS it's undefined). A Unit ("void"?) type would be fine here. The next callbacks on the resulting promise would then be called without any arguments. The promise does no more represent an asynchronous result value, but just the end of the task.

The spec specifies with 2.2.7.1 that If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure, however since this is a strongly type language the resolve and reject types are different (can be the same if the user wants to but most likely will be different), so I can't pass directly the onRejected value to the resolution function with out first casting the reject value to a resolve type.

Well, if your language was typed sufficiently strong enough, it should be possible to enforce that both functions have the same return type. If it is not, then you will either have to use a cast, or make your returned promise have a union result type.

2.3.3 Otherwise, if x is an object or function, 2.3.3.1 Let then be x.then. [3.5]

This full sections is actually close to impossible to do efficiently.

This is how JS does "interfaces" via duck typing. It's quite important to implement this step, however it is not necessary to use reflection in your language. A Thenable interface that other libraries can implement will suffice.

This is another case of then being overloaded. If you want to do it properly, you might even want to consider implementing two different methods. In pseudocode:

interface Thenable<a,e>
    then ( (a -> *), (e -> *) ) -> * // "*" means "anything"

class Promise<a,e> implements Thenable<a,e>
    then ( (a -> Thenable<r,e>), (e -> Thenable<r,e>) ) -> Promise<r,e>
        // monadic chain
    then ( (a -> r), (e -> r) ) -> Promise<r,e>
        // functor map
joscarras commented 9 years ago

@bergus, thank you so much! You have provided wonderful answers and lots of ideas that I am not sure why I did not think of them my self. You really did answer all my questions and gave some very nice solutions!

I just have tree comments just to make sure:

There is indeed a useful application: passing nothing instead of a function. then should be able to be called with only a onFulfilled callback ("chain"), only a onRejected callback ("catch") or both ("bichain"). It could be called with neither as well, which is more like an identity function, but might have some meaning for cancellation semantics.

I have already done that. If the users pass NULL I just do 2.2.7.3 If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1. or 2.2.7.4 If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.

Indeed, passing in arbitrary values that are no functions is not a design goal, and some implementations do log warnings in that case. You won't need it.

Am I safe here to throw an exception or compile error? Or just accept it and continue? As per log warnings because I am trying to implement this with generic programming (generics is meta-programming in C++) so users can adopt it anyway they want to, I don't want to do any error handling on my own and leave it to the user to handle them anyway they like with either overloads or exceptions.

Forget about that "platform code" terminology (it's controversial in JS as well). This just says that the callback must, in every case, be invoked asynchronously, and not from within then (on the stack) even if the promise is already fulfilled. Again, I don't know what concurrency primitives your language is based on, but this could be some kind of multithreading situation.

Yes that is why I want to do it later when I am stronger. I don't want to start doing multithreading right now because I am very new to C# so I rather get the basics first and then starting learning about threading and since I am already doing generic programming which is not as simple and can be overwhelming to new-comers don't want to overdo it right now. It might be easy, to be honest I have not touched the subject at-all yet.

Again thanks a lot for all the clarifications and you valuable help!

bergus commented 9 years ago

passing in arbitrary values that are no functions is not a design goal

Am I safe here to throw an exception or compile error?

Yes, a compile error would be fine here. JS just didn't do this because there is no static compiler :-)

I want to do it later when I am stronger. I don't want to start doing multithreading right now because I am very new to C#, to be honest I have not touched the subject at-all yet.

I wonder what you need promises for when you're not doing anything asynchronous yet…

joscarras commented 9 years ago

I wonder what you need promises for when you're not doing anything asynchronous yet…

Well let me answer this with a little story I was told in the US, I think it originates from china... When the empire was very wealthy they started creating a railroad and adding train tracks everywhere, without having trains or any plans for them. When they ask the leader why was he doing something so silly, he said something funny but true; he said lets put the tracks first, and then the train will come. Same reasoning; I wan't it be fully threaded, and it will happen when the time is ready.

However your statement is half true, and when the promise is not resolved it actually works async and is fully compliant with the spec. Take a look at this demo which I am using to test my work:

namespace testing
{
    class Download : Promise <string, ArgumentException>
    {
        public Download( string url ) {
            Console.WriteLine ("Downloading " + url + " ...");

            // This here is an async class that creates a thread and downloads the content of a URL. Its part of the C# System.
            var client = new WebClient ();

            // This here is the callback which resolves or rejects the promise when the async operation is complete, from a different thread.
            client.DownloadStringCompleted += (s, ev) => {
                // ev is a union type which you also suggested before and might even do my self with the promise implementation.
                if( null != ev.Error ) {
                    Console.WriteLine( "An error occurred... rejecting the promise." );
                    reject( ev.Error ); 
                }

                else {
                    Console.WriteLine( "... Download completed." );
                    // Resolve the promise here.
                    resolve( ev.Result );
                }
            };
            // Invoke here the async call.
            client.DownloadStringAsync( new Uri( url ), null );
        }
    }

    class MainClass
    {
        public static void Main (string[] args)
        {
            Console.WriteLine ("Start");
            var running = true;

            // This is here a new promise.
            var download = new Download ( "http://www.google.com" );

            download.then (
                result => Console.WriteLine ("1st."), // Success callback
                result => Console.WriteLine ("FAIL") // Reject callback
            );

            download
                .then (result => Console.WriteLine ("2nd")); // Another success

            // Callback that terminates the main thread.
            download
                .then (
                    result => ( running = false),
                    result => ( running = false),
                                ); 

            while (running) { Thread.Sleep (10); } // Keep running until all everything is complete.
            Console.WriteLine ("Done");
            Console.ReadLine ();

        }
    }
}

So before the promise is settled, everything is async and threaded. Once the promise is settled, then all the callbacks occur in the when scope however I will fix that when I see how C# threading is done.

I will close this now! I might bug you some more later on with the promise resolution when I finish the rest.

Have a wonderful day and many many many thanks!