-
-
Notifications
You must be signed in to change notification settings - Fork 700
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
Add 'std.format.read.formattedRead' overloads to return a Tuple with values read #8647
base: master
Are you sure you want to change the base?
Conversation
Thanks for your pull request and interest in making D better, @iK4tsu! We are looking forward to reviewing it, and you should be hearing from a maintainer soon.
Please see CONTRIBUTING.md for more information. If you have addressed all reviews or aren't sure how to proceed, don't hesitate to ping us with a simple comment. Bugzilla referencesYour PR doesn't reference any Bugzilla issue. If your PR contains non-trivial changes, please reference a Bugzilla issue or create a manual changelog. Testing this PR locallyIf you don't have a local development environment setup, you can use Digger to test this PR: dub run digger -- build "master + phobos#8647" |
It seems the documentation is not style-compliant. How do I document the template and the function correctly? I tried to look for existing examples but couldn't find any. |
You need to document both the outer template and the inner function. |
5b7cb0c
to
1870b04
Compare
update: autosquashed commits since CI passed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting approach. I'm basically in favor of this. I think it's worth adding a changelog entry for this. Can you also please add a unittest for a CTFE formattedRead
?
import std.typecons : tuple; | ||
|
||
auto expected = tuple("hello", 124, 34.5); | ||
auto result = "hello!124:34.5".formattedRead!("%s!%s:%s", string, int, double); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the format string can be checked at compile-time, why would one need to pass in types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The format string is not responsible for unformatting into the types the user wants, it's only to know the places in the input string that need to be unformatted and to tell the formatted the restrictions of the characters it's reading. Using %f
can still be valid for both float
and double
for example, and it asserts statically when the types are not format-compatible.
float value;
"123".formattedRead!"%d"(value);
// Error: static assert: "incompatible format character for floating point argument: %d"
It also asserts on orphan format specifiers or if the number of arguments is higher than the required by the format string. This is useful for generic code when we don't have total control over the argument quantity or the format string. The format string might be generated during compile time causing it to assert if some malformation occurs, or the variadic template arguments might be calculated at compile time asserting for the same reasons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually think I found a bug in checkFormatException
for the %f
format.
int i;
"123".formattedRead!"%f"(i);
// should static assert but instead throws at run time with:
// std.format.FormatException@/dlang/dmd/linux/bin64/../../src/phobos/std/format/internal/read.d(104): Wrong unformat specifier '%f' for int
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually think I found a bug in
checkFormatException
for the%f
format.int i; "123".formattedRead!"%f"(i); // should static assert but instead throws at run time with: // std.format.FormatException@/dlang/dmd/linux/bin64/../../src/phobos/std/format/internal/read.d(104): Wrong unformat specifier '%f' for int
Could you please file a bug report for this?
Just leaving the tuple members default-initialized when there's nothing to read is not good enough, because it gives the user no way to distinguish between a default value read from the input and a default value returned due to incorrectly formatted input. For example: auto result1 = formattedRead!int("0", "%d");
auto result2 = formattedRead!int("", "%d");
assert(result1 == result2); // no difference We need some other way of signalling an incomplete read in situations like this. |
We could extend the type to return the number of arguments read as the first tuple argument. But that would defeat the practicality of this, and kinda force the user to handle the value, even if it means mapping it out. We also could flip the signature and add an optional out parameter for the number of arguments read. But for that, I think the user might as well just use the already existing overload that receives a tuple since they would have to handle the value separately anyway. Tuple!(int, float) values;
if (!"1 2.125".formattedRead("%s %s", values) == 2) ...
... Or perhaps adding a sentinel template argument for that: |
Another option is to throw an exception—iirc this is what |
Not really what writeFile("deleteme", "123 ");
scope(exit) removeFile("deleteme");
"deleteme".slurp!(int)("%d");
// object.Exception@/dlang/dmd/linux/bin64/../../src/phobos/std/file.d(5191): Trailing characters at the end of line: ` ' This is runs fine: writeFile("deleteme", "123");
scope(exit) removeFile("deleteme");
assert("deleteme".slurp!(int, string)("%d %s").equal([tuple(123, "")])); |
I think the way is to throw an exception, but I would say to also provide an overload to allow the user to specify a tuple to avoid throwing and allocating on these cases. |
@ljmf00 The existing overloads of |
Ok cool. Then I think if the user wants to use this prettier version, my suggestion is to raise an exception. If the user wants to use it for control flow they have alternatives and exceptions are zero cost if not raised, so it's fine to do it. |
There's also the possibility of making it an optional choice too. Instead of always raising an exception we can have a flag to disable it (this would match the current slurp behavior). formattedRead(Flag!"exhaustive" exhaustive = Yes.exhaustive, ...)
{
...
auto argsRead = .formattedRead(...);
static if (exhaustive) enforce(argsRead == Args.length)
...
} This would make the transition to deprecate slurp in favor of this easier I think. |
Another bug I found while playing with Tuple!(int, string) tup;
"123".formattedRead!"%d %s"(tup);
// Error: static assert: "Orphan format specifier: %s" The non-templated version executes just fine. |
With only an exception being thrown there's no real way to handle failures. The best I could think of is: only("123 num", "123")
.formattedRead!(int, string)("%d %s")
.map!nullable
.handle!(<ExceptionName>, RangePrimitive.front, (e, r) => typeof(r.front).init );
// [Tuple!(int, string)(123, "num"), Nullable.null] But if the exception also stored info on how many arguments were read and the tuple result then the user could handle it better if they wanted to. There may be a high possibility the user would like to keep the read arguments. |
File a bug report. |
@atilaneves we need to decide an acceptable design for this feature. What's your thoughts about the ideas we discussed above? |
done: #8661 |
I agree with @pbackus and think that throwing an exception is the right way to deal with missing data. |
Also, use an exception, as suggested by @atilaneves |
1870b04
to
4489cd8
Compare
Sorry for the delay on this. Now both functions optionally throw a |
I will add a changelog entry when the final design gets the green light. That way I don't have to update it every time I change the design here. |
…ent 'fmt' Signed-off-by: João Lourenço <jlourenco5691@gmail.com>
…lues Signed-off-by: João Lourenço <jlourenco5691@gmail.com>
Signed-off-by: João Lourenço <jlourenco5691@gmail.com>
4489cd8
to
71f05e6
Compare
update: rebased with master |
What's the use-case for |
The main usage is to comply with the current The other usage is when the user does not care about the number of args filled at all but wants to chain the call. With the overload, they would ignore the result but they would be forced to declare all default initialized variables beforehand. With the forced exception, they would be forced to use By being optional it still follows the current design. The "error" value can be ignored, and all unfilled arguments remain unchanged, in this case, with their default initializers, while providing a compatible way to replace the alternative/s. One issue remains IMO if the exception is indeed thrown and caught. The user knows something happened with the format, but not in specific. If it failed due to unfilled arguments, the user won't know it - by the FormatException - nor how many were filled. Should I change the Exception to a more specific one that also stores how many arguments were filled? It could potentially store the tuple with all arguments as well, but that would require it to be a templated exception. Just throwing an exception doesn't really match what can be done with the overload. |
IMO the way
Under what circumstances, exactly, would a user call
I think the plain |
Well, but wouldn't that make it harder to deprecate
The same situations where users write code like: int a, b, c;
// don't care about the output
// don't care if the format ends early
formattedRead!someFormat(a, b, c);
// use a b c or any slurp usage. But once again, this was more to facilitate the transition from one to another and to keep the existing nonthrowing logic of the other overloads. |
As far as I know there is no plan to deprecate or remove If you mean, would it make it harder to refactor
In other words: situations where users write buggy code because the API makes it easy to do the wrong thing. :) Since we have the opportunity here to design a new API, I think we should try to avoid making it easy to do the wrong thing—which meas not including a "do the wrong thing" flag. |
Fair enough. I will change it and create a changelog entry when I have a bit of free time. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Fix the style issues reported by buildkite
@iK4tsu if you can resolve the style issues found by buildkite, we'll put the auto-merge tag on this. |
This enhancement was mentioned in a Discord discussion as a potentially better alternative to the existing
std.file.slurp
. Althoughstd.file.slurp
does a similar thing, it is restricted to reading from files. This feature could be extended to any input range and replace the currently implemented instd.file
since that one "combines multiple things together that would be more useful if they were separate and composable" (quoted from @pbackus).