Per the proposal, with -XOverloadedStrings (or -XQualifiedStrings), you can use the same syntax with different types. So it’s exactly consistent with vanilla string literals in that respect — if String is what you want to use in general, you get that by default, and if it isn’t, the same extensions that you’d enable to get convenient string literals are the ones that you’d enable to get convenient interpolated strings.
I’m not sure I follow. When I write fromText "dec " <> decimal 42 <> fromText " hex " <> hexadecimal 42 :: Data.Text.Lazy.Builder, at no point of execution any String is allocated. Does the proposal for string interpolation allow me achieve the same? How?
Upd.: Apparently instance IsString Builder is lacking; I’ve updated the example.
s"dec ${decimal 42} hex ${hexadecimal 42}" will desugar to
interpolateString $ \convert raw append empty ->
raw "dec " `append`
convert (decimal 42) `append`
raw " hex " `append`
convert (hexadecimal 42) `append` empty
With OverloadedStrings enabled, interpolateString is not confined to being the proposed Data.String.Interpolate.Experimental.interpolateString, with its use of the Interpolate class with the offending type signature. It will be any interpolateString you’ve imported, with any type that allows the code to compile. If it is typed such that the raw parameter can accept Text, then "dec " etc. will also take advantage of OverloadedStrings and be Text literals. I don’t know if GHC optimizes Text.pack "literal" such that no String allocation results, but if it does, then this example will also be String-allocation-free.
No, OverloadedStrings would keep using the built-in interpolateString, which works for any IsString. You’d have to use M.s"..." to use the interpolateString from M.
@Bodigrim in your example, you’d have to implement Builder.interpolateString (which can have any type you want, e.g. no Interpolate class and force all interpolations to be a Builder) and do Builder.s"...". If you want to use the default s"..." syntax, you have to interpolate via the Interpolate class with interpolate :: s -> String. That’s the crux of this proposal: a decent, simple implementation with decent performance for default string interpolation, with flexibility in qualified string interpolation.
It does (or at least it should) for literal literals.
I’m probably too dense tonight. Could you possibly give an example? (And maybe add it to the proposal too, I’m likely not the only one to scratch my head)
Would it allow me to write Builder.s"The number is ${n}"? How would it know how to convert n to Builder efficiently, avoiding String?
Oh? So when the proposal says, ‘This [the qualified interpolation syntax] allows using locally-scoped string interpolation for non-String types without enabling it globally with -XOverloadedStrings,’ it’s not actually claiming that -XOverloadedStrings does enable it globally? Might want to clarify that sentence then.
Anyway, if any non-qualified use of the syntax forces you to use the Interpolate-based API, I share @Bodigrim’s concerns. If interpolation goes through String, it isn’t really allowing overloading in the same way that OverloadedStrings does. I think other users are likely to miss that, particularly if it results in code that compiles but doesn’t optimize well.
If you’re trying to nudge people into using the qualified syntax for anything other than the simplest case, then wouldn’t it be better to drop the IsString-based signature on the default builder and just restrict it to Strings, with OverloadedStrings applying one big fromString to the result of the interpolation, if enabled, exactly as it does for uninterpolated literals?
(Personally, I’d prefer something that allows for the unqualified syntax and a non-String-based implementation — maybe via an interaction with -XRebindableSyntax instead, which is closer to how I thought this was going to work — but that’s a more subjective request.)
Builder.interpolateString would have total control over its definition, so facetiously, you could implement it however you want
:
- Avoid any implicit conversions, force any interpolations to have type
Builderby settingconvert = id - Implement your own
Builder.Interpolateclass (with a new set of instances) - Reuse the existing
interpolateSfunction in the providedInterpolateclass and setconvert = Builder.fromShowS . interpolateS(assumingBuilder.fromShowSfuses correctly)
The “Builder” section in the Appendix of the proposal might be of note to you, but I could augment it if some of these details would be helpful in there.
@rhendric Two different things - if you have just StringInterpolation set, you can use s"..." only for String. If you have StringInterpolation + OverloadedStrings, all s"..." exprs will add fromString invocations. If you have StringInterpolation + QualifiedStrings, you’d separately have access to M.s"...", however M defines interpolateString.
I don’t understand this. It’s still overloading in the sense that everything gets coerced to IsString s after initially going through String (either the literal or via interpolate). And in practice, wouldn’t most implementations of interpolate for Text be T.pack $ ...? While I appreciate the conceptual “it’d be nice to not go through String at all in the first place”, I don’t see a practical benefit for the majority of simple string interpolation use cases.
I think it would help if this section illustrated how to “implement your own Builder.Interpolate class (with a new set of instances)” with things like
instance Interpolate Int where
interpolate = Data.Text.Lazy.Builder.Int.decimal
I might be dense now too, but you’d just… implement a new class?
module Data.Text.Lazy.Builder.Qualified (interpolateString, Interpolate (..)) where
import Data.Text.Lazy.Builder
interpolateString :: _ -> Builder
interpolateString f = f interpolate fromString mappend mempty
-- To make a new data type interpolatable (e.g. URL),
-- it would need to add an instance of Data.String.Interpolate (to interpolate into non-qualified strings)
-- and this one (to interpolate into Builder-qualified strings)
class Interpolate a where
interpolate :: a -> Builder
instance Interpolate Int where
interpolate = decimal
Overall I don’t believe that relying on rewrite rules for fundamental optimizations is a wise strategy.
There is a comparable situation where this is already being done in “base”: fromIntegral.
fromIntegral :: (Integral a, Num b) => a -> b
fromIntegral = fromInteger . toInteger
Much like the way the proposed Interpolate uses String, fromIntegral uses Integer as an intermediate datatype that is converted to and from. This of course is very inefficient in many cases. So there’s a bunch of rewrite rules that kick in after fromIntegral is inlined and remove the indirection for common combinations of source/destination types.
This is explained in the Optimising conversions between numeric types GHC note.
The StringLiteralsAreText extension is called NamedDefaults , available in GHC since 9.12:
{-# LANGUAGE NamedDefaults #-}
module ...
default IsString (Text)
And the default declaration can be exported, so need to repeat it in every module.