6

I want to use Kmett's lens library to access an element of a (key, value) list under a specific key. In other words, I would like to replace this code with something more idiomatic and perhaps shorter:

type Headers = [ (ByteString, ByteString) ]

headerLens :: 
  Functor f => 
  ByteString -> 
  (Maybe ByteString -> f (Maybe ByteString)) ->
  Headers ->
  f Headers
headerLens header_name f headers 
    | old_header_value <- fetchHeader headers header_name = fmap 
       (\ new_header_value -> 
              replaceHeaderValue 
              headers 
              header_name 
              new_header_value
       )
       (f old_header_value )

where the support functions are defined as below:

-- | Looks for a given header and returns the value, if any
fetchHeader :: Headers -> ByteString -> Maybe ByteString
fetchHeader headers header_name = 
    snd <$> find ( \ x -> fst x == header_name ) headers 

-- | replaceHeaderValue headers header_name maybe_header_value looks for 
--   header_name. If header_name is found and maybe_header_value is nothing, it 
--   returns a new headers list with the header deleted. If header_name is found
--   and header_value is Just new_value, it returns a new list with the header 
--   containing the new value. If header_name is not in headers and maybe_header_value
--   is Nothing, it returns the original headers list. If header_name is not in headers
--   and maybe_header_value is Just new_value, it returns a new list where the last element
--   is (header_name, new_value)
replaceHeaderValue :: Headers -> ByteString -> Maybe ByteString -> Headers 
replaceHeaderValue headers header_name maybe_header_value = 
    disect id headers
  where 
    disect builder [] = case maybe_header_value of 
        Nothing -> headers
        Just new_value -> builder $ (header_name, new_value):[]
    disect builder ( el@(hn,hv) : rest) 
        | hn /= header_name = 
            disect
                (\ constructed_list -> builder $ el:constructed_list )
                rest
        | otherwise = case maybe_header_value of 
            Nothing -> builder rest 
            Just new_value -> builder $ (hn, new_value):rest    
dsign
  • 12,340
  • 6
  • 59
  • 82

1 Answers1

4

Well, if you were to use Data.Map.Map as your structure instead (should be a pretty easy refactor) you won't have to replicate all this work yourself:

import qualified Data.Map as M
import Control.Lens

type Headers = M.Map ByteString ByteString

fetchHeader :: Headers -> ByteString -> Maybe ByteString
fetchHeader = flip M.lookup

replaceHeaderValue :: Headers -> ByteString -> Maybe ByteString -> Headers
replaceHeaderValue headers header_name maybe_header_value
    = M.alter (const maybe_header_value) header_name headers

Then you could keep your headerLens as it is. Or you could look into something like

headerLens name = lens (M.lookup name) (\hs mhv -> M.alter (const mhv) name hs)

Which doesn't need the supporting functions at all and can have a pretty generic signature for working with more types of Maps. Example usage:

> let hs = M.fromList [("foo", "bar")]
> hs ^. headerLens "foo"
Just "bar"
> hs ^. headerLens "baz"
Nothing
> headerLens "foo" .~ Just "baz" $ hs
fromList [("foo", "baz")]
> headerLens "foo" .~ Nothing $ hs
fromList []
> headerLens "qux" .~ Just "baz" $ hs
fromList [("foo", "bar"), ("qux", "baz")]
> headerLens "qux" .~ Nothing $ hs
fromList [("foo", "bar")]

However, this won't preserve the order of the elements, which might be a problem for you. There's probably an ordered map out there somewhere, similar to Python's OrderedDict, but I haven't used it in Haskell before.

bheklilr
  • 53,530
  • 6
  • 107
  • 163
  • Thanks @bheklilr . You are right about the ordering being important, and generally I have refrained from optimizing before profiling. – dsign May 17 '15 at 17:19
  • @dsign If that's the case then simply use your `Headers` type, replace `M.lookup` with `Prelude.lookup` and re-implement [`Data.Map.alter`](http://hackage.haskell.org/package/containers-0.5.6.3/docs/src/Data-Map-Base.html#alter) for association lists, which will be the tricky bit. Or you could browse around Hackage for a while trying to find if someone else has done that already, because they probably have. – bheklilr May 17 '15 at 17:23
  • Good point. One nasty little detail that I just realized is that I can not obey the lens laws in my current setup.... so yes I think I need to switch to a map and order the keys with a custom function (it is not lexicographical, but there is a *proper* ordering). Thanks! – dsign May 17 '15 at 17:33
  • @dsign You could wrap `ByteString` in a newtype that implements `Ord` in the correct way, and then you wouldn't have any problems whatsoever, so long as that ordering is well behaved. There are functions in `Data.Map` for getting ascending or descending association lists, too, so once you need the order you can extract it. – bheklilr May 17 '15 at 17:35