簡體   English   中英

解決模糊類型變量

[英]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'

這是有道理的,我沒有給出具體類型, loadsend都是多態的。 不知何故,編譯器必須決定使用哪個版本的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)說“選擇任何aAsset a )你喜歡並給我一個Reference ,我會給你一個產生Maybe aIO動作” 。 調用者選擇他們希望由引用加載的類型; 文件的內容不會影響加載的類型。 但是你不希望調用者選擇它,你希望它是通過存儲在磁盤上的內容來選擇的,所以類型簽名根本不能表達你真正想要的操作。 那是你真正的問題; 如果loadsend單獨正確並且組合它們是唯一的問題,那么當組合loadsend時,模糊類型變量將很容易解決(使用類型簽名或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 WagnerBen的答案之后,我最終使用我放在這里的兩個組合來解決我的問題,希望它能幫助其他人。

首先,根據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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM