string interpolation by passing format string to template, args as tuple#186
string interpolation by passing format string to template, args as tuple#186mdparker merged 9 commits intodlang:masterfrom
Conversation
DIPs/1NNN-ADR.md
Outdated
|
|
||
| The helpful people in the D forum point them at the spec: "I know it looks like a string," the Master explains, "but it is actually lowered to an auto-expanding tuple whose first element is a string. That's causing your problem. I'd love to fix it in the library, but it is literally impossible to differentiate this case from legitimate usage :(" | ||
|
|
||
| "D is weird," the user replies, deflated and defeated, "I think I'm just going to use Python. Have you see its f-strings?" |
There was a problem hiding this comment.
This is too much anecdotal here. A comparison and description with python would be useful here.
There was a problem hiding this comment.
oh that's just me having some fun referencing an IRC conversation, one of the users in there I was bouncing ideas off of kept referencing python so I threw it in there for my light-hearted amusement.
If you wanna know though the python f string is just like f"Hello, {name}. You are {age}." and it yields a plain old string, not that different than the typical dynamic language string. It does have the advantage of being simple to explain to people though and fits well for dynamic use cases which is why the frustrated user in the narrative went back to it after giving up on D's weirdness.
Of course, this whole section is a rebuttal to Walter's (now rejected) DIP; with the proposal in this document, such things do NOT occur.
There was a problem hiding this comment.
I agree with @12345swordy, the point can be made without the dialogue in the story. Just identify what would normally be expected, vs. what would happen.
There was a problem hiding this comment.
y'all are no fun :(
There was a problem hiding this comment.
Trust me I'm fun (come to dconf, you will see). But I also want to provide no excuses for rejection.
There was a problem hiding this comment.
I think we met in 2015! (maybe not though i don't remember for sure) Though I didn't stick around after hours anyway....
I'm considering going to the 2020 dconf... but I kinda hate flying, it is going the wrong way time zone wise, it means leaving the country... so idk, I guess i need to decide soonish though to put together a talk proposal just I'm not excited about it.
anyway i changed the paragraphs there in the recent commit
There was a problem hiding this comment.
I've been going since 2013 (and in 2014 the infamous "single page slide written last night", it was a work of art). I've enjoyed your talks.
The plane ride sucks, especially going that direction.
DIPs/1NNN-ADR.md
Outdated
| auto window = createWindow(i"Process debugger $pid"); | ||
| ``` | ||
|
|
||
| It compiles without error. In bliss, the happy user thinks: "d rox". Then... the program is run and the window width is extraordinary. But the documentation says it will be automatically sized by default. The confused user wonders: why did this change? They run the program again and get another random width. Is the library incompatible with the new dmd version? |
There was a problem hiding this comment.
Be more technical and less anecdotal here. What unintended errors would have occurred if this hypothetical string interpolation feature were to be implemented? How does it compare to your implementation?
DIPs/1NNN-ADR.md
Outdated
|
|
||
| *** | ||
|
|
||
| On the other hand, with `_d_interpolated_string`, it IS possible to tell those uses apart! In fact, the library author doesn't have to do anything - the user will see a helpful error message from the compiler and perhaps try `i"Process debugger $pid".idup` instead, or another suitable alternative. |
There was a problem hiding this comment.
How? Be explictly descriptive here. Example would have assist your case here.
There was a problem hiding this comment.
It is just ordinary function overloading at this point; line 261 prior to the quoted segment (see https://github.com/dlang/DIPs/pull/186/files#diff-2d3c5bf5c5d1f001279a15e3449b2338R261 ) actually shows a sample writefln overload to specialize on one of these.
I could explicitly call back to that in the text though.
|
@marler8997 can you assist @adamdruppe on this? This needs an actual implementation in order to have any reasonable success of this being accepted. |
|
The implementation is actually very simple: I think Walter implemented his and this is a pretty small change from that (at least in dmd, but the druntime sample implementation from the doc should be copy/pasteable already). We could use that as a starting point. idk where his code is right now lol |
DIPs/1NNN-ADR.md
Outdated
|
|
||
| ### Library additions | ||
|
|
||
| While the compiler only lowers to library calls and thus does not need to know about any of this, user functionality relies on a small library implementation. |
There was a problem hiding this comment.
What the struct/function does is essential to this DIP. While the implementation may change, the end result must be defined so code can reasonably know what to expect.
To that end, I think the easiest thing is for the spec to say 3 things:
- The struct will have an alias to the Parts.
- The struct will define a
toFormatStringaccessor, and define it's aspects - For the use case of calling C functions, the struct will alias this itself to a null terminated const char *, only when all format specifiers are provided.
Then you can give a sample implementation.
There was a problem hiding this comment.
oh yeah, and actually the name of the type will have to be public somehow (my personal preference is to have the compiler use an ugly name, then the library can alias it, potentially in a module like core.interpolation;) which has the formal public interface.
But public name is important so people can actually use it in their own overloads.
DIPs/1NNN-ADR.md
Outdated
| // alias this to a string, even a string literal, loses | ||
| // the implicit conversion to const(char)* needed by | ||
| // printf. so gotta use a wrapper that explicitly returns | ||
| // that :( |
There was a problem hiding this comment.
There should be a bugzilla on this. There's zero reason for the compiler to lose this implicit conversion. Note that it works fine if the enum is done in some cases
|
|
||
| ``` | ||
| string name; | ||
| writefln(i"Hello, ${%d}name"); // causes a compile error for invalid format specification |
DIPs/1NNN-ADR.md
Outdated
|
|
||
| The helpful people in the D forum point them at the spec: "I know it looks like a string," the Master explains, "but it is actually lowered to an auto-expanding tuple whose first element is a string. That's causing your problem. I'd love to fix it in the library, but it is literally impossible to differentiate this case from legitimate usage :(" | ||
|
|
||
| "D is weird," the user replies, deflated and defeated, "I think I'm just going to use Python. Have you see its f-strings?" |
There was a problem hiding this comment.
I agree with @12345swordy, the point can be made without the dialogue in the story. Just identify what would normally be expected, vs. what would happen.
DIPs/1NNN-ADR.md
Outdated
| // printf. so gotta use a wrapper that explicitly returns | ||
| // that :( | ||
| // | ||
| // A future compiler enhancement could allow alias this to |
There was a problem hiding this comment.
How come future and not right in? This would allow:
string s = i"";
Yes?
There was a problem hiding this comment.
no, that wouldn't allow it. If the alias this thing was there, it would allow implicit conversions to a writef / format style argument list, so they wouldn't have to explicitly overload on it.
I actually think that would be a BAD thing, so I do NOT think we should do it, even if we can. I'm just trying to point out that it is possible in the future if other people disagree.
The string s = i""; thing will never work with this proposal, a tuple cannot be converted to a string without either a function (which is what .idup does in the sample implementation) or total compiler magic that I know a lot of people would object to.
|
I can't find Walter's implementation of his DIP... do any of you know if it even exists? I could do this one from scratch too, it shouldn't be hard, I just would love to base it directly off his to show how similar the two proposals really are. |
|
Yeah you could take my implementation and modify it, but would probably be better to start with Walter's implementation if he has one. Take a look at my implementation for test cases though. |
DIPs/1NNN-ADR.md
Outdated
| writefln(_d_interpolated_string!("I ate ", _d_interpolated_format_spec(null), " and ", _d_interpolated_format_spec("%d"), " totalling ", _d_interpolated_format_spec(null), " fruit.")(), apples, bananas, apples + bananas); | ||
| ``` | ||
|
|
||
| where `_d_interpolated_string` and `_d_interpolated_format_spec` are defined exclusively inside druntime. |
There was a problem hiding this comment.
What does this mean? That this won't work with better C at all?
There was a problem hiding this comment.
string is defined inside druntime and works inside betterC. This is no different.
This little addition just means that user code cannot redefine these templates to hook the language; all the rewrites in this document refer specifically to the one global definition, provided by druntime, rather than being looked up in local scope, potentially provided by a user.
There was a problem hiding this comment.
I just added more verbiage to try to clear that up a bit more, though I didn't mention betterC specifically, it'll work there. (my big concern here is to be very very clear to Walter that this is NOT "user-defined semantics")
|
As documented here: https://forum.dlang.org/post/r31g6e$2d4$1@digitalmars.com I think we should reword this so the lowering and specific type names are implementation defined (make it a suggested implementation rather than the core DIP spec). The spec just needs to document that an implementation-defined type for that call will be passed to the function as the first parameter, and the API for that type (how to get out the format strings, and/or individual string items). Given that we don't really want to name or specify the implementation of the lowering, we should have an I am also feeling concerned here about the lack of usefulness of this on existing code. Yes, we don't want string interpolations to work incorrectly with A potential solution is to provide a pre-defined template for wrapping such functions without having to do any other work. Then a simple solution is to alias this wrapper in the library. e.g.: module std.stdio;
...
alias writefln = stringInterpolationWrapper!("%s", "writefln");And now, you can use |
|
Yeah, that all works. ...except it would be Maximum flexibility with solid convenience for lib authors and users alike without sacrificing compile checks in the |
Is TypeInfo implementation defined? Because if not then isn't there precedent of ? It would be nice to be able to write normal functions that take interpolated strings. It seems like this only allows for templated functions? Or maybe I just misunderstood... |
|
On Wed, Feb 26, 2020 at 03:29:26AM -0800, Ali Akhtarzada wrote:
Is TypeInfo implementation defined?
I don't know.
`Object` is defined to live in object.d, yet the spec doesn't describe its members.
I just have no clue what's going on in Walter's head. This is why
dictatorships are bad.
Because if not then isn't there precedent of `typeid(blah) -> TypeInfo` and `i"blah" -> InterpolatedTuple(string, specifiers...)`
Note that it isn't `InterpolatedTuple(string, specifiers...)`, it is
actually `InterpolatedTuple(string), specificers...`.
I know there's a bit of a mess of parenthesis in the lowering example,
but I was fairly careful to write them somewhat precisely. ONLY the
format string itself gets a special wrapper, the others are passed just
like in Walter's proposal. This gives it maximum flexibility under
existing language rules, while still providing type safety around the
format string itself.
(i actually think ALL format strings should have a new type btw. but meh
i digress for now)
It would be nice to be able to write normal functions that take interpolated strings. It seems like this only allows for templated functions? Or maybe I just misunderstood...
Yeah, it would have to be a variadic template (so `(Args...)(Args args)`) to handle the parameters, with one of the `is(FirstArg : _d_interpolated_string!Parts, Parts...)` constraints.
It is a little bit gnarley looking, but again, maximum flexibility under existing language rules. Works in compile time and run time contexts, works with scope params, ref params, alias params, everything without defining any new special cases, thus building on what D already has.
Of course users can always just do `i"anything".idup` and get a perfectly normal `string` out of it for basic cases.
|
TypeInfo's public API is defined by the spec. So the precedent we should follow is to define that there will be a type and what its API should be. The difference here is that the type is going to be dependent on the string interpolation data. This is necessary to avoid runtime generation of format strings for forwarding to actual implementation. This means that the "type" can't be exactly defined, but its API can be, and we can define a way to test for it.
Yes, only templates can reasonably accept interpolated strings with this proposal. But there are other possibilities, that can be explored and added on to the interpolated string API later. For instance, we could generate a single type from it that gives all the same information, just at runtime. You can generate a format string, and replace it with that. You can make easy wrappers to either mix in as members or to wrap module-level functions so that adding the ability to handle interpolated strings is as easy as declaring an alias or mixing in a template. The downside of being able to distinguish interpolated strings from strings is that you have to have an overload to handle it. This means you have to opt-in to accepting them. But making that easy is straightforward.
We have introspection ;) Regarding why I used a string instead of a function alias I'm a bit concerned about a declaration like: template foo(alias symbol) {...}
alias x = foo!x;Does this work? I actually haven't tried. I suppose using the string wouldn't be any different. |
|
So this document as written doesn't define a type per se - just that the compiler lowers the format string to a templated function call. Obviously, my intention is for that to be a struct constructor, but it doesn't technically define exactly how that is implemented. But I don't understand what is wanted here. On the one hand, we're told not to specify the implementation details. But on the other hand we're told the lowering are "user-defined semantics". It is so frustrating to have no clarity. |
|
I think we need to separate out the API of the type from the sample implementation. The original DIP didn't define a type, it just lowered to a compile-time list. Our DIP necessarily needs a type. But we don't have to name it, and we don't have to specify how it needs to be implemented. We just need to specify the requirements. We don't even need to specify how the compiler will construct it via lowering. I'll see if I can submit a PR to your PR. In short, the API needs:
The example implementation can remain but is not part of the spec. |
|
On Wed, Feb 26, 2020 at 05:30:58AM -0800, Steven Schveighoffer wrote:
I'll see if I can submit a PR to your PR. In short, the API needs:
yeh i've gone full circle back to the point where i think this is a
complete waste of time. Like I inevitably end up thinking every time I try to
contribute to D upstream. I'd rather bang my head against a brick wall.
At least then I know something will actually break.
1. Since the type is a template, we need a mechanism to validate the type is in fact a string interpolation (i.e. `isStringInterpolation(T)`
by just giving the type a predictable name, we get all this for free.
but whatever, if you write it up your way I'll continue to support it.
One thing to keep in mind though is the compile-time list of actual
format args is super useful. That avoids reparsing format strings in
cases when you don't actually care about format strings.
|
|
I think it would be a shame to drop this DIP in the dustbin, just because it's here and not hard to just ask for a ruling. But I share your sentiment. I think it's likely the result here is predetermined. Sometimes even when you know the result, it's worth having a concrete thing to point at. |
I think it would be a lot more productive for you if you start coding the implementation then to describe to walter on how it suppose to work. You have a much better chance at convincing him, if he has code to look at. |
|
One annoying thing to note is that template overloads will have to be done like this if you have a generic one and one specific to string interpolation. The generic one will also need to negate string interpolation. |
|
Yeah, that's a generic problem with the language though. (It is a pity the template specialization syntax doesn't work with variadic instantiations I would note though that your example is incomplete... string interp yields There's a couple other options with various tradeoffs:
* there's two tuples passed by this proposal - one of strings and format specifiers and one of arguments. The rewrite is basically (this section is non-normative) |
Not really. You can overload a template with a non template (or maybe your template parameters are fine as-is, see below). In other words, as long as your overload isn't a generic "accept anything" type, it doesn't need the void writef(Char, A...)(in Char[] fmt, A args) {...} // existing, will not match string interpolation
void writefln(F, T...)(F format, T args) if (isStringInterpolation!F) {...} // just another overloadBut the extra cost here isn't terrible anyway, even if you DID have to add an extra constraint. It's opt-in. |
|
Ya, you can have other overloads as well of course. But, another thing I thought of, a library like |
|
That would be up to I'd actually be inclined for getopt to just go ahead and consume it internally for maximum efficiency and "just working" for th euser. Thanks to the _d_interpolated_string type, it is possible to consume the variadic arguments just fine and proceed past them (there is guaranteed to be one spec thing passed to it for each placeholder arg). We could write library functions to assist with that, though I would the ink that's beyond the scope of the dip itself. If it doesn't... it should throw a static assert or similar error now. I'll double check later when I'm on my other computer with the test program. So again the proposed diagnostic will tell users to try .idup in the worst case which isn't too bad at least. But yeah variadic template consumers and/or users passing these in general will have to be aware of how it works one way or another. |
Here is a comparison of our proposal vs. DIP 1027: // with DIP1027 you get this exact call
getopt(
args,
"type", "The type of service you want to create - %s", listOfTypes, &type,
"name", "The name of this service - %s", numServices, &name,
);
// with our version you get this:
getopt(
args,
"type", .object._d_interpolated_string!("The type of service you want to create - ", .object._d_interpolated_format_spec(null)), listOfTypes, &type,
"name", object._d_interpolated_string!("The name of this service - ", .object._d_interpolated_format_spec(null), numServices, &name,
);Let's compare! With DIP1027, you will get a compiler error, because you are not passing a pointer after the description. With our proposal, getopt has the option of handling this based on the type of the parameter. In other words, it can see that a section of parameters is an interpolation tuple, and simply forward that to std.format, or it could handle it specially, or it could throw a more tailored error (i.e. "getopt doesn't process interpolated strings directly, try using .idup on your interpolation"). As it is now, it probably would throw a similar error to DIP1027. But the major difference is that it's possible to process it. |
|
Sounds super complicated for library authors to handle this stuff. My mind is hurting just thinking of how to handle At the least it looks like |
|
It doesn't need anything specific api to expose it - all those strings and specs are available to any function/template that accepts the type. It is actually pretty easy to do in Something along those lines are how to handle with only raw, core language features. I didn't test that so I might have made small mistakes. But the point is we CAN make it work, even without any compiler help. Of course, we could offer various library facilities to make this easier for users, just I wouldn't want those part of the language spec. Like a template: that would be implemented with a recursive template in current D and actually filters those other things right out or converts it for you internally or whatever. Just to keep you from writing the long-form loop yourself. And if you don't wanna do that? Just don't - the compiler will issue the error so the user handles it at the next level up. All this is just if you need maximum flexibility as a library author planning to consume these things. |
Yeah, I was about to write that. From your examples here @aliak00 I think you just want to |
|
And don't forget, you don't have to know what the interpolation string lowering looks like to handle it. You just have to know that it's a specialized type and that it has certain properties on it. It's enough to simply understand that e.g.: getopt(
args
i"arg-$a-$b", i"Description for $a-$b", &argAB,
i"arg-$c-$d", i"Description for $d-$d", &argCD,
);Lowers to: getopt(
args
somespec, a, b, somespec, a, b, &argAB,
somespec, c, d, somespec, d, d, &argCD,
);And know that |
Ya. But from inside an API i first need to detect that it's an interpolated type because this means that X number of following arguments need to be taken in to account to call (did I explain properly that I'm thinking of this from inside an api like
Yes! I'll need to know certain properties. But it sounds like properties are not being exposed? Or did I miss that? I'll need to know that the I looked at how getopt is implemented, It'd probably need something like this: And yes of course not handling it and making the user experience of an API worse is always an option. |
|
Yea. If I have some free time later today, I'll finish my library implementation of this and post the file so you can play around with it a little yourself. I'll include some of these helpers too so it feels more realistic - a high-level helper I'm quite sure could encapsulate the whole thing for most simple cases. Like Steven said though, the biggest push behind this proposal over the old dip1027 is that such things are possible now, even if they aren't always easy. With 1027 as-rejected, it wasn't even possible in theory! Having the interp thing be like C# or Swift where the compiler constructs an object out of the arguments is definitely a lot easier to use in a lot of these situations. Then it'd just be one object, and if it has a toString method, it'd probably just work in the vast majority of existing code. And if not, the process to customize it is as simple as taking a subclass of an interface. (I argued for this for a long time, and I still like it a lot - even now I'm balanced on the fence of wanting to just go back to it!) But, again, this new proposal makes it all possible, and like I said in the newsgroup thread, is better positioned to take advantage of D's unique features like template params, non-copyable types, compile-time checking of format strings, and others. So at the cost of a bit more complexity for some library authors and users, it opens up a lot of potential. And I do think the library helpers will make all this a lot easier, just that's pure lib rather than lang (thing std.traits vs __traits). BTW on the grammar... using this with mixin code bugs me. I kinda wanna suggest that That would be an error right now since the $ is followed by an illegal char. I think. tbh not sure how Walter wrote the grammar (I just copy/pasted from his). But I propose that it NOT be an error and instead just work. This might be considered inconsistent though. So like your interpolated D code |
|
oh i forgot it is literally impossible to represent the interpolated string tuple in a library right now, even with mixin. oops. hacking dmd is harder for a demo cuz then you'd have to use my custom build too. but i guess no real choice.... (which I should have known, this was part of my original rationale for not doing string interp in a library in the first place!) |
Most def!
Yeah I read that post. Very valid points. It was based on something like this right? I was thinking, that if name and count were passed in as alias parameters to There're problems with where expressions are evaluated maybe? But the non-copyable stuff works at least. The refness is preserved with the alias, and the attribute also goes through. And maybe I misunderstood what you meant by intermediaries.
Maybe make the interpolation symbol |
yeah, something like that. But if it were passed as alias params, it wouldn't work with expressions (e.g.
so here,
Yes, that would actually be my personal preference (and has precedent in ruby at least) but it looks like everyone else liked $ and I don't wanna reopen the wound and bikeshed it. I do think # is a better fit than $ though exactly because of the dollar symbol... D very very rarely uses # so less likely to conflict. heck I'll make the change if none of you object just i also don't feel TOO strongly about it... |
I did as well. But now there's a very objective reason as to why
Does it use it at all for syntax at least? I can't think of any... |
|
On Sun, Mar 01, 2020 at 02:22:38PM -0800, Ali Akhtarzada wrote:
Does it use it at all for syntax at least? I can't think of any...
D uses it for #line, an extremely rare "special token sequence".
see dlang.org/spec/lex#special-token-sequence
I don't think we'd miss having to write ## sometimes... but the
lexer might just conflict. (Remember a q{} string is passed through
the D lexer so.... idk.)
|
|
lol i was just looking something else up and stumbled across all kind of "ast macros" in another dip
lol |
|
@adamdruppe There's an obvious double standard for DIPs, well obvious to everyone except for one person. |
That’s not the same, it’s inserting a call to something the user will never need to l ow about whereas this dip is lowering code to a new type that needs to be well defined in order to be handled by a user. |
|
@aliak00 https://forum.dlang.org/post/r32gb7$224r$1@digitalmars.com
What Walter deems as "Ast Macros" was his misunderstanding of what the feature was. That somehow you would define your own String interpolated type and change behavior based on that user defined type. That's not what was being suggested, and I don't think Walter ever fully took the time to understand what was actually trying to be implemented, he just kept shifting goal posts bring up something else that was wrong (such as with implementation details in the DIP seen above). Is it to my understanding you are going to continue to propagate this misunderstanding that he had? |
|
@anass-O talk about shifting goal posts. My comment was purely directed at the implication that lowering to a druntime function was akin to creating a new user type as in this DIP. Which it is not. The other point you bring up about walter having double standards about not wanting implementation details and then putting in implementation details himself I have said nothing about. And what an AST macro is I have also not said anything about. |
defined for the DIP. Cleaning up a few things.
Stringinterpupdates
No description provided.