Juleps
Juleps copied to clipboard
WIP: string formatting Julep
Thanks to @andyferris for the idea.
I was wondering - do we need a new stringformat
function or can we just add keyword arguments to our string
methods? E.g. https://github.com/JuliaLang/julia/pull/25804
We could for keyword arguments, but not for positional (since string(a,b)
is equivalent to string(a)*string(b)
).
Just to pull together my (now hidden) suggestions from above, what if we made "π is approximately $(pi, fracdigits=4)."
lower to something like:
LazyString(("π is approximately ", StringFormat(pi, fracdigits=4), "."))
with the definitions:
# String-like object formed by lazily concatenating printed args
struct LazyString{T<:Tuple} <: AbstractString
args::T
end
function write(io::IO, ls::LazyString)
# write this loop more cleverly to unroll at compile-time
for arg in ls.args; print(buf, arg); end
end
String(ls::LazyString) = let buf=IOBuffer(); write(buf, ls); String(take!(buf)); end
# ... other string methods...
struct StringFormat{T,KW}
x::T # the value to be printed
kws::KW # NamedTuple of keyword arguments
end
write(io::IO, f::StringFormat) = print(io, f.x) # fallback method
Then you could write specialized StringFormat
methods as desired to handle various keyword options for specific types T
.
Using a LazyString
object would allow common things like print(io, "foo $bar")
to be fast, allocating minimal temporary storage and making no string copies. On the other hand, other operations on a LazyString
object would potentially be slow, especially if it is stored and re-used … in such cases, you should convert to a String
. The construction of a LazyString
might also be surprising to new users.
(This would be a breaking change since "foo $bar"
would also no longer produce String
.)
I can see why we'd want LazyString
in principle, but to have a = "foo $bar"
not produce a string would be pretty surprising for casual use. That kind of seems like a bad user experience to me and a bit overly complex without a really compelling benchmark. On the whole eager formatting seems like a better default to me. It seems reasonable that the relatively small amount of performance critical string printing code can pay the syntax burden of using Simon's @print
macro or something similar.
I agree that from a usability perspective, strings should be the default result. Another option would be to make the lazy version available via a string macro?
A negative thing about having the formatting inline is that it makes it harder to read the string:
"π is approximately $(e, fracdigits=6, someother=2), while e is approximately $(e, fracdigits=6, someother=2) ."
vs
"π is approximately ${pi}, while e is approximately ${e}.".format(
pi = fmt(pi, fracdigits = 6, someother = 6,
e = fmt(e, fracdigits = 6, someother = 6)
Separating content and formatting is usually also a good idea.
@KristofferC I haven't seen any other language do that sort of thing, but we could do that via let
blocks
let pi = fmt(pi, fracdigits = 6, someother = 6), e = fmt(e, fracdigits = 6, someother = 6)
"π is approximately $pi, while e is approximately $e."
end
Note that all of this could potentially be implemented in a package if you are willing to use a string macro, e.g. f"..."
.
You could just do "π is approximately $(fracdigits(π, 4))."
, where fracdigits
can return a stringformat object. Seems like this is more easily extensible in that I can define my own rounding routine, for example.
@MikeInnes, the problem with that is that it forces the construction of a temporary string, which slows down I/O.