I’m not opposed to string interpolation syntax, but it’s a surprisingly complicated feature:
We have almost no new syntax to give it aside from an escape code in “normal” string literals. Backticks are not an option since we use that for infix expressions. I’m going to assume \{ ... } escape syntax (or something equivalent like \${ ... } or whatever sigil you choose).
The issue always comes down to how you lex and parse strings then.
String interpolation must emit a series of delimiter tokens. It’s no longer a single token, but you must have things like TokStringStart, TokStringMid, TokStringEnd to represent the different boundaries because they can contain arbitrary expressions.
We already use }, ], and ) for other delimiters so this will require a stateful, context-sensitive lexer. It must know that it has emitted string “start” or “mid” token in order to decide how it should lex the delimiter.
Parsing literals is no longer a matter of casing on a single token, but now must consume an unbounded number of tokens.
All of these are surmountable of course, but are also very non-trivial.
I’m going to throw out an alternative using typeclases (and instance chains!):
class Interp a where
interp :: String -> a
instance interpString :: Interp String where
interp a = a
else instance interpFunction :: Interp a => Interp (String -> a) where
interp a b = interp (a <> b)
else instance interpShow :: (Show b, Interp a) => Interp (b -> a) where
interp a b = interp (a <> show b)
i = interp
test = i "foo" 42 "bar" true "baz"
There are a few things to note about this:
It requires no language changes.
You can write any interpolation function you want this way (for example, trimming and separating by spaces).
You can add directives in the middle of interpolation (it can be an extensible DSL).
It’s the same number characters typed as “normal” interpolation
With a very simple inliner it will compile to the code you’d write by hand.
I love it . I am convinced.
I’m not sure about using Show for anything but basic types but that is now up to the implementor of the function you showed above.
Maybe something like some custom FormatDisplay class instead of Show with some default instances for basic types and some more configurable newtypes too could be packaged up? @i-am-the-slime, @natefaubion what do you think? Do you have any plans related to packaging this solution?
The only issue with this implementation (which is pretty minor considering the brilliance of the solution and the minor inconvenience) is that the first argument must always be a String value.
For example, this code fails to compile:
interp 42 " apples and " 52 " oranges."
Could not match type
Int
with type
String
while checking that type Int
is at least as general as type String
while checking that expression 42
has type String
in value declaration main
I’m not yet sure whether it’s possible to get around that for the below reasons.
If I define a local binding that applies an empty String argument to interp, I can get around this:
main = do
let interp' = interp ""
log $ interp' 42 bar "baz" true
However, using the same binding in two different ways will produce problems:
main = do
let interp' = interp ""
log $ interp' 42 "baz" true
log $ interp' 42 true "baz" true
-- `Boolean` (from true) does not unify with String (from "baz")
This is because of interp :: String -> a, so the initial application is fixed to String. You could probably reformulate this with just interp :: a, or make interp :: a -> b with a multi-parameter typeclass.
I wasn’t sure how to encode the type class using a multi-parameter typeclass. Everything I’ve tried runs into a problem sooner or later. Your solution seems to work only because the first argument is hard-coded to String.
Also, don’t use this library when doing a fold (e.g. foldl i "" arrayOfInts) until the inliner optimization is done. See this benchmark, which I hope isn’t naive.
Edit: the above benchmark was implemented incorrectly. See this comment for an accurate one.
So I saw the benchmark, the blue line indicates standard append and it blows up, while I guess the red should have, right?
I’ve run this benchmark with changed functions with one size = 10000.
Raw data results (because making a graph is beyond my skills )
foldl (\a b -> show a <> show b) "" array causes JavaScript heap out of memory
0.001428 mean of foldl (\a b -> a <> show b) "" array
0.001227 mean of foldl i "" array
0.001106 mean of foldl interp "" array
In the original benchmark (1) is blue (the blow-up one) and (3) is red.
Remarks:
Running show on an accumulated string breaks my memory
Difference between (2),(3),(4) is negligible - using fold with interp is ok here
I think the performance problem is not here, but when you write a long expression with interp (or when it is generated with another typeclass generic magic)
Example
Simple usage of interp vs. append
fooI = i "a" 1 "b" 2 "c" 3
fooA = "a" <> show 1 <> "b" <> show 2 <> "c" <> show 3
And the compiled JS (without inlining)
var fooI = function (dictInterp) {
return Data_Interpolate.i(
Data_Interpolate.interpStringFunction(
Data_Interpolate.interpIntFunction(
Data_Interpolate.interpStringFunction(
Data_Interpolate.interpIntFunction(
Data_Interpolate.interpStringFunction(
Data_Interpolate.interpIntFunction(
dictInterp)))))))("a")(1)("b")(2)("c")(3);
};
var fooA = "a" + (Data_Show.show(Data_Show.showInt)(1) + ("b" + (Data_Show.show(Data_Show.showInt)(2) + ("c" + Data_Show.show(Data_Show.showInt)(3)))));
So, the original benchmark I created used \acc next -> show acc <> show next when it should have been \acc next -> acc <> show next. @paluh pointed this out.
Here’s the correct benchmark, which better reflects my expectation: