4

I'm currently trying to make parts of my code more concise using lenses. In particular, I have a HTTP Request where I want to replace the value of a header with the name Private-Header.

I managed to write the function that updates the RequestHeaders:

updateHeaders :: RequestHeaders -> RequestHeaders
updateHeaders headers = headers & traverse . filtered (\header -> fst header == "Private-Header") %~ set _2 "xxxxxx"

However, I'm struggling with coming up with a function that extracts the headers from a requests and updates them. Without lenses, it could look like this:

updateRequest :: Request -> Request
updateRequest req = req {requestHeaders = updateHeaders (requestHeaders req)}

Is there a way to implement this function using lenses?

l7r7
  • 1,134
  • 1
  • 7
  • 23

1 Answers1

4

Certainly. First, you need an optic that represents the value of the "Private-Header" header within a RequestHeaders object. A reasonable candidate is a traversal, which allows zero or more occurrences of one type within another. (Typically, you'd only have zero or one private headers, but there's nothing fundamental about the RequestHeader type that prevents two or more headers with the same name, so a traversal seems the safest bet.)

The appropriate type for this optic is:

privateHeader :: Traversal' RequestHeaders ByteString

You've already done most of the work for defining this optic in updateHeaders, you just need to rearrange the parts. The expression:

traverse . filtered (\header -> fst header == "Private-Header")

is an optic that pulls out matching Header values from the RequestHeader. It's a valid traversal as long as you don't use it to modify the keys and break the filtering, so we can compose it directly with the lens _2 to create a new traversal that extracts the header values from type Header = (ByteString, ByteString):

privateHeader = traverse . filtered (\header -> fst header == "Private-Header") . _2

By the way, this new traversal allows us to simplify the implementation of updateHeaders, too.

updateHeaders :: RequestHeaders -> RequestHeaders
updateHeaders = set privateHeader "xxxxxx"

Second, we need an optic that represents the value of the RequestHeaders fields of a Request. You can build one with the lens function:

headers :: Lens' Request RequestHeaders
headers = lens getter setter
  where getter = requestHeaders
        setter req hdrs = req { requestHeaders = hdrs }

Now, you can compose headers and privateHeaders to create a new traversal:

privateHeaderInRequest :: Traversal' Request ByteString
privateHeaderInRequest = headers . privateHeader

and updateRequest can be implemented as:

updateRequest :: Request -> Request
updateRequest = set (headers . privateHeader) "xxxxxx"

Full code:

{-# LANGUAGE OverloadedStrings #-}

import Control.Lens
import Network.HTTP.Client
import Network.HTTP.Types
import Data.ByteString (ByteString)

privateHeader :: Traversal' RequestHeaders ByteString
privateHeader = traverse . filtered (\header -> fst header == "Private-Header") . _2

updateHeaders :: RequestHeaders -> RequestHeaders
updateHeaders = set privateHeader "xxxxxx"

headers :: Lens' Request RequestHeaders
headers = lens getter setter
  where getter = requestHeaders
        setter req hdrs = req { requestHeaders = hdrs }

updateRequest :: Request -> Request
updateRequest = set (headers . privateHeader) "xxxxxx"

main = do
  request <- parseRequest "http://localhost:8888/"
  -- could use "headers" lens to set this, but let's do it manually
  -- for clarity...
  let request' = request { requestHeaders = [("Private-Header","hello"),
                                             ("Other-Header","goodbye")] }
  print $ requestHeaders (updateRequest request')
K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • Ok so the main point I missed was that I construct a single optic that points to the field I want to update and in the end I call `set` on it once. Btw, the header names are case insensitive, so it automatically does a case-insensitive match – l7r7 Aug 25 '20 at 06:25
  • Oh, you're right. I've deleted the comment about doing a case insensitive match. – K. A. Buhr Aug 25 '20 at 13:59