这里有一个难题一直困扰着我,而且我相信之前没有任何SO问题涉及到:当我明确知道某些键存在于涉及的Map中时,如何最好地使用lens
库在管理嵌套数据结构的State
单子中设置或获取值?
{-# 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
这将生成一个聚焦于Maybe User
的Lens
。这意味着,后续的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’
尝试三:使用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’
尝试三b:与 at
的 Maybe
协作
让我们尝试另一种方式来处理那个 Maybe
balance <- use $ users . at uid . non undefined . balance
根据文档:
如果v是类型a的一个元素,且a'是不包含元素v的类型,则非v是从Maybe a'到a的同构。
我们可以使用undefined、error或任何“空”的User,因为在地图中存在用户ID的情况下,这种情况永远不会被触发。
为了使其正常工作,我们需要为User提供Eq。并且它已经编译和运行,也就是说,进行读取操作时可以正常工作。然而,对于写入操作,最初会出现意外的转折:
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
会掩盖这个错误。它始终用空用户替换现有用户,在尝试充值时实际上失去了用户的初始余额。
结论
因此,我已经找到了使阅读工作的选项,但没有一个是令人信服的。我无法弄清楚如何进行写作。
您有什么建议?该镜头应如何构建?
编辑:将解决方案移动到自我答案。
maybeToExceptT
? - n. m.justified-containers
吗? - Joseph Sible-Reinstate Monicausers . at uid . traversed . balance += b
这样的写法可行吗? - Daniel Wagner