3

In my main function for my haskell game, I start by loading each sprite. I decided to make a data type called SpriteCollection which holds all the sprites (typed Picture) so I can access them later by their name.

loadBMP is a Gloss function typed String -> IO Picture. This code works:

main = do
  -- Load sprites
  playerBmp <- loadBMP "assets/mario.bmp"
  goombaBmp <- loadBMP "assets/goomba.bmp"
  brickBmp <- loadBMP "assets/brick.bmp"
  let sprites = SpriteCollection { 
      marioSprite = playerBmp,
      goombaSprite = goombaBmp
      brickSprite = brickBmp
    }
  playIO (... sprites ...)

So as you can see, as I load more sprites, it feels like I am writing double the code that I should need. Is there a way to get rid of the intermediate variables? For example:

main = do
  -- Load sprites
  let sprites = SpriteCollection { 
      marioSprite <- loadBMP "assets/mario.bmp",
      goombaSprite <- loadBMP "assets/goomba.bmp",
      brickSprite <- loadBMP "assets/brick.bmp"
    }
  playIO (... sprites ...)

but that does not work.

piyepi
  • 85
  • 4

2 Answers2

3

You can use RecordWildCards for that:

{-# LANGUAGE RecordWildCards #-}

main = do
  -- Load sprites
  marioSprite <- loadBMP "assets/mario.bmp"
  goombaSprite <- loadBMP "assets/goomba.bmp"
  brickSprite <- loadBMP "assets/brick.bmp"
  let sprites = SpriteCollection {..}
  playIO (... sprites ...)
Noughtmare
  • 9,410
  • 1
  • 12
  • 38
  • I tend to disapprove of the various “better records” extensions, but this use of RecordWildCards is actually pretty neat and I don't see an alternative way to do it nearly as nicely. – leftaroundabout Nov 04 '22 at 10:07
  • A middle ground is to list the names out explicitly using `NamedFieldPuns`, which I find helps make the code much easier to read and edit later, i.e. `do { marioSprite <- …; goombaSprite <- …; brickSprite <- …; let { sprites = SpriteCollection { marioSprite, goombaSprite, brickSprite } }; playIO … }`. I sometimes use `Arrows` for this as well, since `proc` notation without `-<<` is effectively an `Applicative` comprehension. – Jon Purdy Nov 05 '22 at 01:20
2

Applicative is the tool for combining multiple values that are in the same sort of context.

main = do
  sprites <- SpriteCollection <$> 
    loadBMP "mario" <*> 
    loadBMP "goomba" <*> 
    loadBMP "brick"
  ...

In general,

do
  x <- mx
  y <- my
  return (f x y)

can be written as

f <$> mx <*> my

but only if the final call to f is the only thing that references x and y. This approach scales to arbitrarily many arguments. For small numbers of arguments, you can try something from the liftA family of functions, such as liftA2 f mx my.

amalloy
  • 89,153
  • 8
  • 140
  • 205
  • 2
    If you do this you do need to make sure you get them in the right order. In this case they are all the same type, so the type checker won't help you. – Noughtmare Nov 03 '22 at 22:24
  • Thank you, this works. like @Noughtmare mentioned, is there a way to still take advantage of the record's field names so they don't need to be in order? If not I'll also mark your answer as the solution. – piyepi Nov 03 '22 at 22:32