TheEnvironmentalist
TheEnvironmentalist

Reputation: 2862

Dynamically-constructed enumerated types in Haskell

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

Answers (1)

Daniel Wagner
Daniel Wagner

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 Actions or create arbitrary Actions -- 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

Related Questions