Watching syntax evolve

Published October 08, 2014
Advertisement
I'm still thinking a lot about the question of task syntax in Epoch. The more I contemplate the matter, the less I like the idea of having tasks be overloaded as the "only" way to do namespacing. I also don't like the idea of static methods/data very much.

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}
3 likes 9 comments

Comments

dmatter

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.

October 08, 2014 12:06 PM
Washu
My opinions:

Namespacing:
Package names should be fully qualified, so that you don't end up with the "nested namespace" nonsense you get sometimes in C++. C# has this, and its very nice to be able to just write
namespace A.B.C {
instead of
namespace A { namespace B { namespace C {
This also means that nested namespaces need not be supported.

Nothing:
I've found "nothing" to be difficult to use, you can't pass it for ref parameters (so far as I can tell), which means that in cases where you have optional outs, you can't set them to a non-value. This is especially important with Win32, as sometimes a field being something other than null can change the behavior of a function entirely (see IOCP/OVERLAPPED IO). It also doesn't appear that I can pass nothing in place of a function... again, this can be behavior changing for many APIs.

Standard Library:
Hmm, we need arrays, string manipulation needs to be cleaned up. Characters should probably be separate from strings, or maybe not (hard call).

Tasks:
Nothing to see here.
October 08, 2014 11:05 PM
ApochPiQ
Keep in mind that "nothing" is a type, not a value. The compiler source is pretty illustrative of how it works in practice, although I'll be the first to say that it would be nice to have inline type decomposition of sum types without needing to call separate functions.

If you want to represent optional values, use a sum type:

type Optional<type T> : T | nothing
Actually the sample on the Epoch project site has a decent illustration as well.
October 09, 2014 04:48 AM
Washu

ahh, indeed it did. Yes, I had considered that. I just felt that it was a bit unwieldy in how one employed it. Hmm, I'll have to play with this some more and see.

Tasks:

I worry about reference parameters. Either they have to be eliminated from tasks, or we need some kind of enforcement of a synchronization accessor for them...

October 09, 2014 05:43 AM
ApochPiQ

Hm.

There are three things we need from message passing to tasks:

- Ability to make copies when desired

- Ability to transfer ownership when desired

- Minimal synchronization on data

I kind of dislike the idea of having ref parameters to message handlers suddenly take on weird semantics like "hey this is now protected by synchronization at all times" or whatever. Maybe forbidding them altogether is the right thing to do.

But that only gives us 1 and 3, not 2. How does ownership transfer look?

October 09, 2014 05:09 PM
Washu
The problem is that passing a reference inherently implies "shared ownership," or at least "I'm not giving up ownership of this."

However, there are obvious performance concerns when you're attempting to pass around a large object, whereby having a "reference" is preferable to copying.
October 09, 2014 10:51 PM
ApochPiQ

What if message passing had semantics of moving ownership to the target task? Can we call that out syntactically somehow to make it unavoidably obvious?

Are there actually valid use cases for wanting shared ownership *across* task boundaries?

October 10, 2014 12:26 AM
Washu

Not that I can immediately think of, other than the ability to share large amounts of read only data between multiple tasks.

October 10, 2014 01:46 AM
ApochPiQ
Seems to me like the clean solution is to have a first-class mechanism for handling read-only shared data, and still have standard messages transfer object ownership.


Lots to ponder.
October 10, 2014 02:23 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement