5

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)
Saurabh Nanda
  • 6,373
  • 5
  • 31
  • 60

3 Answers3

11

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
Rein Henrichs
  • 15,437
  • 1
  • 45
  • 55
4

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
Benjamin Hodgson
  • 42,952
  • 15
  • 108
  • 157
3
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.

Gurkenglas
  • 2,317
  • 9
  • 17