Объективы для списка ассоциаций - PullRequest
4 голосов
/ 20 марта 2019

В Control.Lens.At есть at объектив для Map / HashMap / и т. Д.Но похож ли какой-либо объектив на at для типа списка ассоциаций [(k, v)] (который можно преобразовать в карту)?

1 Ответ

7 голосов
/ 20 марта 2019

Я не знаю ни одного, который вам предоставлен, но at принадлежит классу типов At, поэтому мы могли бы написать его сами.Чтобы не запачкать руки гибкими (и, возможно, перекрывающимися) расширениями экземпляров, мы сделаем это в новом типе.

newtype AList k v = AList [(k, v)]

Сначала нам понадобится пара экземпляров семейства.

{-# LANGUAGE TypeFamilies #-}

type instance IxValue (AList k v) = v
type instance Index (AList k v) = k

Это просто определяет, что такое «ключ» и «значение» в нашем новом типе, что просто.Теперь нам нужно иметь возможность читать и записывать значения по определенному ключу.Haskell уже дает нам способ чтения значений (Data.List.lookup), но мы должны сами сделать функцию записи.Здесь нет ничего необычного или объективного: только обычные старые фильтры и карты Haskell.

replaceAt :: Eq k => k -> Maybe v -> AList k v -> AList k v
replaceAt k Nothing (AList m) = AList $ filter (\(k', _) -> k /= k') m
replaceAt k (Just v) (AList m) =
    case lookup k m of
      Nothing ->
          -- Not present in the list; add it
          AList ((k, v) : m)
      Just _ ->
          -- Present; replace it
          AList $ map (\(k', v') -> if k == k' then (k', v) else (k', v')) m

Теперь нам нужно написать экземпляр At, который зависит от экземпляра Ixed.К счастью, библиотека линз обеспечивает реализацию по умолчанию для Ixed, поскольку мы реализуем At, поэтому объявление первого экземпляра просто.

instance Eq k => Ixed (AList k v)

Запись at также довольно проста.,Просто посмотрите на типы и немного следите за своим носом, и мы приходим к реализации, к которой мы стремимся.

instance Eq k => At (AList k v) where
    at k f (AList m) = fmap (\v' -> replaceAt k v' (AList m)) $ f (lookup k m)

И мы закончили.Теперь at будет работать для AList.Если вас беспокоит обертка нового типа, вы можете довольно легко создать новую функцию (at', если хотите), которая выполняет упаковку / распаковку нового типа.

Доказательство того, что этот экземпляр удовлетворяет законам объектива, осталосьв качестве упражнения для читателя.

Полный код

{-# LANGUAGE TypeFamilies #-}

import Control.Lens.At
import Data.List(lookup)

newtype AList k v = AList [(k, v)]

type instance IxValue (AList k v) = v
type instance Index (AList k v) = k

replaceAt :: Eq k => k -> Maybe v -> AList k v -> AList k v
replaceAt k Nothing (AList m) = AList $ filter (\(k', _) -> k /= k') m
replaceAt k (Just v) (AList m) =
    case lookup k m of
      Nothing ->
          -- Not present in the list; add it
          AList ((k, v) : m)
      Just _ ->
          -- Present; replace it
          AList $ map (\(k', v') -> if k == k' then (k', v) else (k', v')) m

-- Just take the default implementation here.
instance Eq k => Ixed (AList k v)

instance Eq k => At (AList k v) where
    at k f (AList m) = fmap (\v' -> replaceAt k v' (AList m)) $ f (lookup k m)
...