Как я могу получить проверку литералов в экземплярах IsString?

Я хотел бы иметь возможность делать экземпляры IsString с использованием расширения GHC OverloadedStrings, так что мой экземпляр отклоняет некоторые литералы как недопустимые и такие, что это отклонение происходит во время компиляции, так что ошибки программирования не делают это в код, который я предоставляю своим пользователям.

У меня есть несколько вариантов использования, когда у меня есть тип Name, который допускает только определенные строки. например.

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

В реальном варианте использования я бы проверял правильные символы, а не начинал с цифры, что-то типа.

Идея состоит в том, что мы не экспортируем конструктор Name, что означает, что любой, кто использует значение Name, может доверять тому, что он имеет определенные свойства (в данном случае непустые).

Моя проблема в том, что я бы хотел использовать литералы во многих местах. например.

programName :: Name
programName = fromJust $ makeName "the-great-and-powerful-turtle"

Поскольку я делаю это много, я определил помощника unsafeMakeName, который делает почти то же самое:

unsafeMakeName :: Text -> Name
unsafeMakeName name = fromMaybe (error $ "Invalid name: " <> Text.unpack name) (makeName name)

Проблема с этим подходом заключается в том, что, хотя причиной ошибки является ошибка программирования, я не узнаю об этом до времени выполнения.

Что бы я хотел сделать, это написать экземпляр IsString для Name, который выполняет эту проверку, например

instance IsString Name where
  fromString = unsafeMakeName . Text.pack

... но чтобы получить ошибку о недопустимых именах в литералах во время компиляции.

Когда я пытаюсь это сделать, я только, кажется, получаю ошибки во время выполнения, когда используется буквальное значение. Это меньше, чем идеально, поскольку это ошибка в моем фактическом коде.

Есть ли способ сделать это? Это что-то, что можно было бы исправить в GHC? Заметьте, что я уже подал там ошибку.

Ответ 1

Кажется, что вы хотите quasiquoter, а не OverloadedStrings. Затем логика проверки переходит внутрь монады Q, которая запускается во время компиляции. Для вашего простого примера выше:

{-# 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 }

Затем в другом модуле следующие компиляции:

{-# LANGUAGE QuasiQuotes #-}
import Name

main = print [name|valid-name|]

Но следующее не делает и выдает сообщение об ошибке Invalid name.

{-# LANGUAGE QuasiQuotes #-}
import Name

main = print [name||]

Обратите внимание, что вы можете получить квазикваторы, которые тоже работают для шаблонов (так что что-то вроде myFunc [name|valid-name|] = True может быть корректным определением функции)!