Crystal: [RFC] Format options within string interpolations

Created on 13 May 2020  路  5Comments  路  Source: crystal-lang/crystal

This is not even a proposal, just an idea that crossed my mind some time ago, but it makes more sense now that #9134 is merged.

When a string is interpolated, the expressions are evaluated with a call to to_s(IO) receiving the string builder. But there are some classes that have overloads receiving extra arguments to customize the format. For example:

  • Int#to_s(io : IO, base : Int, *, upcase : Bool)
  • Time#to_s(io : IO, format : String)

It's currently not possible to pass values to these arguments, thus having to create intermediate strings with the desired format. The idea is have a syntax usable within interpolations to pass some values. The compiler still calls to_s(IO) appending the extra arguments.

"Hex integer: #{value :: 16, upcase: true}, Time: #{time :: "%H:%M"}"
feature discussion compiler

Most helpful comment

I like this.

I think we should also consider method calls. For example:

string = "world"
"hello #{world.upcase}"

The problem here is that you want the upcase call with the IO, argument, how to specify that?

So in general, maybe it can be like this

If there's an overload for the method that accepts an IO as a first argument, use that in string interpolation.

Then the code in the original snippet becomes:

"Hex integer: #{value.to_s(16, upcase: true)}, Time: #{time.to_s("%H:%M")}"

but the IO overloads end up being called automatically. And it also works with methods other than IO.

It's also kind of transparent to the user: they just call the regular methods, but the compiler finds and uses optimized methods. We could even not mention this implementation detail and everything would work well out of the box.

All 5 comments

I like this.

I think we should also consider method calls. For example:

string = "world"
"hello #{world.upcase}"

The problem here is that you want the upcase call with the IO, argument, how to specify that?

So in general, maybe it can be like this

If there's an overload for the method that accepts an IO as a first argument, use that in string interpolation.

Then the code in the original snippet becomes:

"Hex integer: #{value.to_s(16, upcase: true)}, Time: #{time.to_s("%H:%M")}"

but the IO overloads end up being called automatically. And it also works with methods other than IO.

It's also kind of transparent to the user: they just call the regular methods, but the compiler finds and uses optimized methods. We could even not mention this implementation detail and everything would work well out of the box.

Either variant is a bit obscure to the user, but I definitely prefer the compiler just looking for an IO argument in a regular call than a new syntax.

I have actually thought about this before, but wanted to wait until after 1.0 before discussing the idea =)

IMO the approach @asterite describes seems like a good way. I think it still needs an additional step, though because I wouldn't want to just to impose some implicit meaning on any method that receives an IO as first argument. The solution I had in mind was using an annotation. I haven't found a good name yet, so I'll use @[Stringify] as a working title.
That annotation could even be used to automatically define the non-IO overload for returning string.
These methods are typically just duplicates of the IO-overload without the actual IO argument and some boilerplate with String.build:

def foo(arg : Bool) : IO
  String.build do |io|
    foo(io, arg)
  end
end

def foo(io : IO, arg : Bool) : Nil
  # ...
end

With such an annotation, we could omit that explicit overload:

@[Stringify]
def foo(io : IO, arg : Bool) : Nil
  # ...
end

And it can be used for other purposes, such as string interpolations. I don't know how that could be implemented, though. But at least from a user perspective, this would be a neat solution and totally transparent because of the annotation.

I'm worried about the magic but I too have thought about this at times and if it's worth it.

The annotation idea looks workable to me.

Here's how Erlang/Elixir solves a related problem class: https://www.evanmiller.org/elixir-ram-and-the-template-of-doom.html (In particular the "IO list" data structure).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

RX14 picture RX14  路  62Comments

straight-shoota picture straight-shoota  路  91Comments

asterite picture asterite  路  70Comments

malte-v picture malte-v  路  77Comments

chocolateboy picture chocolateboy  路  87Comments