Skip to content

string interpolation by passing format string to template, args as tuple#186

Merged
mdparker merged 9 commits intodlang:masterfrom
adamdruppe:lol
Sep 8, 2020
Merged

string interpolation by passing format string to template, args as tuple#186
mdparker merged 9 commits intodlang:masterfrom
adamdruppe:lol

Conversation

@adamdruppe
Copy link
Contributor

No description provided.

Copy link

@12345swordy 12345swordy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some thoughts on this.

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?"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too much anecdotal here. A comparison and description with python would be useful here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

y'all are no fun :(

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trust me I'm fun (come to dconf, you will see). But I also want to provide no excuses for rejection.

Copy link
Contributor Author

@adamdruppe adamdruppe Feb 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?
Copy link

@12345swordy 12345swordy Feb 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How? Be explictly descriptive here. Example would have assist your case here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@12345swordy
Copy link

12345swordy commented Feb 23, 2020

@marler8997 can you assist @adamdruppe on this? This needs an actual implementation in order to have any reasonable success of this being accepted.

#dlang/dmd#7988 (comment)

@adamdruppe
Copy link
Contributor Author

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

Copy link
Member

@schveiguy schveiguy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds awesome.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. The struct will have an alias to the Parts.
  2. The struct will define a toFormatString accessor, and define it's aspects
  3. 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

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?"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come future and not right in? This would allow:

string s = i"";

Yes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@adamdruppe
Copy link
Contributor Author

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.

@marler8997
Copy link
Contributor

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean? That this won't work with better C at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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")

@schveiguy
Copy link
Member

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 isInterpolatedString!(T) template defined by the spec for usage on template functions (they have to be template functions) that act differently for string interpolations.

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 CreateWindow, but it would be nice to have it work out of the box with D code like writefln or mysql-native (even if you have to specify default formats). In that sense, you need to define an overload to get it to work, which is going to be a basic wrapper, and a lot of boilerplate.

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 writefln(i"I have $apples apples"). This could be mentioned as a possible library addition outside the scope of the DIP.

@adamdruppe
Copy link
Contributor Author

adamdruppe commented Feb 25, 2020

Yeah, that all works. ...except it would be stringInterpolationWrapper!(writefln, "%s", 0), taking an alias. and prolly "%s" is also likely the default second arg and 0 as the default third. The zero is the index of the argument you intend to start the interpolation on (consider sformat or sprintf where it would be 1, since the 0th arg is the buffer). Then the user could even define and use it inline: Wrapper!writefln(i""); at the usage point even if the lib author doesn't provide it.

Maximum flexibility with solid convenience for lib authors and users alike without sacrificing compile checks in the createWindow etc. case.

@aliak00
Copy link

aliak00 commented Feb 26, 2020

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)

Is TypeInfo implementation defined? Because if not then isn't there precedent of typeid(blah) -> TypeInfo and i"blah" -> InterpolatedTuple(string, specifiers...)

?

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...

@adamdruppe
Copy link
Contributor Author

adamdruppe commented Feb 26, 2020 via email

@schveiguy
Copy link
Member

Is TypeInfo implementation defined? Because if not then isn't there precedent

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.

It would be nice to be able to write normal functions that take interpolated strings. It seems like this only allows for templated functions?

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.

consider sformat or sprintf where it would be 1, since the 0th arg is the buffer

We have introspection ;) wrapper!(sprintf, "%s")(target, i"I'm so sick of these $adjective apples") should be able to figure it out. It might not even have to -- the caller tells you where the formatting string should go. Even if they get it wrong, they would have got it wrong with a straight sprintf call too.

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.

@adamdruppe
Copy link
Contributor Author

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.

@schveiguy
Copy link
Member

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:

  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)
  2. The type should provide compile-time access to a format string with a provided default format specifier (i.e. toFormatString)
  3. The type should provide compile-time access to a null-terminated C format string (i.e. toFormatStringz)
  4. If all specifiers are present, the type shall implicitly convert to the null-terminated C format string.

The example implementation can remain but is not part of the spec.

@adamdruppe
Copy link
Contributor Author

adamdruppe commented Feb 26, 2020 via email

@schveiguy
Copy link
Member

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.

@12345swordy
Copy link

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.

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.

@aliak00
Copy link

aliak00 commented Feb 27, 2020

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.

void g(T)(T t) if (!isStringInterp!T) {}
void g(T)(T t) if (isStringInterp!T) {}

@adamdruppe
Copy link
Contributor Author

adamdruppe commented Feb 27, 2020

Yeah, that's a generic problem with the language though. (It is a pity the template specialization syntax doesn't work with variadic instantiations writefln(Fmt : _d_interpolated_string!Specs, Specs..., Args...)(Fmt fmt, Args args) could avoid the negation nicely).

I would note though that your example is incomplete... string interp yields (T, Args...), not just T. But still, I get the point.

