下面是我一直遇到的一个难题,我相信,以前没有这样的问题:我如何最好地使用lens库在管理涉及Maps 的嵌套数据结构的State monad中设置或获取值,因为我知道在涉及的映射中存在某些键。
这是一个谜题
{-# LANGUAGE TemplateHaskell, DerivingVia #-}
import Control.Monad.State
import Control.Monad.Except
import Control.Lens
import Data.Maybe
import Control.Monad
import Data.Map
type M = StateT World (ExceptT String Identity)
data World = World
{ _users :: Map UserId User
, _otherStuff :: Int
}
type UserId = Int
data User = User
{ _balance :: Balance
, _moreStuff :: Int
}
newtype Balance = Balance Int
deriving (Eq, Ord, Num) via Int
makeLenses 'World
makeLenses 'User
deleteUser :: UserId -> M ()
deleteUser uid = do
user <- use $ users . at uid
unless (isJust user) (throwError "unknown user")
-- from here on we know the users exists.
-- Question: how should the following lens look like?
balance <- use $ users . ix uid . balance
when (balance < 0) (throwError "you first have to settle your debt")
when (balance > 0) (throwError "you first have to withdraw the remaining balance")
users . at uid .= Nothing尝试1:使用ix
上面的片段使用ix。
balance <- use $ users . ix uid . balance这会产生一个Traversal,因此它可能关注多个元素,或者根本没有。在use上下文中,这意味着我们需要一个Monoid和一个Semigroup实例。事实上,这就是GHC要说的:
• No instance for (Monoid Balance) arising from a use of ‘ix’
• In the first argument of ‘(.)’, namely ‘ix uid’
In the second argument of ‘(.)’, namely ‘ix uid . balance’
In the second argument of ‘($)’, namely ‘users . ix uid . balance’
|
45 | balance <- use $ users . ix uid . balance没有一种为<>实现Balance的好方法。我可以实现加法,或者使用error,因为实际上,这个函数永远不会被调用。但这是最干净的方法吗?
尝试2:使用at
另一种选择似乎是使用at。
balance <- use $ users . at uid . balance这将产生一个专注于一个Lens的Maybe User。这意味着,后续镜头balance有错误的类型.
• Couldn't match type ‘User’
with ‘Maybe (IxValue (Map UserId User))’
Expected type: (User -> Const Balance User)
-> Map UserId User -> Const Balance (Map UserId User)
Actual type: (Maybe (IxValue (Map UserId User))
-> Const Balance (Maybe (IxValue (Map UserId User))))
-> Map UserId User -> Const Balance (Map UserId User)
• In the first argument of ‘(.)’, namely ‘at uid’
In the second argument of ‘(.)’, namely ‘at uid . balance’
In the second argument of ‘($)’, namely ‘users . at uid . balance’尝试3:使用at的Maybe
让我们试着用那个Maybe
balance <- use $ users . at uid . _Just . balance这一次,我们有一个Prism,当它必须使用Nothing时,它需要处理这种情况。因此,我们又开始要求一个Monoid。
• No instance for (Monoid Balance) arising from a use of ‘_Just’
• In the first argument of ‘(.)’, namely ‘_Just’
In the second argument of ‘(.)’, namely ‘_Just . balance’
In the second argument of ‘(.)’, namely ‘at uid . _Just . balance’尝试3b:使用at的Maybe
让我们尝试另一种方法来处理那个Maybe
balance <- use $ users . at uid . non undefined . balance从文件中:
如果v是a型的一个元素,a‘是一个sans的元素v,那么非v是一个可能从a’到a的同构。
我们可以使用undefined、error或任何我们想要的“空”User,这并不重要,因为在地图中有用户id时,这种情况永远不会触发。
为此,我们需要Eq For User,这是相当公平的。它编译并似乎起作用,也就是说,用于阅读。对于写作来说,最初有一个意想不到的转折:
topUp :: UserId -> Balance -> M ()
topUp uid b = do
user <- use $ users . at uid
unless (isJust user) (throwError "unknown user")
users . at uid . non undefined . balance += b运行这个会爆炸
experiment-exe: Prelude.undefined
CallStack (from HasCallStack):
error, called at libraries/base/GHC/Err.hs:79:14 in base:GHC.Err
undefined, called at app/Main.hs:55:25 in main:Main解释是,在写作时,我们使用光学“从右到左”,而在这个方向上,non是注入我们提供的User作为它的参数。将undefined替换为“空”User会掩盖这一错误。它总是用空的用户替换现有的用户,在尝试升级时有效地放松了用户的初始平衡。
结论
因此,我已经找到了使这一工作的阅读,但似乎没有令人信服的选择。我也想不出这是为了写作。
你的建议是什么?那镜片该怎么做?
编辑:移动的解决方案自我回答。
发布于 2022-02-11 18:14:03
有:
balance <- use $ users . at uid . to fromJust . balance用于阅读(see this answer)users . at uid . traversed . balance += b用于编写(see this comment)这个镜头是用来读写的:
users . unsafeSingular (ix uid) . balance (see this answer)发布于 2022-02-09 09:27:00
如果确实存在密钥,则可以使用fromJust将Maybe User转换为User。
balance <- use $ users . at uid . to fromJust . balance尽管作为一个设计问题,我建议用抛出有意义的错误的函数替换use $ users . at uid ...:
getUser :: UserId -> M User对于使用方便的镜头:
getsUser :: UserId -> Getter User a -> M a然后,每次您想要查找用户时,只需调用其中之一即可。这样,您就不必在函数的开头单独检查。
发布于 2022-02-09 19:42:44
lens用于执行此操作的最直接工具是不安全的操作,这些操作处理您“知道”只针对一个元素的遍历,就好像它是一个透镜一样。正如您可能知道的,这里有一个操作符可以作为^.或^?的变体
s <- get
let user = s ^?! users . at uid但是对于view (在Reader中)或use (在State中),似乎没有任何内置的变体。不过,您可以使用unsafeSingular函数在Control.Lens.Traversal中编写自己的代码:
use1 :: MonadState s m => Traversal' s a -> m a
use1 = use . unsafeSingular
view1 :: MonadReader s m => Traversal' s a -> m a
view1 = view . unsafeSingular之后:
balance <- use1 $ users . ix uid . balance应该行得通。
如果你宁愿让光学本身不安全,而不安全地使用一个安全的光学,你可以直接使用unsafeSingular来修改光学。例如:
balance <- use $ users . unsafeSingular (ix uid) . balancehttps://stackoverflow.com/questions/71046129
复制相似问题