Используйте линзу для замены определенного элемента списка (ключ, значение)

Я хочу использовать библиотеку линз от Kmett для доступа к элементу списка (ключ, значение) в конкретный ключ. Другими словами, я хотел бы заменить этот код чем-то более идиоматичным и, возможно, более коротким:

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 )

где функции поддержки определены следующим образом:

-- | 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    

person dsign    schedule 17.05.2015    source источник


Ответы (1)


Что ж, если вместо этого вы будете использовать Data.Map.Map в качестве своей структуры (должен быть довольно простой рефакторинг), вам не придется копировать всю эту работу самостоятельно:

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

Тогда вы можете оставить свой headerLens как есть. Или вы могли бы изучить что-то вроде

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

Который вообще не нуждается во вспомогательных функциях и может иметь довольно общую сигнатуру для работы с большим количеством типов Map. Пример использования:

> 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")]

Однако это не сохранит порядок элементов, что может стать для вас проблемой. Вероятно, где-то есть упорядоченная карта, похожая на Python OrderedDict, но я раньше не использовал ее в Haskell.

person bheklilr    schedule 17.05.2015
comment
Спасибо @bheklilr. Вы правы в том, что порядок важен, и обычно я воздерживался от оптимизации перед профилированием. - person dsign; 17.05.2015
comment
@dsign Если это так, просто используйте свой тип Headers, замените M.lookup на Prelude.lookup и повторно реализуйте Data.Map.alter для списков ассоциаций, что будет сложной задачей. Или вы можете некоторое время просмотреть Hackage, пытаясь найти, сделал ли кто-то еще это уже, потому что они, вероятно, сделали. - person bheklilr; 17.05.2015
comment
Хорошая точка зрения. Одна неприятная маленькая деталь, которую я только что осознал, заключается в том, что я не могу подчиняться законам объектива в моей текущей настройке .... так что да, я думаю, мне нужно переключиться на карту и заказать клавиши с пользовательской функцией (это не лексикографический, но есть правильный порядок). Спасибо! - person dsign; 17.05.2015
comment
@dsign Вы можете обернуть ByteString в новый тип, который правильно реализует Ord, и тогда у вас не будет никаких проблем, если этот порядок будет правильным. В Data.Map также есть функции для получения восходящих или нисходящих списков ассоциаций, поэтому, как только вам понадобится порядок, вы можете его извлечь. - person bheklilr; 17.05.2015