Is there something closely related to template literals in JS or f-strings in Python inside SuperCollider?
No. It was proposed a while ago and met with some skepticism (which might not have been warranted). (Brian had proposed it and, unfortunately, I and another list member argued against it, on the grounds of “too many ways to do things.” The objections got a bit [maybe a lot] too strenuous, which I still regret.)
But… during that discussion, I did actually say, “Well, if we’re not sure whether or not it’s a good idea, we could use the preprocessor to prototype it and try it out, see if it’s comfortable.” But that post went ignored. Nobody tested it, nobody commented on it.
Anyway, I saved the prototype, you can give it a shot.
hjh
// this block installs the f"" preprocessor
(
var
convertFStrings = { |code|
var stream = CollStream(code), out = String.new, ch, closeQuote;
while {
ch = stream.next;
ch.notNil
} {
case
{ ch == $f and: { stream.peek == $" } } {
stream.next; // swallow quote
out = out ++ parseFString.(stream);
}
{ ch == $" or: { ch == $' } } { // scan a whole normal string/symbol literal, with escapes
closeQuote = ch;
out = out ++ ch;
while {
ch = stream.next;
ch.notNil and: { ch != closeQuote }
} {
if(ch == $\\) {
out = out ++ ch ++ stream.next; // escape, esp. for quotes
} {
out = out ++ ch;
}
};
if(ch.notNil) { out = out ++ ch };
}
{
out = out ++ ch;
};
};
out;
},
parseFString = { |stream|
// assumes 'f"' is already scanned; return: code converted to "str".format(...)
var start = stream.pos, list = List.new, formatStr = String.new, out, ch;
while {
ch = stream.next;
ch.notNil and: { ch != $" }
} {
if(ch == ${ and: { stream.peek == ${ }) {
stream.next; // swallow second brace
parseOneExpression.(stream, list);
formatStr = formatStr ++ "%";
} {
formatStr = formatStr ++ ch;
};
};
if(ch.isNil) {
Error("open-ended f-string: %".format(stream.collection[start .. start + 20])).throw;
};
if(list.size >= 1) { // .format only if there's something to format
out = CollStream.new;
out << $" << formatStr << $" << ".format(";
list.do { |expr, i|
if(i > 0) { out << ", " };
out << expr;
};
out << ")";
out.collection
} {
// -1 -- we need the opening quote, and don't include .next char
stream.collection[start - 1.. stream.pos - 1]
}
},
parseOneExpression = { |stream, list|
// assumes '{{' is already scanned; result: item added to list
var start = stream.pos, out = String.new, ch;
while {
ch = stream.next;
ch.notNil and: { ch != $} or: { stream.peek != $} } }
} {
// check for nested f-string
if(ch == $f and: { stream.peek == $" }) {
stream.next; // swallow quote
out = out ++ parseFString.(stream);
} {
out = out ++ ch;
};
};
if(ch.isNil) {
Error("open-ended string interpolation argument: %".format(
stream.collection[start .. start + 20]
)).throw;
};
list.add(out);
stream.next; // scan second brace
stream
};
// some tests
c = CollStream("abc}}xyz");
l = List.new;
parseOneExpression.(c, l);
l.debug("list, expected [ \"abc\" ]");
c.collection[c.pos..c.pos+5].debug("stream state, expected \"xyz\"");
c = CollStream("abc = {{abc}}\"; xyz");
parseFString.(c).debug("parseFString test");
c = "var abc = 10.rand; f\"abc = {{abc ++ f\"nested string {{xyz}}\"}}\".postln;";
convertFStrings.(c).debug("nested conversion");
c = "\"normal \\\"string\\\" ending with f\".postln";
convertFStrings.(c).debug("escaped \"quotes\" and closing f");
c = "'abcdef\"xyz'";
convertFStrings.(c).debug("weird symbol");
try {
convertFStrings.("f\"{{abc}xyz\"")
} { |error|
if(error.errorString.beginsWith("ERROR: open-ended string interpolation argument")) {
"open-ended string interpolation argument detected OK".debug;
} {
error.throw;
}
};
try {
convertFStrings.("f\"{{abc}}xyz; 123")
} { |error|
if(error.errorString.beginsWith("ERROR: open-ended f-string")) {
"open-ended f-string detected OK".debug;
} {
error.throw;
}
};
thisProcess.interpreter.preProcessor = convertFStrings;
)
// With the preprocessor installed,
// evaluate this block just like any other
(
var abc = 10.rand;
"normal string, OK".postln;
f"no expressions, OK".postln;
f"abc = {{abc}}, abc+1 == {{abc+1}}".postln;
f"abc = {{abc.asString ++ f" plus nested {{abc*2}}"}}".postln;
)
thankyou James, super helpful – shall have a play with this shortly!
~format = {
|str|
var bt = Object.getBackTrace.caller;
var toResolve = Set();
var resolved = ();
var find = str.findRegexp("\\$\\{.+?\\}");
find.do {
|sub|
toResolve.add(sub[1][2..(sub[1].size-2)].asSymbol);
};
while { bt.notNil && toResolve.notEmpty } {
toResolve.do {
|name|
var index = bt.functionDef.varNames !? _.indexOf(name);
if (index.notNil) {
resolved[name.asSymbol] = bt.vars[index];
};
};
toResolve = toResolve - resolved.keys;
bt = bt.context;
};
resolved.keysValuesDo {
|name, val|
str = str.replace("${%}".format(name), val.asString);
};
str;
};
{
var x = 4, y = 6, result;
result = x * y;
~format.("${x} * ${y} = ${result}").postln;
{
result = x + y;
~format.("${x} + ${y} = ${result}").postln;
}.defer(0.25);
}.();
This will pull the formatting from variables in scope, and doesn’t require preprocessing. The regex replacement isn’t perfect - you’d probably want something like James’ parser, or better just implement it as a primitive…
Note that you should be able to more or less the same thing as an extension on string, so you can e.g. do f("some ${string}")
.
That’s also kinda cool.
getBackTrace
sometimes crashes. Not as often as it used to, but I saw that a couple of times lately. I wouldn’t rely on it to do real work.
hjh
very interesting! thankyou both, and @scztt for the alternate option.
I will dabble and relight this thread if I run into any questions