GHC.TypeLits 的 ScopedTypeVariables 和 RankNTypes

ScopedTypeVariables and RankNTypes with GHC.TypeLits

我正在尝试使用 DataKinds 和类型级文字来制作类型安全的货币转换库。到目前为止,我已经定义了这些数据类型:

data Currency (s :: Symbol) = Currency Double
    deriving Show

type USD = Currency "usd"
type GBP = Currency "gbp"

usd :: Double -> USD
usd = Currency

gbp :: Double -> GBP
gbp = Currency

data SProxy (s :: Symbol) = SProxy

以及允许我在它们之间进行转换的函数:

convert :: forall a b. (KnownSymbol a, KnownSymbol b) => Currency a -> Currency b
convert (Currency a) = case (symbolVal (SProxy :: SProxy a), 
                             symbolVal (SProxy :: SProxy b)) of
          ("usd", "gbp") -> Currency (a * 0.75)
          ("gbp", "usd") -> Currency (a * 1.33)

在这里,我使用 ScopedTypeVariables 将约束条件 KnownSymbol a 提供给 symbolVal SProxy。这工作正常,但是,我希望能够从外部源更新转换率,可能是文本文件或 API,例如 fixer

显然,我可以将 return 类型包裹在 IO 中,形成

convert :: forall a b. (KnownSymbol a, KnownSymbol b) => Currency a -> IO (Currency b)

但我希望能够保持纯洁API。我的第一个想法是使用 unsafePerformIO 获得转换率图,但这是不安全的,所以我认为我可以使用另一个函数 getConvert,其类型为

getConvert :: IO (forall a b. (KnownSymbol a, KnownSymbol b) => Currency a -> Currency b)

(即一个 IO 操作 returning 一个 convert 类型的函数)以便它可以像这样使用:

do
    convert <- getConvert
    print $ convert (gbp 10) :: USD

但是,我无法对其进行类型检查 - GHC 抱怨说:

Couldn't match expected type ‘forall (a :: Symbol) (b :: Symbol). 
                              (KnownSymbol a, KnownSymbol b) => 
                              Currency a -> Currency b’ 
with actual type ‘Currency a0 -> Currency b0’

当我让 GHC 推断出 return convert 的类型时,它并没有推断出我想要的类型,而是将 forall a b 移到了prenex 位置,进行了类型检查,直到我尝试使用convert' <- getConvert,此时它没有说有 No instance for (KnownSymbol n0)

我的问题是为什么不进行类型检查,函数的正确类型是什么 getConvert?

首先我认为这可能是 ScopedTypeVariablesRankNTypes 以不同方式使用 forall 量词的事实,但切换 RankNTypes 没有效果。我也尝试按照 GHC 的建议将量词移到前面,但这并没有给我我需要的 rank-2 类型。

ImpredicativeTypes 不要 不能 真的行得通;避开他们。您可以将汇率转换 table 包装在保留其多态类型的 data 类型中,而不是使用 IO (forall a. b. ...)

data ExchangeRates = ExchangeRates {
    getER :: forall a b. (KnownSymbol a, KnownSymbol b) => Currency a -> Currency b
}

和 return 一个 IO ExchangeRates 而不是

-- getConvert :: IO (forall a b. (KnownSymbol a, KnownSymbol b) => Currency a -> Currency b)
getConvert    :: IO ExchangeRates
getConvert = return (ExchangeRates convert)

并且几乎按照您的预期使用它。请注意括号将 :: USD 类型签名与转换后的值分组。

main = do
    convert <- getER <$> getConvert
    print $ (convert (gbp 10) :: USD)