Reputation: 6793
If I have lenses for a nested record, where each lens returns a Maybe
, how can I get them to compose, so that if anything in the "traversal" returns a Nothing
the final result is a Nothing
?
data Client = Client
{
clientProperties :: Maybe Properties
, ...
}
data Properties = Properties
{
propSmtpConfig :: Maybe SmtpConfig
, ...
}
c :: Client
c = undefined
smtp = c ^. (properties . smtpConfig) -- How to make these lenses compose?
Edit I tried a lot of options, but this is the best I could come up with. Looking for something cleaner:
(client ^. properties) >>= (view smtpConfig)
Upvotes: 6
Views: 2349
Reputation: 2317
client ^? properties . _Just . smtpConfig . _Just
Edit: Optics like lenses turn a small action into a big action. Optic composition is function composition. _Just
turns an action on a
into an action on Maybe a
. Lenses can turn small reading actions into large reading actions and small writing actions into large writing actions, but _Just
can't process reading actions because in a Nothing
there is no a
. Therefore _Just
is a weaker optic than a lens, a traversal. A composed optic can always work with exactly those actions which all parts can work with. Therefore properties . _Just . smtpConfig . _Just
is a traversal. (^?)
uses a variation on reading actions which a traversal can work with: "Maybe read a value". Therefore the above line turns the trivially successful reading action Just :: SmtpConfig -> Maybe SmtpConfig
into a large reading action Client -> Maybe SmtpConfig
.
Upvotes: 3
Reputation: 44634
traverse
is a valid Traversal
, remember.
getSmtpConfig :: Traversal' Client SmtpConfig
getSmtpConfig = properties . traverse . smtpConfig . traverse
A Traversal
is the best you can do here - you can't get a Lens
- because there may not be an SmtpConfig
. (A Lens
says "there's always exactly one of these things", whereas a Traversal
says "there may be zero or many".)
This code actually produces the same Traversal
as if you'd used the _Just
prism, but it's perhaps a little easier to understand if you haven't grokked prisms yet.
Note that since a Traversal
might not find any results, you can't use ^.
to access a single result as you did in your question. You need to use the "safe head" operator ^?
(aka flip preview
).
smtp :: Maybe SmtpConfig
smtp = c^?properties.traverse.smtpConfig.traverse
Upvotes: 4
Reputation: 15605
You can use the _Just
prism. Here's a contrived example:
> (Just (Just 1, ()), ()) & _1 . _Just . _1 . _Just +~ 1
(Just (Just 2,()),())
In your case, I think you want
properties . _Just . smtpConfig . _Just
Upvotes: 11