首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >镜头,州单,和地图与已知的钥匙

镜头,州单,和地图与已知的钥匙
EN

Stack Overflow用户
提问于 2022-02-09 08:23:52
回答 3查看 186关注 0票数 4

下面是我一直遇到的一个难题,我相信,以前没有这样的问题:我如何最好地使用lens库在管理涉及Maps 的嵌套数据结构的State monad中设置或获取值,因为我知道在涉及的映射中存在某些键。

这是一个谜题

代码语言:javascript
复制
{-# 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

代码语言:javascript
复制
   balance <- use $ users . ix uid . balance

这会产生一个Traversal,因此它可能关注多个元素,或者根本没有。在use上下文中,这意味着我们需要一个Monoid和一个Semigroup实例。事实上,这就是GHC要说的:

代码语言:javascript
复制
    • 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

代码语言:javascript
复制
   balance <- use $ users . at uid . balance

这将产生一个专注于一个LensMaybe User。这意味着,后续镜头balance有错误的类型.

代码语言:javascript
复制
    • 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:使用atMaybe

让我们试着用那个Maybe

代码语言:javascript
复制
   balance <- use $ users . at uid . _Just . balance

这一次,我们有一个Prism,当它必须使用Nothing时,它需要处理这种情况。因此,我们又开始要求一个Monoid

代码语言:javascript
复制
    • 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:使用atMaybe

让我们尝试另一种方法来处理那个Maybe

代码语言:javascript
复制
   balance <- use $ users . at uid . non undefined . balance

从文件中:

如果v是a型的一个元素,a‘是一个sans的元素v,那么非v是一个可能从a’到a的同构。

我们可以使用undefinederror或任何我们想要的“空”User,这并不重要,因为在地图中有用户id时,这种情况永远不会触发。

为此,我们需要Eq For User,这是相当公平的。它编译并似乎起作用,也就是说,用于阅读。对于写作来说,最初有一个意想不到的转折:

代码语言:javascript
复制
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

运行这个会爆炸

代码语言:javascript
复制
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会掩盖这一错误。它总是用空的用户替换现有的用户,在尝试升级时有效地放松了用户的初始平衡。

结论

因此,我已经找到了使这一工作的阅读,但似乎没有令人信服的选择。我也想不出这是为了写作。

你的建议是什么?那镜片该怎么做?

编辑:移动的解决方案自我回答。

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 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)

这个镜头是用来读写的:

票数 1
EN

Stack Overflow用户

发布于 2022-02-09 09:27:00

如果确实存在密钥,则可以使用fromJustMaybe User转换为User

代码语言:javascript
复制
balance <- use $ users . at uid . to fromJust . balance

尽管作为一个设计问题,我建议用抛出有意义的错误的函数替换use $ users . at uid ...

代码语言:javascript
复制
getUser :: UserId -> M User

对于使用方便的镜头:

代码语言:javascript
复制
getsUser :: UserId -> Getter User a -> M a

然后,每次您想要查找用户时,只需调用其中之一即可。这样,您就不必在函数的开头单独检查。

票数 3
EN

Stack Overflow用户

发布于 2022-02-09 19:42:44

lens用于执行此操作的最直接工具是不安全的操作,这些操作处理您“知道”只针对一个元素的遍历,就好像它是一个透镜一样。正如您可能知道的,这里有一个操作符可以作为^.^?的变体

代码语言:javascript
复制
s <- get
let user = s ^?! users . at uid

但是对于view (在Reader中)或use (在State中),似乎没有任何内置的变体。不过,您可以使用unsafeSingular函数在Control.Lens.Traversal中编写自己的代码:

代码语言:javascript
复制
use1 :: MonadState s m => Traversal' s a -> m a
use1 = use . unsafeSingular

view1 :: MonadReader s m => Traversal' s a -> m a
view1 = view . unsafeSingular

之后:

代码语言:javascript
复制
balance <- use1 $ users . ix uid . balance

应该行得通。

如果你宁愿让光学本身不安全,而不安全地使用一个安全的光学,你可以直接使用unsafeSingular来修改光学。例如:

代码语言:javascript
复制
balance <- use $ users . unsafeSingular (ix uid) . balance
票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/71046129

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档