這是我不斷遇到的一個難題,我相信,以前沒有任何 SO 問題已經解決:我如何最好地使用該lens庫在 monad 中設定或獲取值,該Statemonad 管理涉及Maps的嵌套資料結構,當我知道時因為某些鍵存在于所涉及的地圖中?
這是謎題
{-# 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
這會產生 a 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
There is no good way to implement <> for Balance. I could just implement addition, or use error, because, in fact, this function will never be called. But is this the cleanest way to do this?
Attempt 2: using at
Another option seems to be using at.
balance <- use $ users . at uid . balance
This yields a Lens that focuses on a Maybe User. This means, the follow-up lens balance has the wrong type.
? 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’
Attempt 3: working with at's Maybe
Let's try to work with that Maybe
balance <- use $ users . at uid . _Just . balance
This time, we have a Prism, which needs to deal with the situation when it has to work with Nothing. So we are back at requiring a 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’
Attempt 3b: working with at's Maybe
Let's try another way to work with that Maybe
balance <- use $ users . at uid . non undefined . balance
From the documentation:
If v is an element of a type a, and a' is a sans the element v, then non v is an isomorphism from Maybe a' to a.
We can use undefined, error or any "empty" User we want, it does not matter, since this case is never triggered, given the user id is present in the map.
For this to work we need Eq for User, which is fair enough. And it compiles and seems to work; that is, for reading. For writing, there is an, initially, unexpected twist:
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
Running this blows up
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
The explanation is that when writing, we use the optics "right-to-left", and in this direction non is injecting the User we provide as its parameter. Replacing undefined with a "empty" User obscures this mistake. It always replaces the existing user with the empty user, effectively loosing the user's initial balance when trying to top-up.
Conclusion
So, I have found options to make this work for reading, but none seems to be convincing. And I could not figure this out for writing.
What is your recommendation? How should that lens be constructed?
Edit: moved solution to self answer.
uj5u.com熱心網友回復:
如果您確定密鑰存在,那么您可以使用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
然后,每次您想查找用戶時,只需呼叫其中一個。這樣您就不必在函式的開頭進行單獨的檢查。
uj5u.com熱心網友回復:
執行此操作的最直接工具lens是不安全操作,將??您“知道”的遍歷將僅針對一個元素視為鏡頭。正如您可能知道的那樣,有一個運算子可以作為^.or的變體^?:
s <- get
let user = s ^?! users . at uid
但是對于view(within a Reader) 或use(within a 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) . balance
uj5u.com熱心網友回復:
有:
balance <- use $ users . at uid . to fromJust . balance閱讀(見這個答案)users . at uid . traversed . balance = b寫作(見此評論)
這個鏡頭適用于閱讀和寫作:
users . unsafeSingular (ix uid) . balance(見這個答案)
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/427273.html
上一篇:在下一步中使用結果流式傳輸管道
