To start off, who here has been following the proposal? Let me emphasize that the proposal is still evolving. I'm presenting the latest version of the proposal, but details may change in the final feature.
String interpolation allows ergonomically building strings by inlining expressions directly in the string. It's available in many popular languages like Python, Rust, Javascript/Typescript, and Scala. Anecdotally, a lot of people coming to Haskell from these other languages ask how to do it / why Haskell doesn't have it, and it's one thing I personally miss a lot when writing Haskell. I originally opened this proposal in Jan 2023. After lots of discussion and evolution, it should be in the final stages of review now, and will hopefully be available in GHC 10.2.
In ghc-string-interpolation-sandbox: - cabal run demo - cabal bench ./bench
First, we introduce the `Interpolate` class. One major advantage of string interpolation is the ability to automatically convert values to string within an interpolation. Primitive values like `Int` should convert to `String`, then lifted with `fromString`. But most user-defined types should only need to delegate to other `Interpolate` instances, like `SrcLoc` here. You could implement `SrcLoc` here by converting to String with `fromString`, but keeping everything as `s` avoids roundtripping via String in some cases.
This is the final desugaring, but let's break it down in pieces.
At its core, string interpolation simply calls `interpolate` on every value and concatenates them all together. For simple examples like this, this should already be pretty performant. `mappend` is right-associative, so it should be linear performance. But what if `interpolate` does more concatenations? Let's use `ShowS` to be safe.
This uses the `ShowS` technique to avoid O(n^2) performance when `interpolate` includes additional concatenations. Great; at this point, we're at a decent implementation for `String`.
<Walk through each bullet> To solve these last issues, we can turn to `-XQualifiedStrings`.
How does this help us? Going back to where we left off:
This desugaring involves five points of customization: buildString, fromString, interpolate, and append/empty. To get to the final desugaring, we decouple the desugared function calls from the implementation.
Now, we can define the definitions at the bottom in some internal GHC module and desugar the interpolation this way. And now we have 5 functions that can be hooked into with QualifiedStrings (e.g. `M.interpolateRaw`, etc.) You might be wondering why we don't keep fromString/mappend/etc. as the overloadable names with QualifiedStrings. If a module wants to be used with qualified strings and also export other names, these are common names that could conflict with other exports. This also clearly identifies the string interpolation functions separate from the rest of the API. You might also be wondering why we should provide interpolateRaw/interpolateAppend/interpolateEmpty; we could instead always desugar to IsString/Monoid operations. But that restricts the ability for custom interpolators to those interfaces. For example, it would preclude type-changing appends like building a heterogenous list.
As part of the feature, we will also be shipping some interpolators out of the box in the `ghc-experimental` library. One useful interpolator is `Basic`, which does not do any implicit conversions or finalization. This is useful for cases like Text.Builder, where roundtripping via String using the built-in Interpolate class massively degrades performance, and it's reasonable to explicitly convert.
Low barrier: String interpolation is a pretty unique feature in that even newcomers are familiar with it from other languages, so everyone has opinions, whether they're new to Haskell or have been around. Bikeshedding: String interpolation also involves so many decisions: delimiter for quotes, delimiter to interpolate, whether to automatically convert or not, how performant it needs to be. It's hard to think of another proposal with this many orthogonal decisions that need to be made at once. String interpolation in general is a large design space, so I think this particular proposal has unique challenges that probably won't be a common occurrence in other proposals.
However, Haskell also has specific challenges that other languages don't face. The biggest one is having two utf-8 string types, where the more performant one is not in the stdlib. This forces us to make a trade-off between ergonomics, performance, and simplicity. The current design is relatively simple, with the default interpolator being ergonomic and somewhat performant, and qualified interpolators being somewhat ergonomic and mostly performant. There's a lot of disagreement on the weighting of these priorities, which drags out the discussion.
Lastly, there's the non-technical aspects of the proposal. Let's be honest; we Haskellers love our theoretical purity, and this feature is completely driven by ergonomics and practicality. I'll give the community credit; I think overall, the community has engaged with the proposal well and in good faith. But there has been a non-zero number of people who feel strongly about 'Avoid "success at all costs"' and see this as unacceptable cost, which has added friction to the process. I do think some pushback is healthy, but I would challenge the community to keep this in mind and continue making sure pushback is in good faith. The Github interface itself also made conversation a bit challenging. If Github hides a comment behind "Load more", you have to keep clicking "Load more" until you get to the end of the hidden history, which takes a long time for 627 comments. Also, if people make top-level comments, it's hard to respond to individual comments, especially if 5 top-level comments were added overnight. I don't have a good solution for this, and maybe this particular proposal is uniquely controversial such that this isn't a common problem, but I thought it would be worthwhile bringing up.