There's a couple other options with various tradeoffs:

  1. Use static if inside the template instead of a constraint on an overload. This is actually my preference in general (also tends to give much nicer compile errors btw), but does mean you need to edit the body of the function in question. But there's a good chance you'd want to anyway for various other reasons, so I'm totally OK with this. Won't overload with non-templates... but when doing that, the single constraint is enough; no need to negate on a non-templated thing.

  2. Change the proposed type to be friendlier to specialization syntax while still being a template. Instead of a tuple*, pass two value-type arrays, or something like that. We'd have to define rules to recombine these two arrays back into a single stream; more complicated implementation on the compiler and user side, but doable without loss of functionality (or we trade functionality for a simpler implementation, but I don't want that). Then we don't use constraints and instead use specialization.

* 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) tuple(indicator!(strings, and, format, specs), args, to, format) so just gotta keep in mind which tuple we're talking about

@schveiguy
Copy link
Member

One annoying thing to note is that template overloads will have to be done like this

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 !isStringInterpolation.

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 overload

But the extra cost here isn't terrible anyway, even if you DID have to add an extra constraint. It's opt-in.

@aliak00
Copy link

aliak00 commented Feb 28, 2020

Ya, you can have other overloads as well of course. But, another thing I thought of, a library like getopt What will happen to this:

getopt(
  args,
  "type", i"The type of service you want to create - $listOfTypes", &type,
  "name", i"The name of this service - $numServices", &name,
);

@adamdruppe
Copy link
Contributor Author

That would be up to getopt... (another really good example for why we need to be able to detect these things though!)

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.

@schveiguy
Copy link
Member

What will happen to this

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.

@aliak00
Copy link

aliak00 commented Feb 28, 2020

Sounds super complicated for library authors to handle this stuff. My mind is hurting just thinking of how to handle getopt with multiple interpolated strings:

getopt(
  args
  i"arg-$a-$b", i"Description for $a-$b", &argAB,
  i"arg-$c-$d", i"Description for $d-$d", &argCD,
);

=>
getopt(
  args
  .obj._d_str!("arg- ", .obj._d_fmt(null), "-", .obj._d_fmt(null))(), a, b,
    .obj._d_str!("Description for ", .obj._d_fmt(null), "-", .obj._d_fmt(null))(), a, b, 
      &argAB,
  .obj._d_str!("arg- ", .obj._d_fmt(null), "-", .obj._d_fmt(null))(), c, d,
    .obj._d_str!("Description for ", .obj._d_fmt(null), "-", .obj._d_fmt(null))(), c, d, 
      &argCD,
);

At the least it looks like _d_interpolated_string will need to expose the number of _d_interpolated_format_specs? so that an API like getopt can deal with multiple interpolated strings? (i think - still feels really complicated)

@adamdruppe
Copy link
Contributor Author

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 static if.

