Saurabh Nanda
Saurabh Nanda

Reputation: 6793

How to compose "Maybe" lenses?

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

Answers (3)

Gurkenglas
Gurkenglas

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

Benjamin Hodgson
Benjamin Hodgson

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

Rein Henrichs
Rein Henrichs

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

Related Questions