我进行了一次计算,将值插入Map
中,然后再次查找它们。我知道我从来没有在插入密钥之前使用过密钥,但是无论如何随意使用(!)
会让我感到紧张。我正在寻找一种获取不返回Maybe
且类型系统可以防止我意外滥用的总查询功能的方法。
我的第一个想法是制作一个类似于StateT
的monad转换器,其中状态为Map
,并且在monad中有用于插入和查找的特殊功能。 insert函数返回一个Receipt s k
新类型,其中s
是一种ST
monad样式的幻像索引类型,而k
是键的类型,并进行查找函数使用Receipt
而不是裸键。通过隐藏Receipt
构造函数并使用类似于runST
的量化运行函数,这应确保仅在插入同一映射后才进行查找。 (完整代码如下。)
但是我担心我已经重新发明了轮子,或者担心还有一种替代的方法来获取已经使用的安全的总地图查找。在某个地方的公共包装中是否存在针对此问题的现有技术?
{-# LANGUAGE DeriveFunctor, LambdaCase, RankNTypes #-}
module KeyedStateT (KeyedStateT, Receipt, insert, lookup, receiptToKey, runKeyedStateT)
where
import Prelude hiding (lookup)
import Control.Arrow ((&&&))
import Control.Monad (ap, (>=>))
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (fromJust)
newtype KeyedStateT s k v m a = KeyedStateT (Map k v -> m (a, Map k v)) deriving Functor
keyedState :: Applicative m => (Map k v -> (a, Map k v)) -> KeyedStateT s k v m a
keyedState f = KeyedStateT (pure . f)
instance Monad m => Applicative (KeyedStateT s k v m) where
pure = keyedState . (,)
(<*>) = ap
instance Monad m => Monad (KeyedStateT s k v m) where
KeyedStateT m >>= f = KeyedStateT $ m >=> uncurry ((\(KeyedStateT m') -> m') . f)
newtype Receipt s k = Receipt { receiptToKey :: k }
insert :: (Applicative m, Ord k) => k -> v -> KeyedStateT s k v m (Receipt s k)
insert k v = keyedState $ const (Receipt k) &&& Map.insert k v
lookup :: (Applicative m, Ord k) => Receipt s k -> KeyedStateT s k v m v
lookup (Receipt k) = keyedState $ (Map.! k) &&& id
runKeyedStateT :: (forall s. KeyedStateT s k v m a) -> m (a, Map k v)
runKeyedStateT (KeyedStateT m) = m Map.empty
module Main where
import Data.Functor.Identity (runIdentity)
import qualified KeyedStateT as KS
main = putStrLn . fst . runIdentity $ KS.runKeyedStateT $ do
one <- KS.insert 1 "hello"
two <- KS.insert 2 " world"
h <- KS.lookup one
w <- KS.lookup two
pure $ h ++ w
编辑:一些评论者问我为什么要保留Receipt
而不是实际值。我希望能够在Receipt
s和Set
s中使用Map
(我没有为{{1}添加Eq
和Ord
实例}},但是我的Receipt
中的值不能相等。如果我用键值对newtype替换了Map
,我将不得不为那个忽略值的对实现一个不诚实的Receipt
实例,然后我对此会感到不安。 Eq
可以确保在任何给定时间,我的任何等价“代理”键都只考虑一个值。
我想一个对我来说很好用的替代解决方案是monad变压器,它提供Map
s,其中Ref
的电源,而monad确保data Ref v = Ref Int v
s仅使用Ref
给出了唯一的Int
ID和Eq Ref
等ID(现在,Int
的唯一性保证了诚实性)。我也会在野外接受指向这种转换器的指针。
答案 0 :(得分:3)
您的解决方案类似于justified-containers使用的技术,以确保键在地图中存在。但是有一些区别:
justified-containers使用延续传递样式。
inserting中的justified-containers中的新密钥要求“回收”现有收据才能在更新的地图中工作。您似乎不需要“循环使用”收据,因为您永远不会同时拥有多个版本的地图。
在功能性珍珠justified-containers中可以找到"Ghosts of departed proofs"使用的技术的扩展说明。