mischief-ecs-0.2.0.0: src/Mischief/ECS/Tutorial/Guides/MonadicQueries.hs
{-# OPTIONS_GHC -Wno-unused-imports #-}
-- |
-- Module: Queries Tutorial
-- Description: How To Mischief.
--
-- [Previous Chapter: Query Piping]("Mischief.ECS.Tutorial.Guides.QueryPiping")
--
-- [Next Chapter: Scheduling and Running Systems]("Mischief.ECS.Tutorial.Guides.Scheduling")
--
-- [Main Page]("Mischief.ECS")
module Mischief.ECS.Tutorial.Guides.MonadicQueries
( -- * Learn You an ECS for Great Mischief! - 2.6. Monadic Queries
-- $intro
-- * Binding
-- $bind
-- * Sources
-- $source
-- * Accumulating and Aggregating Queries
-- $accumulation
-- * Grabbing New Components and Resources
-- $extend
-- * Lifting
-- $lifting
-- * [Next Chapter: Scheduling and Running Systems]("Mischief.ECS.Tutorial.Guides.Scheduling")
)
where
import Control.Monad (void)
import Data.Foldable (for_)
import Data.Traversable (for)
import Mischief.ECS
-- $intro
-- Finally we get to the most powerful and complete ability of Queries: monadic operations!
-- $bind
-- Monadic queries are usually written by using one of the query runners (@query_@, @query@, @single@), followed by a @do@:
--
-- @
-- 'query_' $ do
-- playerName <- ['q'|Name / With Player|]
-- 'pure' ()
-- @
--
-- When you bind a value out of a query (the @<-@ above), you are immediately iterating over all elements of a query and applying operations to them.
-- It is in a way similar to the @List@ monad if you are familiar with it (if you are not, don't worry!).
--
-- Essentially, in pseudo-imperative, python-eseque code, the above is equivalent to:
--
-- @
-- for playerName in [q|Name / With Player|]:
-- pure ()
-- @
--
-- There are many great uses to this, which you'll see in this chapter.
-- The most naive one is that it naturally allows you to write sql-like joins:
--
-- @
-- 'query_' $ do
-- playerName <- ['q'|Name / With Player|]
-- ['q'|Name / With Enemy|]
-- & 'qinfo' (\\enemyName -> ['i'|#{playerName}, #{enemyName}|])
-- @
--
-- Which is, in essence:
--
-- @
-- for playerName in [q|Name / With Player|]:
-- for enemyName in [q|Name / With Enemy|]:
-- print (playerName + \", \" + enemyName)
-- @
-- $source
-- There is one detail about the section above that may not make much sense at first. Essentially, you may be wondering what happens if you do this:
--
-- @
-- 'query_' $ do
-- Health health <- ['q'|Health / With Player|]
-- ['q'|Name / With Enemy|]
-- & 'qinsert' (\_ -> (Health $ health - 1))
-- @
--
-- It does, in fact, insert the player's health (minus 1) to each enemy! But why, and how can we do an insertion on the player?
--
-- The answer are Sources. Each time you start a new query (e.g. the @[q|Name / With Enemy|]@ above), you turn your \"attention\" to the
-- new entiites. The source of the query are the enemies.
--
-- You can change the source of a query using @qrefocus@; this code will iterate over all enemies but insert the Health component on the player:
--
-- @
-- 'query_' $ do
-- (player, Health health) <- ['q'|Entity, Health / With Player|]
-- ['q'|Name / With Enemy|]
-- & 'qrefocus' player
-- & 'qinsert' (\_ -> (Health $ health - 1))
-- @
--
-- Keeping track of sources can be a significant footgun and one of the main disadvantages to writing monadic queries.
--
-- One other way to bring the \"attention\" back to the players is by using @qpure@ (or @pure ()@ followed by @qrefocus@):
--
-- @
-- 'query_' $ do
-- (player, Health health) <- ['q'|Entity, Health / With Player|]
-- enemyName <- ['q'|Name / With Enemy|]
-- 'qpure' player
-- & 'qinsert' (\_ -> (Health $ health - 1))
-- @
--
-- @qpure@ essentially creates a one-element query with the current player as the source, you can think of this as:
--
-- @
-- for (player, Health health) in ['q'|Entity, Health / With Player|]:
-- for enemyName in ['q'|Name / With Enemy|]
-- for _ in [()]:
-- insert (Health $ health - 1) player
-- @
--
-- Note that using just @pure ()@ without refocusing is completely valid. But keep in mind that the source of the computation you're
-- writing will be the \"null entity\". If you want to perform any sourced operations such as @qinsert@, you'll need to refocus first.
-- $accumulation
-- To re-use the context from the previous chapter, consider that you have players marked by a @Player@ component, each with
-- a @Coins Int@ component storing how many coins they have collected. You have coins in the world, marked by @Coin Int@, and want to
-- iterate over all players, then over all coins, and, if the coin is in range of the player, despawn the entity and add its value to the
-- total collected by the player.
--
-- This \"nested query\" is a problem you may encounter in a lot of places. You've already seen how it's solved imperatively, by actually nesting the queries,
-- but that solution isn't great. It's highly imperative and mutable, and the indentation gets bad as you need to nest more and more queries.
--
-- This is where the monadic part of the queries comes in. The goal is to perform all those operations /inside/ the same query, smartly chaining them using /do/.
--
-- As a first step it's often helpful to start with a code like this:
--
-- @
-- 'query_' $ do
-- (entity, 'From' _ tile, Coins coins) \<- ['q'|Entity, OnTile -\> (Entity), Coins / With Player|]
-- 'pure' ()
-- @
--
-- Just copy the main query that you're iterating over. And /bind/ the data out of it using @<-@.
-- Wrap it in a @do@ folllwing a @query_@ or @query@.
--
-- At this point, you've put your hand on a slice of the data flowing through the query. What you do with this data
-- dictates what will happen with all data.
--
-- Next, try replacing the @pure@ with another query, typically the second query that you're iterating over:
--
-- @
-- 'query_' $ do
-- (entity, 'From' _ tile, Coins coins) \<- ['q'|Entity, OnTile -\> (Entity), Coins / With Player|]
--
-- ['q'|Coin / With OnTile -\> tile|]
-- & 'qtap' (\\e _ -> 'despawn' e)
-- @
--
-- This is already a valid query. You're iterating over all the coins that are on each player's tile. You've changed the source, or subject,
-- from being the players to being the coins. But in this case that's not really what you want. You want to somehow collect or aggregate
-- the coins to use them in the larger query.
--
-- Let's try collecting them first:
--
-- @
-- 'query_' $ do
-- (entity, 'From' _ tile, Coins coins) \<- ['q'|Entity, OnTile -\> (Entity), Coins / With Player|]
--
-- collectedCoins <- 'qcollect' $ do
-- ['q'|Coin / With OnTile -\> tile|]
-- & 'qtap' (\\e _ -> 'despawn' e)
--
-- pure ()
-- @
--
-- The @qcollect@ function is, in a way, the opposite of the bind operation. It returns from a loop by aggregating all its results into a list.
-- You may see that @collectedCoins@ has type @[From Coin]@.
--
-- Now we need to calculate the actual sum of all coins. We can use a fold for that:
--
-- @
-- let totalCoins = 'foldr' (\\('From' _ (Coin x)) -> (+ x)) coins collectedCoins
-- @
--
-- In order to update the Coins component you have a few options.
--
-- Firstly, you can use @qpure entity@ (or @pure () & qrefocus entity@) to bring the attention back to the player and run a @qinsert@ on it:
--
-- @
-- 'query_' $ do
-- (entity, 'From' _ tile, Coins coins) \<- ['q'|Entity, OnTile -\> (Entity), Coins / With Player|]
--
-- collectedCoins <- 'qcollect' $ do
-- ['q'|Coin / With OnTile -\> tile|]
-- & 'qtap' (\\e _ -> 'despawn' e)
--
-- pure ()
-- @
--
-- So this is what you want to be doing instead:
--
-- @
-- 'query_' $ do
-- (entity, 'From' _ tile, Coins coins) \<- ['q'|Entity, OnTile -\> (Entity), Coins / With Player|]
--
-- ['q'|Coin / With OnTile -\> tile|]
-- & 'qtap' (\\e _ -> 'despawn' e)
-- & 'qcollect' entity
-- & 'qinsert' (\\collectedCoins -> Coins $ 'foldr' (\\('From' _ (Coin x)) -> (+ x)) coins collectedCoins)
--
-- let totalCoins = 'foldr' (\\('From' _ (Coin x)) -> (+ x)) coins collectedCoins
--
-- 'qpure' entity
-- & qinsert (const $ Coins totalCoins)
-- @
--
-- That's it! The coins are now folded and added to the total held by the player.
--
-- You can also iterate over the \"null entity\" and perform an insertion manually:
--
-- @
-- 'pure' ()
-- & 'qtap' (\\_ _ -> 'insert' (Coins totalCoins) entity)
-- @
--
-- Or you can use @qlift@ which does exactly this!
--
-- @
-- 'qlift' $ 'insert' (Coins totalCoins) entity
-- @
-- $extend
-- One powerup monadic queries give you is that they let you seamlessly read components and resources without unwrapping Maybes.
--
-- @
-- 'query_' $ do
-- (entity, health) <- ['q'|Entity, Health|]
-- (names, player) <- 'qget' entity ['qd'|Likes -> (Names), Player|]
-- someRes <- 'qres' \@SomeRes
-- 'pure' ()
-- @
--
-- @qget@ will grab the respective Query Data from the given entity. Due to the nature of the monad, your query will be nullified
-- if any of these componetns don't exist. You would be iterating over an empty list!
--
-- @qres@ works the same way but grabs a resource.
-- $lifting
-- You can lift systems into the query monad by using @qlift@:
--
-- @
-- 'query_' $ do
-- (player, Health x) <- ['q'|Entity, Health / With Player|]
-- 'qlift' $ 'insert' (Health $ x + 1) player
-- @
--
-- This, essentially, allows you to switch between the two /access modes/. One where you can easily query and grab new data, and one
-- where you can perform fine-grained changes on said data.