You need lenses, in particular the sequenceOf function. Here is an example of a target load of 3 tuples:
sequenceOf _2 :: (IO a, IO b, IO c) -> IO (IO a, b, IO c)
sequenceOf transfers the lens to a polymorphic field that contains the load action, triggers the action, and then replaces the field with the result of the action. You can use sequenceOf for your own types by simply making your type polymorphic in the fields you want to load, for example:
data Asset ab = Asset { _art :: a , _sound :: b }
... and also for creating your lenses using four parameters (this is one of the reasons for their existence):
art :: Lens (Asset a1 b) (Asset a2 b) a1 a2 art k (Asset xy) = fmap (\x' -> Asset x' y) (kx) sound :: Lens (Asset a b1) (Asset a b2) b1 b2 sound k (Asset xy) = fmap (\y' -> Asset x y') (ky)
... or you can automatically generate lenses using makeLenses , and they will be fairly general.
Then you can simply write:
sequenceOf art :: Asset (IO Art) b -> IO (Asset Art b)
... and loading multiple assets is as simple as drawing up Kleisli arrows ::
sequenceOf art >=> sequenceOf sound :: Asset (IO Art) (IO Sound) -> IO (Asset Art Sound)
... and, of course, you can invest assets and create lenses for access to invested resources, and it still "just works."
Now you have a pure Asset type that you can process using pure functions, and all the loading logic is taken into account in lenses.
I wrote this on my phone, so there may be some errors, but I will fix them later.