Skip to content

Commit

Permalink
feat: arrowized systems
Browse files Browse the repository at this point in the history
  • Loading branch information
matthunz committed Jan 30, 2025
1 parent 1fce47f commit f52e7d3
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 22 deletions.
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ A type-safe and friendly [ECS](https://en.wikipedia.org/wiki/Entity_component_sy
- Modular design: Aztecs can be extended for a variety of use cases

```hs
import Control.Monad.IO.Class (MonadIO (..))
import Control.Arrow
import Data.Aztecs
import qualified Data.Aztecs.Access as A
import qualified Data.Aztecs.System as S
import qualified Data.Aztecs.World as W

newtype Position = Position Int deriving (Show)
Expand All @@ -27,22 +28,25 @@ newtype Velocity = Velocity Int deriving (Show)

instance Component Velocity

app :: Access IO ()
app = do
-- Spawn an entity with position and velocity components
A.spawn_ (Position 0 :& Velocity 1)
data Setup

-- Update all matching entities
q <- A.map (\(Position x :& Velocity v) -> Position (x + v))
liftIO $ print q
instance System IO Setup where
task = S.queue (A.spawn_ (Position 0 :& Velocity 1))

data Movement

instance System IO Movement where
task = S.map (\(Position x :& Velocity v) -> Position (x + v)) >>> S.run print

main :: IO ()
main = do
_ <- runAccess app W.empty
w <- S.runSystem @_ @Setup W.empty
_ <- S.runSystem @_ @Movement w
return ()
```

## Benchmarks

Aztecs is currently faster than [bevy-ecs](https://github.com/bevyengine/bevy/), a popular and high-performance ECS written in Rust, for simple mutating queries.

<img alt="benchmark results: Aztecs 932us vs Bevy 6,966us" width=300 src="https://github.com/user-attachments/assets/348c7539-0e7b-4429-9cc1-06e8a819156d" />
Expand Down
3 changes: 2 additions & 1 deletion examples/ECS.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

module Main where

import Control.Arrow
import Data.Aztecs
import qualified Data.Aztecs.Access as A
import qualified Data.Aztecs.System as S
Expand All @@ -26,7 +27,7 @@ instance System IO Setup where
data Movement

instance System IO Movement where
task = S.map (\(Position x :& Velocity v) -> Position (x + v)) <&> S.run print
task = S.map (\(Position x :& Velocity v) -> Position (x + v)) >>> S.run print

main :: IO ()
main = do
Expand Down
3 changes: 1 addition & 2 deletions src/Data/Aztecs.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ module Data.Aztecs
Entity,
(:&) (..),
System (..),
(<&>),
)
where

import Data.Aztecs.Access (Access, runAccess)
import Data.Aztecs.Component (Component (..))
import Data.Aztecs.Entity (Entity, EntityID, (:&) (..))
import Data.Aztecs.System (System (..), (<&>))
import Data.Aztecs.System (System (..))
34 changes: 24 additions & 10 deletions src/Data/Aztecs/System.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

module Data.Aztecs.System where

import Control.Arrow (Arrow (..))
import Control.Category (Category (..))
import Data.Aztecs.Access (Access, runAccess)
import Data.Aztecs.Entity (ComponentIds (componentIds), Entity, EntityT)
import Data.Aztecs.Query (IsEq, Queryable)
Expand Down Expand Up @@ -41,7 +43,7 @@ newtype Task m i o = Task
deriving (Functor)

instance (Monad m) => Applicative (Task m i) where
pure a = Task (,[],\_ _ -> pure (a, id, pure ()))
pure a = Task (,[],\_ _ -> pure (a, Prelude.id, pure ()))
f <*> a =
Task $ \w ->
let (w', cIds, f') = runTask f w
Expand All @@ -51,22 +53,34 @@ instance (Monad m) => Applicative (Task m i) where
\i cs -> do
(f'', fG, access) <- f' i cs
(a'', aG, access') <- a' i cs
return (f'' a'', fG . aG, access >> access')
return (f'' a'', fG Prelude.. aG, access >> access')
)

(<&>) :: (Monad m) => Task m i o -> Task m o a -> Task m i a
t1 <&> t2 =
Task $ \w ->
let (w', cIds, f) = runTask t1 w
(w'', cIds', g) = runTask t2 w'
instance (Monad m) => Category (Task m) where
id = Task (,[],\i _ -> pure (i, Prelude.id, pure ()))
(.) t1 t2 = Task $ \w ->
let (w', cIds, f) = runTask t2 w
(w'', cIds', g) = runTask t1 w'
in ( w'',
cIds <> cIds',
\i cs -> do
(o, f', access) <- f i cs
(a, g', access') <- g o cs
return (a, g' . f', access >> access')
return (a, g' Prelude.. f', access >> access')
)

instance (Monad m) => Arrow (Task m) where
arr f = Task (,[],\i _ -> pure (f i, Prelude.id, pure ()))
first t =
Task $ \w ->
let (w', cIds, f) = runTask t w
in ( w',
cIds,
\(i, x) cs -> do
(o, f', access) <- f i cs
return ((o, x), f', access)
)

all :: forall m v. (Monad m, ComponentIds v, Queryable v) => Task m () [Entity v]
all = view @_ @v (\v cs -> pure $ V.queryAll v cs)

Expand Down Expand Up @@ -103,11 +117,11 @@ mapView f = Task $ \w ->

-- | Queue an `Access` to alter the world after this task is complete.
queue :: (Monad m) => Access m () -> Task m () ()
queue a = Task (,[],\_ _ -> pure ((), id, a))
queue a = Task (,[],\_ _ -> pure ((), Prelude.id, a))

run :: (Monad m) => (i -> m o) -> Task m i o
run f =
Task
(,[],\i _ -> do
o <- f i
return (o, id, pure ()))
return (o, Prelude.id, pure ()))

0 comments on commit f52e7d3

Please sign in to comment.