JuliaLang / Juleps

Julia Enhancement Proposals
Other
67 stars 24 forks source link

WIP: string formatting Julep #49

Open simonbyrne opened 6 years ago

simonbyrne commented 6 years ago

Thanks to @andyferris for the idea.

andyferris commented 6 years ago

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

simonbyrne commented 6 years ago

We could for keyword arguments, but not for positional (since string(a,b) is equivalent to string(a)*string(b)).

stevengj commented 6 years ago

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

c42f commented 6 years ago

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.

simonbyrne commented 6 years ago

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?

KristofferC commented 6 years ago

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.

simonbyrne commented 6 years ago

@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
stevengj commented 6 years ago

Note that all of this could potentially be implemented in a package if you are willing to use a string macro, e.g. f"...".

MikeInnes commented 6 years ago

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.

stevengj commented 6 years ago

@MikeInnes, the problem with that is that it forces the construction of a temporary string, which slows down I/O.