Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interpolated Strings #3479

Closed
marler8997 opened this issue Oct 18, 2019 · 5 comments
Closed

Interpolated Strings #3479

marler8997 opened this issue Oct 18, 2019 · 5 comments
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@marler8997
Copy link
Contributor

marler8997 commented Oct 18, 2019

I'm creating this to share how I would implement interpolated strings in Zig. Note that I'm not advocating whether or not they should be added, just sharing a method for how they could work in Zig and some of my thoughts on the subject.

Description

This method takes advantage of the anonymous struct initialization feature proposed here: #208 (comment)

const a : usize = 10;
const b : usize = 24;

// Note: warn2 is a theoretical function that supports anonymous struct formatting
std.debug.warn2(i"a + b is {a + b}\n");

// Same as
std.debug.warn2(.{"a + b is ", a + b, "\n"});

The whole method is that Zig supports lowering string literals to an anonymous struct literals. These special string literals could be denoted with a special character like i which tells the compiler to lower them.

Why String Interpolation?

Remove a point of error

With string interpolation, synchronization between format strings and argument lists is no longer a problem.

std.debug.warn2("hello {}, my name is {} and I work at {}", audience, name, employer);
// vs
std.debug.warn2("hello {audience}, my name is {name} and I work at {employer}");

With interpolated strings, you can add/remove variables to be formatted in one place instead of modifying two places that need to be kept in sync.

std.debug.warn2("hello {}, my name is {}, the time is {} and I work at {}", audience, name, time, employer);
//                                     ^+++++++++++++++                                  ^+++++
// vs
std.debug.warn2("hello {audience}, my name is {name}, the time is {time} and I work at {employer}");
//                                                 ^+++++++++++++++++++

Note that Zig will detect if you have the number of arguments mismatched, but wouldn't be able to catch an out-of-order bug.

Make it easier to see errors

String interpolation can also be easier to read. Take this example:

std.debug.warn2(.{"a is ", a, "b is ", b"});

Do you see the error? In this example, if a is 0 and b is 1, this would print a is 0b is 1. Here's the same mistake but written with string interpolation:

std.debug.warn2(i"a is {a}b is {b}");

And here are the corrected versions:

std.debug.warn2(.{"a is ", a, " b is ", b"});
std.debug.warn2(i"a is {a} b is {b}");

Why Syntax Lowering instead of Library Support?

The problem with implementing interpolated strings in a library is that the function implementing it will not have access to the caller's scope. The current string formatting function takes the expressions as parameters, but a string interpolation function only accepts the comptime string to be interpolated; it does not have access to the caller's scope to evaluate the expressions inside it.

The language could add support to access the caller's scope, however, this is problematic as it would mean that every function you call has the ability to reach into your scope and use/modify it. Imagine this piece of code:

var x : usize = 42;
foo();

After we call foo, what is the value of x? Currently we can confidently say the value is 42. However, if foo could access the caller's scope then we can't guarantee that. One way to mitigate this would be to limit caller scope access as readonly, however, with indirection the function could still unexpectedly mutate data. I've also seen it suggested that you could solve this by explicitly passing in your scope, i.e. foo(@scope()), but now interpolated strings are starting to get verbose.

Summary

I see 2 benefits with interpolated strings. They remove potential errors from keeping formatting strings and arguments in sync, and they can be easier to read. The downside is that it adds a new character prefix to string literals and requires extra code to parse the interpolated strings in the compiler.

@pixelherodev
Copy link
Contributor

pixelherodev commented Oct 18, 2019

I do think this could be useful, but I'm also hesitant regarding any change that increases the language's complexity. One of Zig's main selling points - and the one that first attracted me - is its simplicity.

As such, I think it might be better to use a builtin function instead?

@kyle-github
Copy link

I haven't thought this fully through, but it seems like you can do most of what you are asking for with a comptime function.

You would need to parse the format string in comptime code and then inline code to print out the values.

I don't think it quite works but there might be some minimal changes to allow it.

@pixelherodev
Copy link
Contributor

Actually, would it be possible to implement this over existing std.fmt functions? e.g. translate "{a} is {b}" into format("{} is {}", a, b)? Being part of the stdlilb would probably be the best option IMO.

@andrewrk andrewrk added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Oct 19, 2019
@andrewrk andrewrk added this to the 0.6.0 milestone Oct 19, 2019
@andrewrk
Copy link
Member

Thanks for typing out this proposal in a clear, detailed manner.

This thing about the callee reaching into the caller's scope is not going to happen.

I don't think Zig needs specific syntax for string interpolation.

This example code can be supported in userland, with no language changes required besides the already planned anonymous struct literals:

std.debug.warn2("hello {audience}, my name is {name} and I work at {employer}", .{.audience = a, .name = b, .employer = c});

@marler8997
Copy link
Contributor Author

marler8997 commented Oct 19, 2019

@andrewrk I like your solution as it provides a solution to the problem of keeping the format string in sync with the arguments.

However, for myself, I'm not sure if I would use it very often as it's more verbose than:

std.debug.warn2("hello {}, my name is {} and I work at {}", a, b, c);

I'm not sure the benefit of removing that potential for error would be worth all the extra code in most cases. Then again, if I was trying to format a longer string, then I think it would work nicely. I may try to implement a formatting function that does this.

To be thorough, when we get anonymous structs, we can also make this work:

std.debug.warn2(.{"hello ", audience, ", my name is ", name, " and I work at ", employer});

But the problem I have with this is it's hard to see what the final string looks like with all the noise that comes from delimiters. Which means without interpolated strings I'll probably stick to formatting functions. If interpolated strings were implemented, I imagine I would default to using those instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

4 participants