Это аккуратный пример, так как создание Endo
, то есть функция a -> a
как ваш [Film] -> [Film]
это отличный способ справиться с «состоянием» в безгосударственном языке. Давайте погружение в.
Поэтому цель состоит в том, чтобы создать функцию, как becomeFan "Joseph" "7 1/2" :: [Film] -> [Film]
является «функция обновления базы данных фильм». Чтобы выполнить это обновление, вам необходимо изменить базу данных фильма, чтобы обновить список вентиляторов для фильма "7 1/2"
, чтобы включить "Joseph"
. Мы предположим, что имя каждого пользователя глобально уникально и несколько раз записывает эту функцию.
Предположим, что если фильм отсутствует в нашей базе данных, то becomeFan
ничего не делает и что в базе данных нет дубликатов.
Сначала у нас есть прямая рекурсивная версия.
becomeFan _ _ [] = [] -- empty film database
becomeFan name film ([email protected](title, cast, year, fans) : fs)
| film == title = (title, cast, year, name:fans) : fs
| otherwise = f : becomeFan name film fs
Который просто итерация по списку фильмов в базе данных и сделать наше обновление тогда и только тогда Флят заголовки тот, который мы пытаемся изменить. Обратите внимание на @
-syntax, который позволяет нам снимать фильм, который мы рассматриваем «в целом», и все еще деконструируем его.
Проблемы с этим методом несметны, хотя-это просто очень сложно! У нас есть ряд базовых предположений, связанных с тем, как мы реализуем becomeFan
, который может стать desynched с другими функциями, которые мы пишем. К счастью, Haskell очень хорошо справляется с такими проблемами.
Первым шагом является введение более мощных типов данных.
Что мы будем делать это устранить некоторые из синонимов типа и ввести некоторые более мощные типы контейнеров, в частности Set
, который ведет себя как математический набор и Map
который, как словарь или хэш.
import qualified Data.Set as Set
import qualified Data.Map as Map
Мы также используем тип «записи» для Film
. Записи изоморфны кортежам («функционально эквивалентны»), но имеют имена полей, которые полезны для документации и позволяют использовать меньшее количество синонимов типов.
type Name = String
type Year = Int
data Film = Film { title :: Title, cast :: Set Name, year :: Year, fans :: Set Name)
С помощью Map Title Film
представлять нашу базу данных, мы также получаем, чтобы гарантировать уникальность фильмов (а Map
делает ключи Title
в ноль или один Film
с-мы не можем иметь несколько матчей). Недостатком здесь является то, что мы можем desync Title
в ключах Database
с Title
в самом Film
.
type Database = Map Title Film
Так как мы можем переписать в этой новой системе becomeFan
?
becomeFan name title =
Map.alter update title where
update Nothing = Nothing -- that title was not in our database
update (Just f) = Just (f { fans = Set.insert name (fans f) })
теперь мы, опираясь в основном на Map.alter :: (Maybe v -> Maybe v) -> k -> Map k v -> Map k v
и Set.insert :: a -> Set a -> Set a
сделать нашу тяжелую работу и поддерживать различные ограничения уникальности. Обратите внимание, что первый аргумент Map.alter
- это функция Maybe v -> Maybe v
, которая позволяет нам обрабатывать недостающие пленки (если вход Nothing
) и принять решение удалить фильм из базы данных (если мы вернем Nothing
).
Стоит также отметить, что наша внутренняя функция update :: Maybe Film -> Maybe Film
может быть более легко записать в виде fmap (\f = f { fans = Set.insert name (fans f) })
поднять «чистый» шаг обновления вверх в Maybe
как это Functor
.
Можем ли мы сделать лучше? Конечно, но это запутывает здесь. Предыдущий ответ может быть лучшим в большинстве ситуаций. Но давайте подружимся для удовольствия.
Мы могли бы использовать объективы от Control.Lens, чтобы сделать наш доступ к Map
, Set
и Film
еще проще.
Чтобы сделать это, мы будем импортировать модуль
import Control.Lens
и переписать Film
типа так, что библиотека может автоматически создавать линзы с помощью макроса.
data Film = Film { _title :: Title, _cast :: Set Name, _year :: Year, _fans :: Set Name }
$(makeLenses ''Film)
все, что мы должны сделать, это предварять подчеркивание каждой записи имени поля и Control.Lens.makeLenses
автоматически сгенерирует наши линзы под оригинальными названиями. Таким образом, после этой линии у нас есть функции, такие как title :: Lens' Film Title
, что мы и хотели.
Тогда мы можем использовать At
экземпляр Map
«s, чтобы создать нашу функцию переделки, в значительной степени, как и раньше, но записанный в виде строки операций линзы
becomeFan name film = over (at film) (fmap . over fans . Set.insert $ name)
где over (at film)
обобщает и заменяет Map.alter
и (fmap . over fans . Set.insert $ name)
заменяют нашу update
которые мы определили ранее.
Мы можем даже построить мощный объектив сеттера, который смотрит прямо на существование определенного вентилятора в списке вентиляторов определенного Film
.
isFan :: Name -> String -> Setter' Database Bool
isFan name film = at film . mapped . fans . contains name
Эти методы довольно неприятны на первый и очень странные (но полностью отмечаемые) типов, но они становятся очень хорошо, как только вы привыкнете к работе на этом уровне абстракции. Он «читается как английский» и чувствует себя как хорошие части XPath.
becomeFan name film = isFan name film .~ True
и с этой конструкции мы можем даже модернизировать весь процесс сразу в State
монады.
flip execState initialDB $ do
isFan "Joseph" "7 1/2" .= True
isFan "Steve" "Citizen Kane" .= True
-- oh, wait, nevermind
isFan "Joseph" "7 1/2" .= False
Хотя, мы могли бы сделать то же самое с Control.Monad.State.withState
использованием любого определения becomeFan
.
Вы пытались написать эту функцию? Как это выглядело? Что случилось? – Floris
Должно ли это быть 'статьFan :: Title -> Fan -> Database -> Database' или' статьFan :: Fan -> Title -> Database -> Database'? После определения всех этих синонимов типов стыдно не использовать их ... – dave4420
Ваше право, это будет лучший тип для использования функции и сделать ее более ясной. – user2240649