Как вы, несомненно, знаете, в отличие от объектов Javascript, ADT Haskell не являются расширяемыми, поэтому вы не можете просто «добавить поле».В определенных обстоятельствах может иметь смысл включить поле с типом Maybe
, изначально установленным на Nothing
, которое затем заполняется.В редких случаях имеет смысл выполнить очень небезопасную операцию, включив поле с его окончательным типом, но его значения инициализируются снизу (т. Е. undefined
) и заполняют его позже.
В качестве альтернативы вы можете переключиться накакой-то явно расширяемый тип записи, такой как HList
.
. Однако наиболее простой подход, использующий систему типов Haskell по назначению, заключается во введении нового типа для представления дорожки.дополнен жанровой информацией.Если у вас есть дополнительные типы данных, включающие поля Track
, которые вы хотите использовать повторно, они могут быть полиморфными в типе дорожки.Итак, учитывая ваши типы данных выше, вы должны ввести новый тип:
data Track' = Track'
{ playedAt :: String
, externalUrls :: String
, name :: String
, artists :: [Artist]
, genres :: [Genre] -- added field
, explicit :: Bool
}
(для которого требуется расширение DuplicateRecordFields
, чтобы сосуществовать с Track
) и сделать зависимые типы полиморфными в типе дорожки:
data Tracks trk = Tracks
{ tracks :: [trk]
}
data RecentlyPlayed trk = RecentlyPlayed
{ recentlyPlayed :: Tracks trk
, next :: String
}
Преобразование списка воспроизведения может быть выполнено с использованием:
addGenre :: (Artist -> [Genre]) -> RecentlyPlayed Track -> RecentlyPlayed Track'
addGenre g (RecentlyPlayed (Tracks trks) nxt)
= RecentlyPlayed (Tracks (map cvtTrack trks)) nxt
where
cvtTrack (Track p e n a ex) = Track' p e n a (concatMap g a) ex
или, альтернативно, с использованием расширения RecordWildCards
, которое будет намного более читабельным, особенно для очень больших записей:
addGenre' :: (Artist -> [Genre]) -> RecentlyPlayed Track -> RecentlyPlayed Track'
addGenre' g RecentlyPlayed{recentlyPlayed = Tracks trks, ..}
= RecentlyPlayed{recentlyPlayed = Tracks (map cvtTrack trks), ..}
where
cvtTrack (Track{..}) = Track' { genres = concatMap g artists, .. }
или с использованием подхода Lens, или даже с использованием deriving (Functor)
экземпляров со всеми тяжелыми подъемами, выполненными fmap
:
addGenre'' :: (Artist -> [Genre]) -> RecentlyPlayed Track -> RecentlyPlayed Track'
addGenre'' g = fmap cvtTrack
where
cvtTrack (Track{..}) = Track' { genres = concatMap g artists, .. }
, хотя подход с использованием функторов не очень хорошо масштабируетсяесли имеется несколько дополнений (например, если вы обнаружите, что хотите ввести тип RecentlyPlayed artist track
).Подход Data.Generics
может хорошо работать в этом случае.
Однако, с более общей точки зрения дизайна, вы можете спросить себя, почему вы пытаетесь расширить представление RecentlyPlayed
таким способом.Это хорошее представление необходимых частей базового API Javascript, но это плохое представление для работы с остальной логикой вашей программы.
Предположительно, остальная часть вашей программы имеет дело главным образом со спискомотслеживает, и не должен заботиться о следующих next
URL, так почему бы не сгенерировать полный список треков с расширенным жанром напрямую?
То есть, учитывая начальный список RecentlyPlayed
и некоторые функции ввода-вывода дляполучите следующий список и найдите информацию о жанре:
firstRecentlyPlayed :: RecentlyPlayed
getNextRecentlyPlayed :: String -> IO RecentlyPlayed
getGenresByArtist :: Artist -> IO [Genre]
вам, вероятно, понадобится что-то вроде:
getTracks :: IO [Track']
getTracks = go firstRecentlyPlayed
where go :: RecentlyPlayed -> IO [Track']
go (RecentlyPlayed (Tracks trks) next) = do
trks' <- mapM getGenre trks
rest <- go =<< getNextRecentlyPlayed next
return $ trks' ++ rest
getGenre Track{..} = do
artistGenres <- mapM getGenresByArtist artists
return (Track' {genres = concat artistGenres, ..})
для первой попытки.Конечно, вы захотите изменить это, чтобы не искать жанры одного и того же исполнителя снова и снова, но это идея.