Practical use cases of `thisProcess.interpreter.executeFile(path...args)`?

Could you share some practical use cases of thisProcess.interpreter.executeFile(path...args)?

My current understanding is as follows:

  1. To use thisProcess.interpreter.executeFile(path...args), the corresponding .scd file should be a function without the enclosing curly braces {}.
    For example:
    In functionTest.scd file:

    |a, b|
    "Arguments received: % and %".format(a, b).postln;
    a + b
    
  2. Then, to execute that file from another .scd file in the same folder as functionTest.scd:
    thisProcess.interpreter.executeFile("functionTest.scd".resolveRelative, 10, 20);

I am asking this in order to add a practical example to the following PR:

Any suggestions or direct modifications to the PR are welcome!

The .scd file can contain any SC expression. executeFile internally calls compile to compile the code and calls the resulting function with the provided arguments (default: none).

(Actually, I did not know that you can use a toplevel | <args> | resp. arg <args>; to pass arguments via executeFile!)

Note that if you only want to execute the file without passing arguments, you can just use String -load instead (which internally calls thisProcess.interpreter.executeFile) *)

I typically use load resp. loadRelative when I have split up a project into multiple files. In the ā€œmainā€ file I have something like this:

(
"foo/cat.scd".loadRelative;
"foo/fish.scd".loadRelative;
"bar/dog.scd".loadRelative;
// etc.
)

Some people might prefer to pass arguments to avoid the use of global variables. For example:

thisProcess.interpreter.executeFile(
    "myModule.scd".resolveRelative,
    16, // channels
    "presets" // preset path
);

Unfortunately, executeFile does not forward keywoard arguments (yet), otherwise you could also do things like:

thisProcess.interpreter.executeFile("myModule.scd".resolveRelative,
    channels: 16, presetPath: "presets");

However, this is all pretty verbose, that’s why I just use String- load + environment variables instead.


*) I don’t know why String -load and String -loadRelative do not take arguments that are forwarded to the script. Maybe that was just an oversight? This would make passing arguments to scripts more attractive.

1 Like

The way I deal with multiple files is through a class called import that calls load inside a new environment. This way, environmental variables are exported out of the file. I also cache these.

moduleA.scd

~foo = 10;
main.scd

~a = Import("moduleA.scd");

~a.foo === 10;

I find this to be much easier to manage at scale.

2 Likes

But isn’t this just env.know_(true) ?


e[\x];  
e.know_(true);
e.x;

What is kinda annoying for me, this is not really a module, rollback restores the namespace, not the world. We are not really working with states (immutable or not). If your module allocated Buffers, Synths, SynthDefs… those are there. The Environment just holds references…

1 Like

In my SC3.15.0_dev, there is no Import class. Where can I get this?

It could be just a simple class which holds an enviroment. From there, you have a few options. You could write a cache system for it. Or you could go functional and make each State immutable, so you create a new one when necessary.

Those things are useful for large projects.

BUT it can gets hairy. Might be worth it for big projects tho — the functional core means you can reason about state transitions in isolation, even if the actual effects are messy. But you need to trust it enough to bring it to a stage (since SC is not designed for that, it’s all on you)

1 Like

I usually just rewrite it every time I start a project.
Something like this…

Import {
	classvar root, cached;
	*projectRoot { |path| root = path; cached = IdentityDictionary(); }
	*new { |path|
		var fullPath = (root +/+ path).asSymbol;
		^cached[fullPath] ?? {
			var e = Environment.make( { fullPath.asString.load } );
			e.know = true;
			cached[fullPath] = e;
			^e
		}
	}
}

...

Import.projectRoot("~/Desktop".standardizePath);

~a = Import("a.scd");
~a.foo == 10; // its starting value

~a.foo = (); // reassigning the value.

~b = Import("a.scd");

~b.foo === ~a.foo // these are *identical* and equal '()'

Some things to consider…

I like to have flat modules, so all the paths are given relative to the project directory, not from the current module. Change that if you don’t like it, but I find it helps me keep things simple.

Modules are singleton objects and you can change their values if you want. This is confusing with the cache system as all instances change. I just don’t mutate stuff, or pass an argument to disable the cache if I find it really necessary.

All the nonsense with name collision and environment variables… when I get the time, I will get abstract object working as this is a good use case for it.

When having a file that starts a process (or something with mutable state), I usually have an ~init function that creates the process, I do not pass arguments to the modules.

Variables declared with var provide a way of making things ā€˜private’ to the file, environment variables are ā€˜exported’.

Additionally, I sometimes add an Import.sync(...) which calls s.sync afterwards, this is useful when making synthdefs. There are other ways of doing this, but often I make all my synthdefs at the beginning and really don’t care. For some projects, I make all imports ā€˜sync’ and put all the main code in an s.waitForBoot block.

It isn’t a perfect solution… but I find it much easier that dealing with files directly as I often abuse them and things quickly get too complicated.

2 Likes

Man, this is a clever hack, I mean, using the filepath in the env.

1 Like

Thank you very much for your opinions and suggestions.

In any case, I have added two examples using thisProcess.interpreter.executeFile(path...args).

I realise these examples may not be ideal, but they reflect what I am currently able to do within my understanding and capacity.

Please feel free to leave a comment here or directly on the PR.

That was a good insight

I’m not super firm with keyword args in sclang yet, but wouldn’t that simply amount to replacing, in Interpreter.executeFile:

	result = this.compileFile(pathName).valueArray(args)

with

	result = this.compileFile(pathName).functionPerformList(args, kwargs)

If so, I’m happy to do a PR, and then also make String.load take args / kwargs…

You need to use performArgs to pass keywords along.

Inside the interpreter, when you call something, there are two counts, the number of normal arguments, and the number of keywords. These all all placed on the stack so that a.(1,2 b: 3) becomes … a, 1, 2, \b, 3 with the number of normals arguments being 2 (or three depending of the receiver is counted) and the number of keywords being 1.

When you get a ...kwargs these have been taken off the stack and put into an array, the only way to put them (or anything) back on the stack as keyword arguments is to use performArgs.

so basically this.compile.performArgs(\value, args, kwargs), which should be the same as valueArgs(args, kwargs)?

Yes! (More characters)

Sorry, this is maybe OT now but in my attempted revision of load/executeFile etc. the pathName argument in the executeFile — called from within String.load —, somehow ends up being nil and I can’t figure out why. Is it something very obvious?

// this string instance will be the pathName arg for executeFile
String {
    load { |...args, kwargs| 
        ^thisProcess.interpreter.executeFile.valueArgs([this] ++ args, kwargs)
    }
}

Interpreter {
    executeFile { | pathName...args, kwargs | 
        if(File.exists(pathName).not) { // I get an Error here because pathName is nil
        } //etc.
    }
}

You are trying to value the result of executeFile, you need to send it as a message. Try this (on subway, can’t check this works right now!).

^thisProcess.interpreter.performArgs(\executeFile, [this] ++ args, kwargs)
1 Like

yes, that was it! Thanks!

1 Like