// this could be a standalone library candidate btw
// but just a helper function to contribute to the filter coming next
bool isInterpolatedArgument(size_t idxq) {
    int count;
   static foreach(idx, arg; args) {
       static if(is(typeof(arg) == _d_interpolated_string!specs, specs...)) {
          static foreach(idx2, part; specs) {
                   static if(is(part == _d_fmt))
                           count++;
       }
        if(idxq == idx) return count > 0;
        if(count) count--;
     }
     return false;
}

static foreach(idx, arg; args) {
    static if(is(typeof(arg) == _d_interpolated_string!specs, specs...)) {
         // this is the actual custom zero-allocation format handling
          foreach(idx2, part; specs) {
              static if(is(part == string))
                    sink(string);
              else static if(is(part == _d_fmt))
                     sink_formatted(part.fmt, args[idx + idx2 + 1]);
              else static assert(0); // in violation of the spec, should never happen
          }
    } else static if(isInterpolatedArgument(idx)) {
           // handled above, so intentionally left blank to avoid double processing...
    } else static if(whatever) {} // handle other supported types like we do currently
}

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:

foreach(idx, arg; handleInterpolation!(args, my_custom_string_sink, my_custom_arg_handler)) {
        // reduces it down to a single arg for easier use here
}

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.

@schveiguy
Copy link
Member

schveiguy commented Feb 28, 2020

And if you don't wanna do that? Just don't

Yeah, I was about to write that. From your examples here @aliak00 I think you just want to .idup the interpolated strings anyway. That's probably what getopt should require. But a) the compiler should give a nice error for this, and b) it's also NOT possible for getopt to always error if the interpolation format is not a separate type from string. The errors you get would be confusing, and you could do some really weird stuff. You could even write interpolation strings that match what getopt is expecting unintentionally (though probably not likely).

@schveiguy
Copy link
Member

schveiguy commented Feb 28, 2020

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 somespec is a language construct that allows creating formatting strings, and accessing the formatted pieces for the trailing parameters.

@aliak00
Copy link

aliak00 commented Feb 28, 2020

Yeah, I was about to write that. From your examples here @aliak00 I think you just want to .idup the interpolated strings anyway.

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 idup on. Hence the need to know the count.

(did I explain properly that I'm thinking of this from inside an api like getopt and not as a caller?

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.:

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 a and b that comes after somespec is part of somespec. So I think @adamdruppe was on the right track here.

I looked at how getopt is implemented, It'd probably need something like this:

void getopt(Args...)(auto ref Args args) {
    static if (args.length)
    {
        static if (isInterpolatedString!(typeof(args[0]))
        {
            // get the string out
            string istr = args[0.. args[0].specLength].format; // or idup, whatevs

            // repass it along and slice out the remaining args
            getopt(istr, args[args[0].specLength .. $]);
        }
        // ...
    }
    //...
}

And yes of course not handling it and making the user experience of an API worse is always an option.

@adamdruppe
Copy link
Contributor Author

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 $ not followed by (, {, or an identifier character should NOT be an error and instead just output the literal $.

// I hate the concat thing but that's what's in there so.... yeah
i"" q{
    a = a[0 .. $];
}

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.

i"$5"; // not an error, literal string "$5"
i"$foo"; // interpolates the foo variable
i"$$"; // literal string "$"
i"$(5)"; // interpolates the D expression (5)
i"${}5"; // error! the { triggered the interpolation parser and the following 5 is invalid now
i"$]"; // not an error, literal string "$]"
i"${}]"; // error! { triggered interpolation parser and ] is not an identifier

So like your interpolated D code

i"" q{
   void $name() { // interpolated name
         a = a[0 .. $-1]; // no error here
         b = "$5"; // no error here
         c = "$foo"; // interpolates the foo!!! The interpolator just looks at chars, it doesn't know the context except for the very next character
   }
};

@adamdruppe
Copy link
Contributor Author

adamdruppe commented Feb 28, 2020

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!)

@aliak00
Copy link

aliak00 commented Feb 29, 2020

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!

Most def!

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.

Yeah I read that post. Very valid points. It was based on something like this right?

auto s = new_type!(
  "hi ", spec(null), ", you are visitor ", spec("%2d")
)(name, count);

I was thinking, that if name and count were passed in as alias parameters to spec would that help? As this program seems to do ok:

import std;

struct S {
    @disable this(ref S);
}

void func(Args...)(auto ref Args) {}

struct somespec(_pack...) {
    alias pack = _pack;
    pragma(msg, typeof(pack));
    pragma(msg, __traits(getAttributes, pack[0]));
}

void pass(T)(T) if (is(T == somespec!pack, pack...)) {
    pragma(msg, "intermediary: ", typeof(T.pack));
    pragma(msg, "intermediary: ", __traits(getAttributes, T.pack[0]));
}

void main()
{
    int f() { return 2; }
    @("yo")
    int a;
    pass(
       	somespec!(a, 3 + 2, f, S())()
    );
}

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.

BTW on the grammar... using this with mixin code bugs me. I kinda wanna suggest that $ not followed by (, {, or an identifier character should NOT be an error and instead just output the literal $.

Maybe make the interpolation symbol # ?

@adamdruppe
Copy link
Contributor Author

Yeah I read that post. Very valid points. It was based on something like this right?

yeah, something like that. But if it were passed as alias params, it wouldn't work with expressions (e.g. a + 2). It could pass them as wrapped lambdas but then it can be evaluated zero or more times which is likely surprising to people, and that doesn't work with various cases either, like if you specifically do want an alias passed through.

   	somespec!(a, 3 + 2, f, S())()

so here, a works due to compiler alias magic, and 3+2 works because both are constants and can be CTFE'd. But try a+2 and you'll get an error. With pure alias, it will be not an lvalue so there is no symbol to alias, and with a template value param, it will complain a cannot be read at compile time.

Maybe make the interpolation symbol # ?

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...

@aliak00
Copy link

aliak00 commented Mar 1, 2020

everyone else liked $

I did as well. But now there's a very objective reason as to why # would be a better choice - hopefully there will be no need to bikeshed it unless we've missed something ;)

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.

Does it use it at all for syntax at least? I can't think of any...

@adamdruppe
Copy link
Contributor Author

adamdruppe commented Mar 1, 2020 via email

@adamdruppe
Copy link
Contributor Author

lol i was just looking something else up and stumbled across all kind of "ast macros" in another dip
https://github.com/dlang/DIPs/blob/master/DIPs/other/DIP1008.md

Such destruction is done by calling:
_d_delThrowable(e);

lol

@anass-O
Copy link

anass-O commented Mar 16, 2020

@adamdruppe There's an obvious double standard for DIPs, well obvious to everyone except for one person.

@aliak00
Copy link

aliak00 commented Mar 16, 2020

lol i was just looking something else up and stumbled across all kind of "ast macros" in another dip

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.

@anass-O
Copy link

anass-O commented Mar 18, 2020

@aliak00
The issue Walter had was that the spec and any DIPs shouldn't include implementation details, specifically that a feature needs to be implemented by a library call to druntime.

https://forum.dlang.org/post/r32gb7$224r$1@digitalmars.com

DIPs for the core language specify only behaviors, not implementations. Implementation possibilities can be included in an advisory manner only.

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?

@aliak00
Copy link

aliak00 commented Mar 19, 2020

@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.

@mdparker mdparker merged commit 6225c25 into dlang:master Sep 8, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants