I’m really excited about the hybrid solution that came out of this process; I think this will truly be the best of all the worlds. A tremendous thank you to everyone who participated, I think we’ve definitely come out of this with a much stronger proposal than what I started with.
Feel free to post comments/questions in this thread, I will respond to what I can. But no matter what, I promise that there will be no more surveys I will update the proposal when I can get to it, so subscribe there for further updates.
The final suggestions you have with both -XStringInterpolation and -XQualifiedLiterals sounds great to me and I love the overall design. I found the SQL example intuitive at a glance, even before I read any of the explanation for it:
Just a sidenote: the result also means that OsPath will not work with this, because it does not have an IsString instance: GitHub · Where software is built
It should work with the new QualifiedLiterals extension proposed in the results?
module System.OsPath.Interpolate where
fromParts :: [String] -> Maybe OsPath
fromParts = encodeUtf . concat
fromString :: String -> String
fromString = id
-- reusing String's Interpolate instances as an example;
-- the signature here could be anything
interpolate :: Interpolate a String => a -> String
interpolate = show
{-# LANGUAGE QualifiedLiterals #-}
import System.OsPath.Interpolate qualified as OsPath
main = do
let user = "testuser"
print OsPath."/home/${user}/foo.txt"
-- Just "/home/testuser/foo.txt"
Why does the QualifiedLiterals idea require qualifying things by modules? As far as I can tell something like foo"..." should work where foo is a record of the appropriate type. This is how Quasiquotes work
Now I come to think of it, why does QualifiedDo? It could also technically work as var.do, where var is a variable whose type is a record containing the desired Monad operations. Perhaps the parser would have a hard time, especially when OverloadedRecordDot is in play.
It removes a possibility: you can’t determine the interpolation function at run time. I’m not sure that feature would be used in practice, but it seems unfortunate to close it off unless absolutely necessary.
For my taste, using the module name would be more direct and understandable than trying to puzzle out the provenance of a value that guides interpolation. Worth the loss in flexibility, especially considering that the implementation would be simpler.
Also, IIUC, the interpolation should work for a potentially open set of value types, so I’m not sure how the record carrying the interpolation functions for each type would work.
And if I wanted to use -XQualifiedLiterals on its own as an alternative to -XOverloadedStrings (say, be able to write T."foo" to build a Text) the more flexible approach would be overkill.
You could get similar behavior with a custom interpolator
module MyInterpolate where
str1 :: [Either String Val] -> String
str2 :: [Either String Val] -> String
fromParts = id
fromString = Left
interpolate :: Val -> Either String Val
interpolate = Right
data Val = I Int | S String
import MyInterpolate qualified as I
main = do
let str = if ... then str1 else str2
putStrLn $ str $ I."..."
No , I don’t have a use case in mind, I just didn’t want to close off a possibility unnecessarily (Haskell has benefitted a lot historically from this kind of approach.)
Ah yes, OK, perhaps if you define an interpretation that’s “open” in some sense (or maybe “initial”?) then you can handle it however you want after the fact.
Ok, I hate to toss this in so late, but, the fromParts is essentially a Monoid like interface. So, it could be broken up into a mempty/<> pair. If we did this, we would also have the option of allowing the <> type to change the type of the result as we go (no longer a true monoid).
My motivation here is to allow for the types to change as we construct the value, which has the benefits of allowing some interesting patterns (e.g. returning functions).
A simple demonstration, I’ve called the functions %% and final, as well as introducing an extract function:
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs #-}
-- Data.Printf.Interpolate
-- defines `%%`, `extract` and `final`
import Data.Proxy
-- Represents a string literal, as opposed to a string that should be interpolated.
newtype Lit = Lit String
infixr 5 %%
class Interpolate a b c | a c -> b where
(%%) :: a -> b -> c
data Printf a where
PrintfEnd :: Printf String
PrintfLit :: String -> Printf a -> Printf a
PrintfArg :: Show b => Printf a -> Printf (b -> a)
final :: Printf String
final = PrintfEnd
instance Interpolate Lit (Printf a) (Printf a) where
Lit s %% p = PrintfLit s p
instance Interpolate String (Printf a) (Printf a) where
s %% p = PrintfLit s p
instance Interpolate (Proxy Int) (Printf a) (Printf (Int -> a)) where
Proxy %% p = PrintfArg p
instance Interpolate (Proxy Float) (Printf a) (Printf (Float -> a)) where
Proxy %% p = PrintfArg p
instance Interpolate Int (Printf a) (Printf a) where
x %% p = PrintfLit (show x) p
evalPrintf' :: Printf a -> (String -> String) -> a
evalPrintf' PrintfEnd f = f ""
evalPrintf' (PrintfLit s p) f = evalPrintf' p ((++s) . f)
evalPrintf' (PrintfArg p) f = \x -> evalPrintf' p ((++ show x) . f)
extract :: Printf a -> a
extract p = evalPrintf' p id
int :: Proxy Int
int = Proxy
float :: Proxy Float
float = Proxy
x :: Int
x = 3
y :: Int
y = 5
z :: Float
z = 3.2
-- Usage:
-- Printf."Example ${x} ${int} ${float}" y z
-- desugars to
res = extract (Lit "Example " %% x %% Lit " " %% int %% Lit " " %% float %% final) y z
-- "Example 3 5 3.2"
-- Or consider possibilities like
-- getNameForPerson :: PersonId -> IO String
-- getNameForPerson = SQL."select name from people where id is ${personid}"
Note how the Printf example above supports both interpolating the x into the string directly, as well as leaving other parameters to be supplied as arguments to the resulting function.
I haven’t really considered any performance or type-error ergonomics issues this may present yet.
Goodness… @brandonchinn178: sorry to be that person, but I have literally no idea what the conclusion of the results is: could you pretty please add a TL;DR at the top or in this thread at least?