I would like to be able to make IsString
instances using the GHC OverloadedStrings
extension such that my instance rejects some literals as being invalid and such that that rejection happens at compile time, so that programming mistakes don't make it into the code I give to my users.
I have a couple of use cases where I have a Name
type that only admits certain strings. e.g.
module Name (Name(getName), makeName) where
import Data.Text (Text)
import qualified Data.Text as Text
-- | A guaranteed non-empty name.
newtype Name = Name { getName :: Text } deriving (Eq, Show, Ord)
makeName :: Text -> Maybe Name
makeName name
| Text.null name = Nothing
| otherwise = Just name
In a real use case, I'd check for valid characters, not starting with a digit, that sort of thing.
The idea is that we don't export the Name
constructor, which means anyone using a Name
value can trust that it has certain properties (in this case, non-empty).
My problem is that I'd like to use literal names in many places. e.g.
programName :: Name
programName = fromJust $ makeName "the-great-and-powerful-turtle"
Because I do this a lot, I've defined an unsafeMakeName
helper that does pretty much the same thing:
unsafeMakeName :: Text -> Name
unsafeMakeName name = fromMaybe (error $ "Invalid name: " <> Text.unpack name) (makeName name)
The problem with this approach is that even though the cause of the mistake is a programming error, I don't find out about it until run time.
What I'd like to do is write an IsString
instance for Name
which does that validation, e.g.
instance IsString Name where
fromString = unsafeMakeName . Text.pack
... but to get the error about invalid names in literals at compile-time.
When I try this, I only seem to get the errors at run-time, when the literal value is used. This is less than ideal, since it's an error in my actual code.
Is there any way I can do this? Is this something that could be remedied in GHC? Note that I've already filed a bug there.
It really sounds like what you want is a quasiquoter and not OverloadedStrings
. The validation logic then goes inside the Q
monad, which runs at compile time. For your simple example above:
{-# LANGUAGE QuasiQuotes, TemplateHaskell #-}
module Name (Name(getName), name) where
import Data.Text (Text)
import qualified Data.Text as Text
import Language.Haskell.TH.Quote hiding (Name)
import Language.Haskell.TH hiding (Name)
-- | A guaranteed non-empty name.
newtype Name = Name { getName :: Text } deriving (Eq, Show, Ord)
makeName :: String -> Q Exp
makeName name
| null name = fail "Invalid name"
| otherwise = [| Name (Text.pack name) |]
name :: QuasiQuoter
name = QuasiQuoter { quoteExp = makeName }
Then, in another module, the following compiles:
{-# LANGUAGE QuasiQuotes #-}
import Name
main = print [name|valid-name|]
But the following doesn't, and spits out the Invalid name
error message.
{-# LANGUAGE QuasiQuotes #-}
import Name
main = print [name||]
Note that you can get quasiquoters that work for patterns too (so something like myFunc [name|valid-name|] = True
could be a valid function definition)!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With