godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.16k stars 97 forks source link

Add literal quoted strings with embedded variables (f-strings) to GDScript #157

Open wyattbiker opened 5 years ago

wyattbiker commented 5 years ago

Describe the project you are working on:

Coding applications using Godot/GDScript

Describe the problem or limitation you are having in your project:

Would like to format literal quoted strings with embedded global/local variables and expressions by preceding the quoted string with the formatter 'f'. See example below.

Would like the option to use embedded f-string formatting similar to Python. Keeps GDScript consistent with Python as much as possible but provides less wordy and error prone methods to display strings.

Describe how this feature / enhancement will help you overcome this problem or limitation:

Makes string formatting simpler and less error prone and uses less code to format strings.

Show a mock up screenshots/video or a flow diagram explaining how your proposal will work:

Example code single line literal string formatting.

>>> var x='abc'
>>> var y=0
>>> output =  f'The alphabet starts with {x} and numerals start with {y}'
>>> print(output)

Output>> The alphabet starts with abc and numerals start with 0

Example code using multiline string formatting with embedded expression.

>>> var name='Godot'
>>> var profession='Game Engine'
>>> message = f"""
...     Hi {name}. 
...     You are a {profession}. 
...     You are version {(Engine.get_version_info()).major}.
... """
...
>>> print(message)

Output>> '\n Hi Godot.\n You are a Game Engine.\n You are version 3.\n'

Describe implementation detail for your proposal (in code), if possible:

Implementation specs should follow Python 3 where possible: https://www.python.org/dev/peps/pep-0498/

If this enhancement will not be used often, can it be worked around with a few lines of script?:

Will be used often. The alternative is using % or .format() formatting which is much more verbose.

Is there a reason why this should be core and not an add-on in the asset library?:

This enhancement needs to be part of the core along with the other formatting options since it is a syntax modification.

Bugsquad edit (keywords for easier searching): template literals, string interpolation

Calinou commented 5 years ago

Now that I think about it, are Python's f-strings internationalization-friendly? If not, this may hinder their real-world use cases. In contrast, format strings can easily be localized as their placeholders are just text, not evaluated expressions.

girng commented 5 years ago

I've had use cases where I had to do

"""
%s %s %s etc etc
""" % [var1, var2, var3]

Keeping track of the indices to match where your %ss are is kinda annoying, however, it works. The example in the OP is definitely more user-friendly. If it could be added without hindering performance, I think it's a good idea.

bojidar-bg commented 5 years ago

For internationalization, we could have a ftr"a {b} c" version, which would first localize the string and then interpolate the variables (maybe with a sanity-check that the translated string refers to the same variables). It could potentially be made to compile to a .format call, so my example compiles to e.g.
tr("a {b} c").format({b = b}).

GameDevLlama commented 5 years ago

I've had use cases where I had to do

"""
%s %s %s etc etc
""" % [var1, var2, var3]

Keeping track of the indices to match where your %ss are is kinda annoying, however, it works. The example in the OP is definitely more user-friendly. If it could be added without hindering performance, I think it's a good idea.

another downside of the current implementation is, that there are many languages flipping the order of certain words / digits, likely when counting objects. Indexed expressions could also solve this issue!

Calinou commented 5 years ago

another downside of the current implementation is, that there are many languages flipping the order of certain words / digits, likely when counting objects. Indexed expressions could also solve this issue!

This is exactly what the .format() method supports :slightly_smiling_face:

GameDevLlama commented 5 years ago

This is exactly what the .format() method supports

Oh, didn't notice until now! Thanks! :-)

AhmedElTabarani commented 3 years ago

I made same idea here #3403

Here is my syntax idea

Using a symbol like @ will be familiar with Godot 4.0 style

var id = 123
var name  = "Ahmed"
var age = 21

print("Hi I'm  @name, my age is @age, id = @id")

If we want to use expressions, we can do it like this @( )

for i in 10:
   print("No.  @(i + 1), @(i * i), @(i + i)")
   # normal way:
   print("No. " +  str(i + 1) + ", " + str(i * i) + ", " + str(i  + i))

in Dart can make a complex expression inside template strings like a ternary

print("There are ${x < 10 ? "a few" : "many"} people in this room");

we can use the same way and put it in godot using template strings

print("There are @('a few' if x < 10 ? else 'many') people in this room")
Calinou commented 3 years ago

@AhmedElTabarani I wouldn't enable string interpolation for every string, as this can cause difficult-to-diagnose bugs (on top of having a small performance overhead). Instead, string interpolation should be explicitly enabled by prefixing the string with a literal such as f.

AhmedElTabarani commented 3 years ago

@Calinou I get it, do what you think is right and better in terms of complexity.

SIsilicon commented 3 years ago

