Reputation: 2862
Say I were designing a game engine in which possible actions are stored as an enumerated type implemented with constructors. For example:
data Action = Walk | Attack | Drop | PickUp | DoMagic
This is simple enough, a game engine can be built using this as a fundamental building block. But say the designers wanted to set possible actions using a configuration file, of the following type:
[Actions]
Walk
Attack
Drop
PickUp
DoMagic
[/Actions]
How does one turn a file like this into the Action
type? In other words, how would I dynamically construct types based on config files?
Upvotes: 1
Views: 111
Reputation: 153172
This sounds like a simple question of how to define an abstract data type. For module-level abstraction, you can use Haskell's mechanism for choosing what to export; for package-level abstraction, you can use Haskell's mechanism for choosing which modules to expose. I'll briefly discuss each below.
For module-level abstraction, you would write something like this:
module Action (Action, act, allActions) where
import Environment
import Actor
data Action = Walk | Inspect
-- give some way for other modules to construct actions
allActions :: [Action]
allActions = [Walk, Inspect]
-- give some way for other modules to consume actions
act :: Actor -> Action -> Environment -> Environment
act = undefined -- exercise for the reader
This would be your "configuration file". (I think your proposed configuration file -- which lists actions, but doesn't say what they mean -- is unreasonably minimal.) Other parts of your library -- that is, outside this module boundary -- will be forced to be Action
-agnostic in the sense that they cannot depend on the specific list of actions available in a meaningful way. In particular, they will not be able to pattern match on Action
s or create arbitrary Action
s -- only act
on some collection of actions chosen from allActions
. So you can change the configuration in any way you like provided you maintain the allActions
and act
interface.
Of course, in real projects, it can be a bit unwieldy to have all of the Action
-specific operations in a single module. So when things get a bit bigger, it's probably better to use package-level abstraction. One common pattern in the Haskell community is to have an Internal
module that exports everything, and a normal module that only exports the bits that should be available to consumers:
module Action.Internal (Action(..)) where
data Action = Walk | Inspect
module Action (Action, allActions, act) where
import Action.Internal
import Environment
import Actor
allActions = [Walk, Inspect]
act = undefined
In your package's cabal file, one can then hide the Internal
module from other packages:
library
exposed-modules: Action
Actor
Environment
other-modules: Action.Internal
These days, though, it is becoming much more common to do this the Python way: expose all the modules, with the understanding that consumers that touch the Internal
modules are consenting adults (and in particular understand that they may need to take care of internal invariants, that the API could change unpredictably, etc.).
Upvotes: 1