Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I get compile-time validation of literals in IsString instances?

Tags:

haskell

ghc

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.

like image 753
jml Avatar asked Jan 02 '17 21:01

jml


1 Answers

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)!

like image 100
Alec Avatar answered Nov 10 '22 19:11

Alec