I still really like keeping tasks as the single mechanism for packaging functionality with data. I also like the idea of tasks existing as siloed off entities that can't interact except via message passing; this gives tremendous power to the model and makes threading and distributed processing a lot easier to realize.
Really the only big thing I'm recanting on is the idea of tasks being the way to group named stuff. I think a package notion is more sensible, for a number of reasons. First, packages can contain related logic/data without demanding that they all share an instantiation (or are static). Second, packages handle the idea of related logic that doesn't share any state, such as a library of free functions. Third, packages can then in turn contain tasks, which I think is an appealing way to go.
So here's my latest concept of how all this might look:
//// Define a namespace of related "stuff"// Could also have structures and tasks inside it, or maybe even nested namespaces?//package Outputters{ console : string text { print(text) } logfile : string text { // Imagine an IO implementation here. }}//// A task is a compartmentalized entity. Once it is// created, it has no interaction with the rest of the// world except via message passing.//// While task functions can return values, only those// functions without a return value can be invoked// via plain messages. However, I'm pondering some// syntax for capturing task return values as futures.//task GenerateStuff :{ // // This is a valueless function (method) so it // can be called as a plain message from outside. // // Messages are not dispatched while the task // is already handling a message, so a caller // can pass a bunch of messages and they will // safely queue up and be executed in order. // go : integer limit, (output : string) { while(limit > 0) { output(cast(string, limit)) } } // // This is a valued function. It cannot be // invoked as a standard function because it // lives inside the task, and must be talked // to via message. In order to get the return // value "out", we use a future; see below. // compute : integer in -> integer out = in * 2}//// Standard Epoch entry function//entrypoint :{ // // Create an instance of the task // // Note the new syntax for creating something // with no data fields to initialize // GenerateStuff generator = () // // Pass some messages, using stuff // from the package as an example. // generator => go(42, Outputters.console) generator => go(42, Outputters.logfile) // // Pass a message to the generator, // get a future back out, and wait on // the future to get its value. // future result = generator => compute(21) // This blocks until the message comes back with the return value of compute() integer foo = result.value}
I'm curious to see how your packages/namespaces will operate.
In languages like C++ and C# where they use scope braces to enclose the namespace I always find it a perculiar fact that from any point in the code you can just re-open that namespace scope and append some more stuff into it. Whereas other scope-like definitions are fixed after the closing brace is encountered. Maybe my issue is just with the use of scope tokens to define a namespace?
In languages like Java and Python the package/module names are derived-from/reflected-by the filesystem which makes them barely a language construct at all - something which I also find perculiar.