You could probably also use the backtick (`) as well just like JavaScript.

Overvault-64 commented 2 years ago

< tl;dr I've made a function to format strings with placeholders without manual mapping >

In Python, you'd just

first_name = "John"
last_name = "Smith"
age = 31
profession = "designer"
print( f"{first_name} {last_name}, {age}, is a {profession}." )

From my experience, in GDScript out of the box you have the following options to format a string. Best in terms of readability:

var first_name = "John"
var last_name = "Smith"
var age = 31
var profession = "designer"
print( "{first_name} {last_name}, {age}, is a {profession}.".format({"first_name" : first_name, "last_name" : last_name, "age" : age, "profession" : profession}) )

The call is like 3 times longer than it should be and forces you to a lot of repeating.

Best in terms of conciseness:

print( "%s %s, %s, is a %s." % [first_name, last_name, str(age), profession])

Yes, this is concise, but look at what happens to readability.

Then you have another option, that is not formatting but concatenating:

print(first_name + " " + last_name + ", " + str(age) + ", is a " +  profession + ".")

This is quite readable and not a pain to write, but I think it's unelegant and ugly.

Since I was uncomfortable with all these options, I came up with a workaround to emulate Python's fstrings:

func _ready():
    print( f("{first_name} {last_name}, {age}, is a {profession}."))

func f(string : String ) -> String:
    var map = {}
    for variable in self.get_property_list():
        map[variable.name] = get(variable.name)
    return string.format(map)

This way, all the variables in the script are mapped automatically and the formatted string is returned. One obvious limitation of this method is that variables have to be properties of the whole node, so they have to be defined outside of the caller function. This limitation is easily resolved passing additional variables as a second argument of f() and make f() handle them as well.

I tried putting the function in an autoload to be reusable but, passing the node as an additional argument, for some reason all the variables are Null, so I'm forced to put the function in every script where I need it. I wonder if there is a way to make the function itself infer the node that called it.

Anyway, adding something like this in the String class (or in the Node class, in its current form) to be called at will, would be great.

wyattbiker commented 2 years ago

Good concept. You can get that to work as an autoload/singleton as below by passing self as a parameter. However there is still the problem with printing local variables. Unfortunately there is no way to get them in the f() method. Passing them also defeats the whole purpose of this formatter. If you can figure out to access locals vars in the f(), then this would be ideal as a plugin.

Edit: I just realized there is an even simpler method that can be used. However the locals vars still an issu.

#Format.gd autoload method
extends Node

func f_old(script, string : String ) -> String:
    var map = {}
    var this_script: GDScript = script.get_script()
    for variable in this_script.get_script_property_list():
        map[variable.name] = script.get(variable.name)      
    return string.format(map)   

#Simpler method
func f(script, string : String ) -> String:
    var map = inst2dict(script)
    return string.format(map)
#test calling script
var first_name = "John"
var last_name = "Smith"
var age = 31
var profession = "designer"

func _ready():
         var salutation="Mr."  # this will not work if passed to f()
    print(Format.f(self,"{first_name} {last_name}, {age}, is a {profession}."))
Overvault-64 commented 1 year ago

Hey sorry @wyattbiker, I somehow missed your reply. Unfortunately I couldn't figure out how to access functions' locals so this is the best I could do (based on your version, which was a great improvement):

func _ready():
    var salutation = "Mr."  # this will not work if passed to f()
    print(Format.f("{salutation} {first_name} {last_name}, {age}, is a {profession}.", self, {"salutation" = salutation}))  


Format.gd autoload

func f(string : String, script : Node, locals := {}) -> String:
    var map = inst_to_dict(script)
    for local in locals:
        map[local] = locals[local]
    return string.format(map)

I moved the string argument to be close to locals, which is optional and needs to be placed after mandatory arguments

I'm gathering more info about accessing locals but I've found only this

timothyqiu commented 1 year ago

The only reason for me to use f-strings in Python is {variable=}. It makes debugging a lot easier:

print(f"User: {id=} {name=}")
# Outputs:
User: id=42 name='Godot'
shadow64 commented 1 year ago

Was hoping that this cause would already have people behind it. I agree that the current methods are pretty verbose and not very user-friendly.

What about using C#'s method of using $?

var power_cells_remaining = 10
print($"We only have {power_cells_remaining} cells left before the core ruptures!")

Output: We only have 10 cells left before the core ruptures!

It's already available to people who are using C# to code projects in Godot. It would just be parity.

Calinou commented 1 year ago

What about using C#'s method of using $?

$ is already used as a shorthand for get_node() in GDScript, so it shouldn't be reused to do string interpolation. We've already added support for r"" raw literals in GDScript in 4.2, so I think we should go for f"" for f-strings.

shadow64 commented 1 year ago

What about using C#'s method of using $?

$ is already used as a shorthand for get_node() in GDScript, so it shouldn't be reused to do string interpolation.

Fair point. I hadn't considered that. I'm all for just getting it working in that way.

P3NG00 commented 8 months ago

Still hoping for f"" strings in godot script one day :eyes: Looks nicer than using the % or .format()

dalexeev commented 8 months ago

@P3NG00 Please don't bump issues without contributing significant new information. Use reactions on the first post instead.

eraoul commented 6 months ago

I feel like a lot of the discussion in this thread revolved around the idea that .format is better for internationalization, or else focused on what syntax would be best for f-strings themselves.

I think this misses the key idea that f-strings are great for many typical use cases. Internationalization is important, but if I were a serious game shop I'd plan for that from the start. I don't think it makes sense to disregard the feature because it wouldn't be useful for the i18n crowd. Indeed, it's still useful for local debugging and development, even if the f-strings don't show up in production code. I use the python f'{var=}' syntax all the time in debugging and in writing good assert messages, for instance, and the existing Godot solutions are much more verbose.

As for the best syntax, lots of good ideas there. Hard to beat the python syntax though, it's battle-tested and well-loved.

SteampunkWizardStudios commented 3 months ago

Python Syntax, Javascript Syntax, a weird blend or something completely new, I don't care but some version of being able to use ANY kind of literal string formatting in gdscript soon is important to me.

m21-cerutti commented 2 months ago

Found recently this library where it could fit to part the main usage FMT library It's interesting because we could translate something like

print(f"Don't {}", "Panic")
formattedString = f"Don't {}"%["Panic"]

with under the hood (cpp pseudo code)

fmt::print("Don't {}"_fmt, "Panic");
formattedString = StringFormat("Don't {}"_fmt, "Panic");

FMT is also battle-tested solution in cpp and have the good taste :

You loose maybe the ability to choose the position of the argument you have in some syntax proposition, but I think it is more readable if you don't have this feature.

Calinou commented 2 months ago

Found recently this library where it could fit to part the main usage FMT library

Godot's own String.format() already does most of what fmtlib does, but adds its own features on top. I don't think we need to integrate fmtlib to implement this anyway, as the main difficulty is modifying the GDScript parser to handle the new functionality, not implementing the string formatting itself.

Switching String.format() to use fmtlib behind the scenes would break compatibility if all its current Godot-specific features aren't implemented in fmtlib.

Ivorforce commented 1 month ago

Here's my 2c about the syntax: Python fstring syntax (f"a = {a}") is of course perfectly acceptable, as it's instantly familiar.

I much prefer the swift syntax though:

"a = \(a)"

This syntax introduces two fewer characters ({ and }) that need to be escaped (\ is already special), and also doesn't need new f"" syntax (because it's guaranteed to be compatible with existing strings). The downside is of course it's now somewhat inconsistent with the existing "a = {}".format(a) syntax. If you want to support python-like f"{a:.2f}" syntax, that may be the better solution.

Anyway, I also got something more interesting to share: An outline for how this feature could work.

Implementation Proposal

I happen to work on a programming language myself, and found myself confronted with string interpolation. The solution I came up with involves a stateful lexer, but keeps the grammar itself contextless. The advantage of this is that this is much easier to implement that switching to a context grammar just for string interpolation.

Basically, when a string is started, the lexer adds a virtual "String begin" token. Then, it enters 'string parsing' mode, emitting a string until either " (end of string) or \( (begin of expression) is reached. For expressions, the lexer counts the number of opened and closed parentheses (or brackets), and when it reaches 0 again, it ends the current format expression, and re-enters 'string parsing' mode.

The compiler later gets an array of strings and expressions, and decides how to compile them. With the simplest implementation, it will just compile to a format string call.

Example

"A \(b) c \(d)\(e)"

results in the tokens

STRING_BEGIN
STRING(A )
EXPRESSION(b)
STRING( c )
EXPRESSION(d)
EXPRESSION(e)
STRING_END

The grammar interprets strings as such:

STRING = STRING_BEGIN STRING_ELEMENT* STRING_END
STRING_ELEMENT = STRING | EXPRESSION

When a string was parsed, the compiler gets a list of tagged unions (pseudo-rust code):

if elements.size() == 1 {
    match elements[0] {
        String(string) => return compile_string(string),
        Expression(expression) => return compile_expression(expression),
    }
}

let mut full_string = "";
let mut format_parts = vec![];
for item in elements {
    match item {
        String(string) => full_string.append(string),
        Expression(expression) => {
            full_string.append("{}");
            format_parts.append(compile_expression(expression));
        },
    }

    // [...] emit string format call like:
    // format(<full_string>, *format_parts)
    return compile_call(format_string, compile_string(full_string), *format_parts)
}

The final compiled code is equivalent to:

format("A {} c {}{}", b, d, e)

This approach turned out very clean for me, and fairly powerful because you can interpret the string parts however you like. As opposed to Python f-strings' implementation, it can handle arbitrarily deep strings, such as "a \(some_call("b"))".

You can peep my Lexer for this here: https://github.com/Ivorforce/Monoteny/blob/main/src/parser/lexer.rs And the part that resolves it to an expression: https://github.com/Ivorforce/Monoteny/blob/aec09fef64e19de672b49941ddcd903a04bbc674/src/resolver/imperative.rs#L451-L481

Note that my implementation joins strings into string appends (and elements are function calls) rather than format strings, but it's the same idea.