[英]Resolving an ambiguous type variable
我有這兩個功能:
load :: Asset a => Reference -> IO (Maybe a)
send :: Asset a => a -> IO ()
Asset類看起來像這樣:
class (Typeable a,ToJSON a, FromJSON a) => Asset a where
ref :: a -> Reference
...
第一個從磁盤讀取資產,第二個將JSON表示傳輸到WebSocket。 在隔離他們的工作很好,但是當我將它們合並,編譯器無法推斷出具體類型a
應。 ( Could not deduce (Asset a0) arising from a use of 'load'
)
這是有道理的,我沒有給出具體類型, load
和send
都是多態的。 不知何故,編譯器必須決定使用哪個版本的send
(以及擴展名為toJSON
版本)。
我可以在運行時確定a
的具體類型是什么。 這些信息實際上是在磁盤上的數據和Reference
類型中編碼的,但是我不確定在編譯時是否正在運行類型檢查器。
有沒有辦法在運行時傳遞正確的類型仍然保持類型檢查器快樂?
附加信息
參考文獻的定義
data Reference = Ref {
assetType:: String
, assetIndex :: Int
} deriving (Eq, Ord, Show, Generic)
通過解析來自WebSocket的請求來推導引用,如下所示,其中Parser來自Parsec庫。
reference :: Parser Reference
reference = do
t <- string "User"
<|> string "Port"
<|> string "Model"
<|> ...
char '-'
i <- int
return Ref {assetType = t, assetIndex =i}
如果我向Reference
I添加了一個類型參數,只需將我的問題推回到解析器中。 我仍然需要在編譯時將我不知道的字符串轉換為類型以使其工作。
您不能創建一個函數將字符串數據轉換為不同類型的值,具體取決於字符串中的內容。 那根本不可能。 您需要重新排列事物,以便返回類型不依賴於字符串內容。
您的load
類型, Asset a => Reference -> IO (Maybe a)
說“選擇任何a
( Asset a
)你喜歡並給我一個Reference
,我會給你一個產生Maybe a
的IO
動作” 。 調用者選擇他們希望由引用加載的類型; 文件的內容不會影響加載的類型。 但是你不希望調用者選擇它,你希望它是通過存儲在磁盤上的內容來選擇的,所以類型簽名根本不能表達你真正想要的操作。 那是你真正的問題; 如果load
和send
單獨正確並且組合它們是唯一的問題,那么當組合load
和send
時,模糊類型變量將很容易解決(使用類型簽名或TypeApplications
)。
基本上你不能只是讓load
返回一個多態類型,因為如果它有,那么調用者會(必須)決定它返回什么類型。 有兩種方法可以避免這種情況或多或少的等價:返回一個存在的包裝器,或者使用rank 2類型並添加一個多態處理函數(continuation)作為參數。
使用存在包裝器(需要GADTs
擴展),它看起來像這樣:
data SomeAsset
where Some :: Asset a => a -> SomeAsset
load :: Reference -> IO (Maybe SomeAsset)
注意load
不再是多態的。 你得到一個SomeAsset
(就類型檢查而言)可以包含任何具有Asset
實例的類型。 load
可以在內部使用它想要拆分成多個分支的邏輯,並在不同的分支上提供不同類型資產的值; 如果每個分支都以SomeAsset
構造函數結束資產值,則所有分支都將返回相同的類型。
要send
它,你會使用類似的東西(忽略我沒有處理Nothing
):
loadAndSend :: Reference -> IO ()
loadAndSend ref
= do Just someAsset <- load ref
case someAsset
of SomeAsset asset -> send asset
SomeAsset
包裝器保證Asset
保持其包裝值,因此您可以解包它們並在結果上調用任何Asset
-polymorphic函數。 但是你永遠不能對任何其他方式依賴於特定類型的值做任何事情1 ,這就是為什么你必須始終保持它包裝和case
匹配的原因; 如果case
表達式導致一個依賴於包含類型的類型(例如case someAsset of SomeAsset a -> a
),則編譯器將不接受您的代碼。
另一種方法是使用RankNTypes
並為load
類型:
load :: (forall a. Asset a => a -> r) -> Reference -> IO (Maybe r)
這里load
根本不返回表示已加載資產的值。 它的作用是將多態函數作為參數; 該函數適用於任何Asset
並返回一個類型r
(由load
的調用者選擇),因此再次load
可以在內部分支但是它想要並在不同的分支中構造不同類型的資產。 不同的資產類型都可以傳遞給處理程序,因此可以在每個分支中調用處理程序。
我的偏好通常是使用SomeAsset
方法,但后來也使用RankNTypes
並定義一個輔助函數,如:
withSomeAsset :: (forall a. Asset a => a -> r) -> (SomeAsset -> r)
withSomeAsset f (SomeAsset a) = f a
這避免了必須將代碼重構為延續傳遞樣式,但是在需要使用SomeAsset
任何地方都會SomeAsset
case
語法:
loadAndSend :: Reference -> IO ()
loadAndSend ref
= do Just asset <- load ref
withSomeAsset send asset
或者甚至添加:
sendSome = withSomeAsset send
Daniel Wagner建議將類型參數添加到Reference
,OP通過聲明只是在構造引用時移動相同的問題來反對。 如果引用包含表示它們所引用的資產類型的數據,那么我強烈建議采用Daniel的建議,並使用本答案中描述的概念在參考構建級別解決該問題。 Reference
具有類型參數防止在那里你知道類型混合到錯誤類型的資產的引用。
如果你使用相同類型的引用和資源進行重要的處理,那么在你的主力代碼中使用type參數可以捕獲混淆它們的容易錯誤, 即使你通常存在外部代碼類型的類型。
1從技術上講,您的Asset
意味着可Typeable
,因此您可以針對特定類型對其進行測試,然后將其返回。
當然,讓Reference
存儲類型。
data Reference a where
UserRef :: Int -> Reference User
PortRef :: Int -> Reference Port
ModelRef :: Int -> Reference Model
load :: Asset a => Reference a -> IO (Maybe a)
send :: Asset a => a -> IO ()
如有必要,您仍然可以通過存在拳擊來恢復原始Reference
類型的強點。
data SomeAsset f where SomeAsset :: Asset a => f a -> SomeAsset f
reference :: Parser (SomeAsset Reference)
reference = asum
[ string "User" *> go UserRef
, string "Port" *> go PortRef
, string "Model" *> go ModelRef
]
where
go :: Asset a => (Int -> Parser (Reference a)) -> Parser (SomeAsset Reference)
go constructor = constructor <$ char '-' <*> int
loadAndSend :: SomeAsset Reference -> IO ()
loadAndSend (SomeAsset reference) = load reference >>= traverse_ send
在回顧了Daniel Wagner和Ben的答案之后,我最終使用我放在這里的兩個組合來解決我的問題,希望它能幫助其他人。
首先,根據Daniel Wagner的回答,我在Reference
添加了一個幻像類型:
data Reference a = Ref {
assetType:: String
, assetIndex :: Int
} deriving (Eq, Ord, Show, Generic)
我選擇不使用GADT構造函數並將字符串引用保留為assetType
因為我經常通過網絡發送引用和/或從傳入文本中解析它們。 我覺得有太多代碼點需要通用引用。 對於那些情況,我用Void
填充幻像類型:
{-# LANGUAGE EmptyDataDecls #-}
data Void
-- make this reference Generic
voidRef :: Reference a -> Reference Void
castRef :: a -> Reference b -> Reference a
-- ^^^ Note this can be undefined used only for its type
這樣, load
類型簽名變為load :: Asset a => Reference a -> IO (Maybe a)
因此Asset始終與Reference的類型匹配。 (Yay型安全!)
這仍然沒有解決如何加載通用引用。 對於那些情況,我使用Ben的答案的后半部分編寫了一些新代碼。 通過將資產包裝在SomeAsset
,我可以返回一個使類型檢查器滿意的類型。
{-# LANGUAGE GADTs #-}
import Data.Aeson (encode)
loadGenericAsset :: Reference Void -> IO SomeAsset
loadGenericAsset ref =
case assetType ref of
"User" -> Some <$> load (castRef (undefined :: User) ref)
"Port" -> Some <$> load (castRef (undefined :: Port) ref)
[etc...]
send :: SomeAsset -> IO ()
send (Some a) = writeToUser (encode a)
data SomeAsset where
Some :: Asset a => a -> SomeAsset
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.