SimulaVR / godot-haskell

Haskell bindings for GdNative
BSD 3-Clause "New" or "Revised" License
171 stars 18 forks source link

Creating a DSL #24

Open lboklin opened 5 years ago

lboklin commented 5 years ago

The currently pending PR (#23) requires the user to make use of Template Haskell, which made me think that if we're going to introduce code generation we may as well go all the way and create a DSL that is at least ergonomic to use and closer to GDScript in syntax.

I doodled some pseudo code for what we might want it to end up looking like:

-- language exts ...
-- module ...
-- import ...
extends @RigidBody2D $ Mob
  { minSpeed = 150
  , maxSpeed = 250
  , mobTypes = ["walk", "swim", "fly"]
  }
data Mob = Mob
  { minSpeed :: Float
  , maxSpeed :: Float
  , mobTypes :: [Text]
  }

_ready :: RigidBody2D -> Mob -> IO Mob
_ready self mobState = do
  let randElem xs = (xs !!) <$> randomRIO (0, length xs - 1)
  randAnim <- mobTypes mobState & randElem >>= toLowLevel
  getNode @AnimatedSprite self "AnimatedSprite" >>= (`set_animation` randAnim)

_on_VisibilityNotifier2D_screen_exited :: RigidBody2D -> Mob -> IO Mob
_on_VisibilityNotifier2D_screen_exited self _ = do
  queue_free self

this would generate loosely something like this:

-- language exts ...
-- module ...
-- import ...

-- `extends ...` Generates a wrapper type, a HasBaseClass and a NativeScript
--  instance which might look like this
data MobWrapper = MobW RigidBody2D (MVar Mob)

instance HasBaseClass MobWrapper where
  type BaseClass MobWrapper = RigidBody2D
  super (MobW base _) = base

instance NativeScript MobWrapper where
  classInit base = MobWrapper base <$> newMVar
    { minSpeed = 150
    , maxSpeed = 250
    , mobTypes = ["walk", "swim", "fly"]
    }
  classMethods =
    -- All the top-level function declarations are turned into this
    [ method0 "_ready" $ \(MobWrapper base mva) -> do
        a <- readMVar mva
        _ready base a
    , method0 "_on_VisibilityNotifier2D_screen_exited" $ \(MobWrapper base mva) -> do
        a <- readMVar mva
        _on_VisibilityNotifier2D_screen_exited base a
    ]

data Mob = Mob
  { minSpeed :: Float
  , maxSpeed :: Float
  , mobTypes :: [Text]
  }

_ready :: RigidBody2D -> Mob -> IO Mob
_ready self mobState = do
  let randElem xs = (xs !!) <$> randomRIO (0, length xs - 1)
  randAnim <- mobTypes mobState & randElem >>= toLowLevel
  getNode @AnimatedSprite self "AnimatedSprite" >>= (`set_animation` randAnim)

_on_VisibilityNotifier2D_screen_exited :: RigidBody2D -> Mob -> IO Mob
_on_VisibilityNotifier2D_screen_exited self _ = do
  queue_free self

This could probably be made even tidier with a state transformer type.

The result is much closer to a functional substitute for GDScript, but whether this works in implementation remains to be seen, as I haven't spent enough time with Haskell code generation to spot any blockers but handling variable number of arguments for various top level function declarations might be difficult (or not).

It may be worth investigating available extensible records libraries as well.

rainbyte commented 5 years ago

Let me see if I understand... Every method would be a function which would have a type similar to the following one?

foo :: Monad m => b -> c -> a -> m a 
lboklin commented 5 years ago

@rainbyte They already do since we usually need to do IO in them; the only changes this makes to type signatures is that we

YellowOnion commented 3 years ago

Is there any update for this? plus what about Godot monad abstracting over Monad.IO.Class?