domenic / proposal-blocks

Former home of a proposal for a new syntactic construct for serializable blocks of JavaScript code
215 stars 5 forks source link

What about parallel functions? #24

Closed tenbits closed 6 years ago

tenbits commented 6 years ago

Some thoughts about this topic. Just blocks are not enough. Normally, you orginize your code in reusable functions.

await Task.Run(() => {
    // a lot of work, but not much code 
});

Often the worker function is passed by ref

async Task Main()
{
    var foo = await Task.Run((Func<string>) Foo.Do);
    Console.Write(foo);
}
class Foo {
    public static string Do () {
        // A lot of code and other method calls
        return string.Empty;
    }
}

When method Do requires arguments, then simple one liner lambda is required.

Now our Do logic is thread independent - better to maintain and to cover with unit tests.


But in JavaScript there are no locks, semaphores and mutexes, so the idea with scope-clojure-less blocks is great, but I would extend it to functions or lambdas with a parallel keyword, just the same as async.

parallel function fetchPeople () {
    const res = await fetch("people.json");
    const json = await res.json();
    return json[2].firstName;
};
const result = await fetchPeople();

// OR as a lambda
const fetchPeople = parallel () => {
    /* ... */
};
const result = await fetchPeople();

Arguments

The method would receive arguments as usual function.

  1. Value Types: no problem
  2. Reference Types: structured cloning.
  3. Transferable Objects
  4. SharedArrayBuffer
  5. Perhaps also some synchronized objects would appear to make reads and writes cluster-safe (aka thread-safe) Like in .NET: ConcurrentDictionary, ConcurrentQueue, ConcurrentStack etc.

this

Apparently no-this for parallel functions or lambdas.

Lexical scoping

Same as this proposal: only arguments.

Or maybe it would be possible to capture other parallel functions from outer scope.

Imports

import statements should be allowed on top level of the function. The modules are evaluated in the cluster scope in which the parallel function is being executed.

parallel function fetchFirstName () {
    import Store from 'HttpStore';
    return (await Store.get('people'))[0].firstName;
};
const result = await fetchFirstName();

Execution

There can be 3 options. Which one is better, is the subject to consider:

  1. Parallel functions can be executed with some Cluster API. A parallel keyword would only ensure a safe environment (see above) for the method.

        parallel function fetchFirstName (id) {
            const res = await fetch(`/api/v1/user/${id}`);
            const json = await res.json();
            return json.firstName;
        };
        parallel function fetchMany (...ids) {
            let promises = ids.map(id => {
                rerturn Cluster.fork(fetchFirstName, id);
            });
            return await Promise.all(promises);
        };
        const result = await Cluster.fork(fetchMany, 1, 2, 3);

    fetchFirstName is parallel, so is shared with fetchMany

  2. Parallel functions are always executed by the engine in new cluster.

        parallel function fetchMany (...ids) {
            let promises = ids.map(parallel id => {
                const res = await fetch(`/api/v1/user/${id}`);
                const json = await res.json();
                return json.firstName;                
            });
            return await Promise.all(promises);
        };
        const result = await fetchMany(1, 2, 3);
  3. Mixed: Force parallel functions to be automatically executed in a new cluster, but only if Cluster.isMaster === true, (UI Cluster).

    I would actually prefer this option

    1. it makes sure the parallel functions never block UI Cluster.
    2. no extra api in UI Cluster: we can pass the method somewhere else by reference, and the consumer handles it the same way as a generic async function
    3. To save resources (cluster forking, switching; module evaluations) there would be a Cluster API for forked clusters, so that Developer can deside in place, if a new cluster is required or not.
domenic commented 6 years ago

Thanks for your interest! It sounds like this is a separate proposal, not a bug report or discussion topic on this proposal, so let me close this thread.

tenbits commented 6 years ago

@domenic I'm wondering, why to provide a new syntax, when it would be better to extend the lexical grammar with a keyword, let's say, parallel.

So instead

const result = await worker({|
  const res = await fetch("people.json");
  const json = await res.json();

  return json[2].firstName;
|});

we have old good IIFE

const result = (parallel function () {
  const res = await fetch("people.json");
  const json = await res.json();

  return json[2].firstName;
})();

With arguments it looks imho also better. Instead:

const resultComputer = workerFunction{|
  const [endpoint] = arguments;
  const res = await fetch(endpoint);
  const json = await res.json();

  return json[2].firstName;
|};

const result = await resultComputer(endpoint);

we have

const resultComputer = parallel function (endpoint) {
  const res = await fetch(endpoint);
  const json = await res.json();

  return json[2].firstName;
};

const result = await resultComputer(endpoint);

So, do you see any reasons and advantages to extend the syntax? Or would you create a new proposal with parallel functions. As you would definitely get more attention to the proposal from the community. Thanks.

surma commented 6 years ago

Please take a look at #2 for that question :)