静态强制两个对象是从同一个 (Int) 创建的 "seed"

Statically enforcing that two objects were created from the same (Int) "seed"

在我正在处理的库中,我有一个 API 类似于以下内容:

data Collection a = Collection Seed {-etc...-}
type Seed = Int

newCollection :: Seed -> IO (Collection a)
newCollection = undefined

insert :: a -> Collection a -> IO () -- ...and other mutable set-like functions
insert = undefined

mergeCollections :: Collection a -> Collection a -> IO (Collection a)
mergeCollections (Collection s0 {-etc...-}) (Collection s1 {-etc...-}) 
  | s0 /= s1  = error "This is invalid; how can we make it statically unreachable?"
  | otherwise = undefined

我希望能够强制用户不能在使用不同 Seed 值创建的 Collection 上调用 mergeCollections

我想尝试用类型级自然标记 Collection:我认为这意味着 Seed 必须在编译时静态已知,但我的用户可能是从环境变量或用户输入中获取它,所以我认为这行不通。

我也希望我可以做类似的事情:

newtype Seed u = Seed Int
newSeed :: Int -> Seed u
newCollection :: Seed u -> IO (Collection u a)
mergeCollections :: Collection u a -> Collection u a -> IO (Collection u a)

其中 Seed 以某种方式被标记为唯一类型,这样类型系统可以跟踪 merge 的两个参数都是从同一调用返回的种子创建的newSeed。在这个(手波浪)方案中要清楚 ab 这里会以某种方式不统一:let a = newSeed 1; b = newSeed 1;.

这样的事情可能吗?

例子

以下是我可以想象的用户创建 SeedCollection 的一些示例。用户希望像使用任何其他 IO 可变集合一样自由地使用其他操作(插入、合并等):

  1. 我们只需要一个种子用于 all Collections(动态)在程序期间创建,但用户必须能够指定某种方式应该如何在运行时从环境中确定种子。

  2. 从环境变量(或命令行参数)收集的一个或多个静态密钥:

    main = do
       s1 <- getEnv "SEED1"
       s2 <- getEnv "SEED2"
       -- ... many Collections may be created dynamically from these seeds
       -- and dynamically merged later
    

我认为这是不可能的,因为种子来自运行时值,例如用户输入。类型检查器作为一种工具,只有在我们能够在编译时确定程序无效时,才能拒绝无效程序。假设有一种类型使得类型检查器能够根据用户输入拒绝程序,我们可以推断出类型检查器正在进行某种时间旅行或者能够完全模拟我们的确定性宇宙。作为库作者,你能做的最好的事情就是将你的类型走私到类似 ExceptT 的东西中,它记录了种子前提条件并导出了它的意识。

可能不方便。要处理仅在运行时已知的种子,您可以使用存在类型;但是你不能静态地检查这些存在包装的集合中的两个是否匹配。更简单的解决方案就是:

merge :: Collection a -> Collection a -> IO (Maybe (Collection a))

另一方面,如果可以强制完成所有操作 "together",在某种意义上,那么您可以做一些类似于 ST monad 所做的事情:将所有一起操作,然后为 "running" 所有操作提供一个操作,只有当这些操作不通过要求它们在幻像变量上完全多态来泄漏集合时才有效,因此 return 类型不会提到幻影变量。 (Tikhon Jelvis 在他的评论中也提出了这一点。)这可能是这样的:

{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Collection (Collection, COp, newCollection, merge, inspect, runCOp) where

import Control.Monad.Reader

type Seed = Int
data Collection s a = Collection Seed
newtype COp s a = COp (Seed -> a) deriving (Functor, Applicative, Monad, MonadReader Seed)

newCollection :: COp s (Collection s a)
newCollection = Collection <$> ask

merge :: Collection s a -> Collection s a -> COp s (Collection s a)
merge l r = return (whatever l r) where
  whatever = const

-- just an example; substitute whatever functions you want to have for
-- consuming Collections
inspect :: Collection s a -> COp s Int
inspect (Collection seed) = return seed

runCOp :: (forall s. COp s a) -> Seed -> a
runCOp (COp f) = f

请特别注意 COpCollection 构造函数未导出。因此,我们永远不必担心 Collection 会逃脱其 COprunCOp newCollection 类型不正确(任何其他尝试 "leak" 集合到外界的操作都将具有相同的 属性)。因此,不可能将用一个种子构造的 Collection 传递给在另一个种子的上下文中运行的 merge