I announced a survey over in GHC String Interpolation Survey Open!, but I got a lot of good feedback around people wanting to have a tool in hand to play around with and get a feel for the actual (not theoretical) ergonomics of the different options.
So I implemented a prototype of 5 different options here, and it’s extensible, so feel free to clone it, play around with your own proposals, and open PRs for any interesting options.
Spoiler: I kind of like the extensible-hasclass option.
Design with MPTC Interpolate can produce rather weird results when one writes instances polymorphic in string type. And given number of possible stings one would want to write such instances. For example
data Foo a = Foo { foo :: String, bar :: a }
instance (Interpolate String s, Interpolate a s) => Interpolate (Foo a) s where
interpolate Foo{..} = interpolate ("Foo<"::String)
<> interpolate foo
<> interpolate (" "::String)
<> interpolate bar
<> interpolate (">"::String)
following code will bind 5 parameters when used with SQL queries. It’s probably not something user wanted. Is it good abstraction?
HasClass
I like this design. But Interpolate is tied to string. It’s suboptimal performance-wise. We could instead work with some abstract string with builder:
class (IsString s, Monoid (Builder s)) => IsInterpolatedString s where
type Builder s = b | b -> s
toBuilder :: s -> Builder s
fromBuilder :: Builder s -> s
interpolateString :: String -> Builder s
interpolateInteger :: Integer -> Builder s
{- Probably a lot of primitives -}
and Interpolate could have polymorphic return type:
class Interpolate a where
iterpolate :: IsInterpolatedString s => a -> Builder s
Another question. Do we need extensibility? We have quasiquotes already and they serve basically same niche.
Why would someone write an instance polymorphic on String? Just write it on the specific types you use Foo with in your project
I’m not actually sure String would be noticably less performant, since it’s a flat mconcat. Maybe try running some benchmarks?
EDIT: Also, since it’s extensible, text could define their own interpolator that interpolates with Text.Builder, if performance is a bigger concern
Quasiquoters are really heavyweight; you have to use TH and figure out how to convert a string into an Exp (not sure how discoverable haskell-src-meta or ghc-meta are), both of which aren’t very welcoming to newer people. Unlike, say, macros in Rust or template literals in JS, which beginners easily use.
I’m not a fan of hardcoding a special formatting syntax a la Python f-strings. It feels too special-cased for me. It’s easily done IMO in userland, just use functions
pad :: Int -> Int -> String
pad n x = replicate (n - length (show x)) "0" ++ show x
s"Month: ${pad 2 month}"
That’s very weird question. Because it’s natural thing to do. It certainly beats writing 3 instances for string, text and lazy text. And it’s good smoke test. If polymorphic code starts to behave weirdly and it’s difficult to explain what does it do design in question likely has problems.
EDIT
Idea is about being able to define single instance which works for all string types and isn’t performance liability
It has advantage of being compact. And very common I think most if not all string interpolation implementations have some variant of it. Problem is we don’t have vocabulary of such functions.