我正在寻找解决一个问题,即我从HTTP调用中构造一些数据,然后基于该数据进行另一次HTTP调用,并使用来自第二次调用的信息丰富原始数据。
我有代码通过wreq将Spotify最近播放的API调用(JSON)作为ByteString,并返回我完全形成的"最近播放"数据类型。
但是,为了在 Spotify API 中获取曲目的流派,需要对其艺术家端点进行第二次 HTTP 调用,我不太确定如何修改我的曲目数据类型以添加"流派"字段,我稍后会填充该字段,我也不确定以后如何实际填充它, 显然,我需要遍历我的原始数据结构,拉出艺术家 ID,调用新服务器 - 但我不确定如何将这些额外的数据添加到原始数据类型中。
{-# LANGUAGE OverloadedStrings #-}
module Types.RecentlyPlayed where
import qualified Data.ByteString.Lazy as L
import qualified Data.Vector as V
import Data.Aeson
import Data.Either
data Artist = Artist {
id :: String
, href :: String
, artistName :: String
} deriving (Show)
data Track = Track {
playedAt :: String
, externalUrls :: String
, name :: String
, artists :: [Artist]
, explicit :: Bool
} deriving (Show)
data Tracks = Tracks {
tracks :: [Track]
} deriving (Show)
data RecentlyPlayed = RecentlyPlayed {
recentlyPlayed :: Tracks
, next :: String
} deriving (Show)
instance FromJSON RecentlyPlayed where
parseJSON = withObject "items" $ recentlyPlayed -> RecentlyPlayed
<$> recentlyPlayed .: "items"
<*> recentlyPlayed .: "next"
instance FromJSON Tracks where
parseJSON = withArray "items" $ items -> Tracks
<$> mapM parseJSON (V.toList items)
instance FromJSON Track where
parseJSON = withObject "tracks" $ tracks -> Track
<$> tracks .: "played_at"
<*> (tracks .: "track" >>= (.: "album") >>= (.: "external_urls") >>= (.: "spotify"))
<*> (tracks .: "track" >>= (.: "name"))
<*> (tracks .: "track" >>= (.: "artists"))
<*> (tracks .: "track" >>= (.: "explicit"))
instance FromJSON Artist where
parseJSON = withObject "artists" $ artists -> Artist
<$> artists .: "id"
<*> artists .: "href"
<*> artists .: "name"
marshallRecentlyPlayedData :: L.ByteString -> Either String RecentlyPlayed
marshallRecentlyPlayedData recentlyPlayedTracks = eitherDecode recentlyPlayedTracks
(https://github.com/imjacobclark/Recify/blob/master/src/Types/RecentlyPlayed.hs)
这对于单个 API 调用非常有效,它的用法可以在这里看到:
recentlyPlayedTrackData <- liftIO $ (getCurrentUsersRecentlyPlayedTracks (textToByteString . getAccessToken . AccessToken $ accessTokenFileData))
let maybeMarshalledRecentlyPlayed = (marshallRecentlyPlayedData recentlyPlayedTrackData)
(https://github.com/imjacobclark/Recify/blob/master/src/Recify.hs#L53-L55)
{-# LANGUAGE OverloadedStrings #-}
module Clients.Spotify.RecentlyPlayed where
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Char8 as B
import qualified Network.Wreq as W
import System.Environment
import Control.Monad.IO.Class
import Control.Lens
recentlyPlayerUri = "https://api.spotify.com/v1/me/player/recently-played"
getCurrentUsersRecentlyPlayedTracks :: B.ByteString -> IO L.ByteString
getCurrentUsersRecentlyPlayedTracks accessToken = do
let options = W.defaults & W.header "Authorization" .~ [(B.pack "Bearer ") <> accessToken]
text <- liftIO $ (W.getWith options recentlyPlayerUri)
return $ text ^. W.responseBody
(https://github.com/imjacobclark/Recify/blob/master/src/Clients/Spotify/RecentlyPlayed.hs)
我希望能够调用第一个 API,构造我的数据类型,调用第二个 API,然后使用从第二个 HTTP 调用返回的数据丰富第一个数据类型。
毫无疑问,与Javascript对象不同,Haskell ADT是不可扩展的,所以你不能简单地"添加一个字段"。 在某些情况下,包含最初设置为Nothing
Maybe
类型的字段,然后填充该字段可能是有意义的。 很少,执行非常不安全的操作可能是有意义的,即使字段包含其最终类型,但其值初始化为底部(即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
表示。 这是底层 Javascript API 所需部分的良好表示,但在程序逻辑的其余部分使用时,它的表现很差。
据推测,程序的其余部分主要处理曲目列表,并且不应该关注以下next
URL,那么为什么不直接生成流派增强曲目的完整列表呢?
也就是说,给定一个初始RecentlyPlayed
列表和一些 IO 函数来获取下一个列表并查找流派信息:
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, ..})
第一次尝试。 当然,您需要修改此设置以避免一遍又一遍地查找同一艺术家的流派,但这就是想法。