Which effect design should I use for my real world tiny Http/Spotify example? #154
-
Working example of what I currently have here: https://github.com/ParetoOptimalDev/real-world-advanced-testing-using-effectful/tree/tinier I'm trying to figure out how to properly design a simple program that queries spotify with effectful. Before I get into details and give context my questions are:
Note that these unanswered question have been the biggest reason keeping me from using effectful by default over mtl. I think that this would have two effects: data Http :: Effect where
HttpGet :: Url -> Http m Aeson.Value
makeEffect ''Http
data Spotify :: Effect where
SearchTrack :: String -> Spotify m Aeson.Value So now I could follow the example of data FileSystem :: Effect where
ReadFile :: FilePath -> FileSystem m String and create a pure interpreter: runFileSystemPure
:: Error FsError :> es
=> M.Map FilePath String
-> Eff (FileSystem : es) a
-> Eff es a
runFileSystemPure fs0 = reinterpret (evalState fs0) $ \_ -> \case
ReadFile path -> gets (M.lookup path) >>= \case
Just contents -> pure contents
Nothing -> throwError . FsError $ "File not found: "o ++ show path This works pretty well for runHttpPure
:: (Error HttpError :> es)
=> M.Map Url Aeson.Value
-> Eff (Http : es) a
-> Eff es a
runHttpPure db0 = reinterpret (evalState db0) $ \_ -> \case
HttpGet url -> gets (M.lookup url) >>= \case
Just response -> pure response
Nothing -> throwError . HttpError $ "no response found" Then I use httpGet in the runSpotifyPure interpeter like: runSpotifyPure
:: (Http :> es, Error SpotifyError :> es)
=> Eff (Spotify : es) a
-> Eff es a
runSpotifyPure = interpret $ \_ -> \case
SearchTrack query -> do
res <- httpGet (Url (playlistSearchUrl <> query))
let tracks = res ^.. _Array % traversed % _String & fmap (Track . toString)
if length tracks == 0 then
throwError (SpotifyError "no tracks found")
else pure tracks The problem is that if I were to implement
res <- httpGet (Url (playlistSearchUrl <> query))
let tracks = res ^.. _Array % traversed % _String & fmap (Track . toString)
if length tracks == 0 then
throwError (SpotifyError "no tracks found")
else pure tracks
So this made me wonder if this means runSpotifyPure should instead be defined just as the quite trivial: runSpotifyPure
:: (Http :> es, Error SpotifyError :> es)
=> Eff (Spotify : es) a
-> Eff es a
runSpotifyPure = interpret $ \_ -> \case
SearchTrack query -> httpGet (Url (playlistSearchUrl <> query)) And then instead of my application using searchTrackProgram :: (Http :> es, Error SpotifyError :> es, Spotify :> es) => String -> Eff es [Track]
searchTrackProgram query = do
trackResponse <- searchTrack query
let tracks = trackResponse ^.. _Array % traversed % _String & fmap (Track . toString)
if length tracks == 0 then
throwError (SpotifyError "no tracks found")
else pure tracks This way I could remove the duplication when using Overall I guess my goal is to be able to write a test like this: main :: IO ()
main = hspec $ do
describe "runSpotify" $ do
it "searches for a track" $ do
let db0 = fromList [("https://api.spotify.com/v1/search?type=track&q=", aesonQQ [aesonQQ|
{
"tracks": {
"items": ["effectful"]
}
}
|])]
result = runPureEff $ runSpotify db0 $ searchTrack "effectful"
result `shouldBe` aesonQQ [aesonQQ|
{
"items": ["effectful"]
}
|] Edit: Potential answer to error question in #149 (comment) |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 2 replies
-
The first example you give for runSpotifyPure looks like it should instead just be called runSpotify. If I understand it right, whether it's pure or not depends entirely on whether you're using the IO Http handler or the pure Http handler. That's also why you have so much code duplication between runSpotifyPure and the theoretical runSpotifyIO: they should be the same function! If you want a unique runSpotifyPure, it probably shouldn't mention Http anywhere in the constraint. One benefit to having the Spotify effect instead of just having a searchTrackProgram function is that, when testing code which depends on the Spotify effect, you only have to mock out Spotify instead of mocking out Http too. This helps you focus your tests by ignoring implementation details like api endpoints and might be useful if you wanted to write a test which did other real Http effects while using mock data when communicating with Spotify. |
Beta Was this translation helpful? Give feedback.
-
Yes... it should be a "program" rather than an interpreter and maybe that means I shouldn't even have the spotify effect...
Wouldn't it internally need to use Http though? Maybe I'm not quite sure exactly how you mean this point. |
Beta Was this translation helpful? Give feedback.
-
Let's assume a program which should add all search results to the favorites: searchTrackProgram :: (Http :> es, Error SpotifyError :> es, Spotify :> es) => String -> Eff es [Track]
addToFavorites :: (Http :> es, Error FavoriteError :> es, Favorite :> es) => Track -> Eff es ()
program :: ( Http :> es
, Error SpotifyError :> es
, Error FavoriteError :> es
, Spotify :> es
, Favorite :> es
) => String -> Eff es ()
program query = do
tracks <- searchTrack query
forM_ tracks addToFavorites Here when it comes to testing, our test should be overwhelmed by handling effects i.e. Let's express our program in terms of resources, I think even the word searchTrack :: TrackSourceManagement :> es => String -> Effect es [Track]
addToFavorites :: FavoritesManagement :> es => Track -> Eff es ()
program :: ( TrackSourceManagement :> es
, FavoritesManagement :> es
) => String -> Eff es ()
program query = do
tracks <- searchTrack query
forM_ tracks addToFavorites Testing this program is hilariously simple:
|
Beta Was this translation helpful? Give feedback.
Let's assume a program which should add all search results to the favorites:
Here when it comes to testing, our test should be overwhelmed by handling effects i.e.
Error
andHttp
which by any means doesn't express our intenti…