2017-01-02 82 views
11

我希望能夠使用GHC OverloadedStrings擴展來創建IsString實例,以便我的實例拒絕一些文字是無效的,並且使得拒絕發生在編譯時,因此編程錯誤不會將其編碼到代碼I中給我的用戶。如何獲得IsString實例中文字的編譯時驗證?

我有幾個使用案例,我有一個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) 

這種方法的問題是,即使錯誤的原因是一個編程錯誤,直到運行時我才發現它。

我想要做的是爲Name寫一個IsString實例,它會進行驗證,例如,

instance IsString Name where 
    fromString = unsafeMakeName . Text.pack 

...但在編譯時得到文字中無效名稱的錯誤。

當我嘗試這個,我似乎只是在運行時,當使用文字值時,得到錯誤。這不太理想,因爲這是我實際代碼中的錯誤。

有什麼辦法可以做到這一點? GHC可以解決這個問題嗎?請注意,我已經在那裏filed a bug

+2

我認爲做到這一點的唯一方法是使用模板Haskell通過編譯時拼接進行驗證。 –

+0

對於重量較重的物體,您可以嘗試使用液體哈斯克爾的總體檢查器。至少它會很有趣。 –

+0

這對於'printf'及其格式字符串來說是很好的例子。 – chi

回答

16

它真的聽起來像你想要的是quasiquoter而不是OverloadedStrings。驗證邏輯然後進入編譯時運行的Q monad中。爲了您上面簡單的例子:

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

請注意,你可以得到的模式工作太quasiquoters(所以像myFunc [name|valid-name|] = True可能是一個有效的函數定義)!