Skip to the content.

autoclose and Processes I/O

Context managers with autoclose

While implementing til-file I realized how awkward using the with command was. I’m not a big fan of using “token words” for commands (that is: words that are pure syntax, with no real purpose or value), so with ended up being with <name> <context_manager_instance>. And you know what? I was writing my tests of til-file using with <context_manager_instance> <name> everywhere…

After some Hammock Time I realized there’s a much better way of working with context managers, one that does not create “new syntax” and, specially, make use of the commands that already exist (because, you see, with was setting a variable in the context, as if having set x and something | as x was not enough): the autoclose command.

This command pops one argument from the stack, configures the current context and pushes it back (what we actually achieve with a peek, that’s cheaper). Since it’s being pushed back, it’s very easy to use it in a pipeline like this:

open.read $path | autoclose | as file

Or even:

autoclose [open.read $path] | as file

It won’t work as intended, though, if used inside an ExecList, since they have their own sub-scope:

# Avoid calling autoclose inside an ExecList:
set file [open.read $path | autoclose]

In the case above, the file will be opened and immediately closed, so that $file will contain an already closed file…

Every scope calls “autoclosed” context managers

At first I implemented the calls to “contextManager.close” in the scope command implementation itself. It was enough for validating the concept, but it would be weird to not allow context manager to be used inside procedures, for instance. Or even in the MainProcess itself.

So now every call to Process.run will call the scope.contextManagers “close” command. That encompasses any SubProgram, including the MainProcess scope.

Processes I/O

One thing I’m trying to achieve is simplicity, both in Til as a language as in the implementation itself.

Previously, each Process had it’s own .input and .output properties, just like Unix-style processes. I did that because it just felt natural. But the results, in the end, were increased complexity: it wasn’t obvious when a Process expected to read something from its input, for instance, since the presence of an input was itself too implicit.

From the implementation part, the command names were all over the place: read/write were actually pop/push, since any sub-Process had Queues as input/output, and there was the .no_wait version of each. The D code had a lot of kind-of-unnecessary-and-complex if/else structures (the main villain I always try to avoid), some ad-hoc class definitions, etc. I never liked that corner of the code, to be honest.

Also, making sub-Processes inputs and outputs Queues was a good idea, but the MainProcess would have it’s own classes and differentiating between each was also kind of a pain.

The question I asked myself, then, was: do we really need it? If sub-Processes inputs and outputs were all Queues, couldn’t we live without it and simply send queues explicitly to spawned code we want to communicate with?

Sure! And that’s how it works, now: everything is explicit: if you are going to spawn a new Process and want to send and/or receive data from it, you should pass a Queue to do that. This way it becomes obvious by the parameters list of the to-be-spawned proc that it’s going to use one or more Queues as a way of communication with other processes.

(Yeap, it resembles the “channels” concept. Well, good thing I’m not alone in this way of doing things, at least…)

Deprecation warnings

There was a good reason to make every command an object: now we can mark some of then as isDeprecated = true and then every time a user calls it, a warning is sent to stderr saying:

Warning: the command with is deprecated