14

This is mostly a theoretical question about FP, but I'll take text adventures (like old-school Zork) to illustrate my point. I'd like to know your opinions on how would you model a stateful simulation with FP.

Text adventures really seem to call for OOP. For example, all the "rooms" are instances of a Room class, you can have a basic Item class and interfaces like Item<Pickable> for things you can carry and so on.

World modeling in FP works differently, especially if you want to enforce immutability in a world that must mutate as the game progresses (objects are moved, enemies are defeated, the scoring grows, the player changes its location). I imagine a single big object World that has it all: what are the rooms you can explore, how they are linked, what the player is carrying, what levers have been triggered.

I think that a pure approach would be to basically pass this big object to any function and have it returned by them (possibly modified). For example, i have a moveToRoom function that gets World and returns it with World.player.location changed to the new room, World.rooms[new_room].visited = True and so on.

Even if this is the more "correct" way, it seems to be enforcing pureness for the sake of it. Depending on the programming language, passing this potentially very big World object back and forth may be expensive. Also, every function may need to have access to any World object. For example, a room may be accessible or not depending on a lever triggered in another room because it may be flooded, but if the player carries a life jacket, it can enter it anyway. A monster may be aggressive or not depending on whether the player has slayered his cousin in another room . This means that the roomCanBeEntered function needs to access World.player.invetory and World.rooms, describeMonster needs to access World.monsters and so on (basically, you must pass the whole load around). This really seems to me to call for a global variable, even if this is all but good programming style especially in FP.

How would you solve this problem?

6
  • 4
    "Depending on the programming language, passing this potentially very big World object back and forth may be expensive." It'll probably be passed by reference. "Also, every function may need to have access to any World object." I find it difficult to believe every function needs access to the whole state of the game.
    – Doval
    May 8, 2015 at 18:00
  • 2
    I think Chris Marten's research would be interesting, it aims to show how to make interactive fiction in declarative languages nice. github.com/chrisamaphone/interactive-lp May 8, 2015 at 18:15
  • 2
    You may want to have a look at this blog describing the author's approach to programming such a game in a functional manner. This post in particular is quite on point.
    – gallais
    May 11, 2015 at 13:40
  • 3
    I have to wonder whether this question influenced @EricLippert's later decision to write a series of articles about implementing (parts of) a Z machine in Ocaml...?
    – Jules
    Jul 28, 2016 at 22:35
  • 1
    @Jules A link to the start of that series, for those interested: ericlippert.com/2016/02/01/west-of-house
    – KChaloux
    Feb 28, 2019 at 15:29

3 Answers 3

4

Mind that functional languages use data structures and separated functions instead of objects. For example you would have a set of rooms and a list of inventory items as a world instead.

Ideally you would also limit the amount of data you give to functions to how much they actually require as much as possible instead of passing the entire world (say you extract a single relevant room from your world; of course completely interdependent worlds can be hard to separate). The result would be reincorporated into the world data structure, creating a new state. You cannot model state without using state; as you say some things inherently require mutation.

Most practical functional languages provide a way to realize mutation either directly (say the ST monad in Haskell or transients in Clojure), or simulate it efficiently (often by reusing unchanged parts of the data structure (Clojure's default data structures are a good example here)). Either way purity is maintained.

Since the amount of state that needs to be mutated seems limited, I wouldn't worry too much about performance issues and go with the (possibly already optimized) naive functional approach.

A different option I have seen would be returning only instructions to change some part of the world from your individual functions, and then updating your world according to these. A series of blog posts describing this is available at http://prog21.dadgum.com/23.html.

Both of these answers deal more with how to organize changes than to not pass your entire world to functions, because a perfectly interdependent one cannot be segmented by definition - but try and do so as best as you can in your case, which is not only functional, but also good practice.

0

Myself, I would definitely look into the ability of the language in question to access some form of database. Most of the events that happen to change the state of the world concurrently would simply be recorded to disk, and would not affect the current player within the current room (outside of special circumstances such as explosions, or in an MMO, switches that open doors remotely, other players' shouts, etc).

As such, the current client really only needs to be aware of the Room object, and things that affect it directly. noticableEventsOutsideRoom could then simply be a subclass of Room affected by recent changes to the database, and your game object has become much smaller.

1
  • I understand this approach doesn't do much for pathfinding or triggering local events (such as agro on nearby mobs), but I've been known to abuse databases in the past... I'd probably just send a call to update mobs set agro=1 where distance<5 and be done with it. Maybe that's not best practice, but it suits my purposes. As for pathfinding via database, one could always use Dijkstra’s shortest path algorithm...
    – Ayelis
    May 15, 2015 at 0:55
0

The real solution is not by collecting everything to a big World object, and then passing it around. Instead, you're recommended to accurately specify the type of the function you're dealing with. Here's some examples:

BAD:
   f :: (World, Int) -> World

Good:
   f :: (Int,Int,Int,Int) -> World

The bad example is trying to modify existing object, but the good example is trying to create a world from the state space your game has. Basically, to create a world, you need to know all the data that is required to select the correct world.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.