Skip to main content
added 271 characters in body
Source Link
mikera
  • 20.8k
  • 5
  • 77
  • 80

I find functional programming extremely helpful in managing complexity. You tend to think about complexity in a different way though, defining it as functions that act on immutable data at different levels rather than encapsulation in an OOP sense.

For example, I recently wrote a game in Clojure, and the entire state of the game was defined in a single immutable data structure:

(def starting-game-state {:map ....
                          :player ....
                          :weather ....
                          :other-stuff ....}

And the main game loop could be defined as applying some pure functions to the game state in a loop:

 (loop [initial-state starting-game-state]
   (let [user-input (get-user-input)
         game-state (update-game initial-state user-input)]
     (draw-screen game-state)
     (if-not (game-ended? game-state) (recur game-state))))

The key function called is update-game, which runs a simulation step given a previous game state and some user input, and returns the new game state.

So where's the complexity? In my view it has been managed quite well:

  • Certainly the update-game function does a lot of work, but it is itself built up by composing other functions so it's actually a pretty simple itself. Once you go down a few levels, the functions are still pretty simple, doing something like "add an object to a map tile".
  • Certainly the game state is a big data structure. But again, it's just built up by composing lower level data structures. Also it's "pure data" rather than having any methods embedded or and class definition required (you can think of it as a very efficient immutable JSON object if you like) so there is very little boilerplate.

OOP can also manage complexity through encapsulation, but if you compare this to OOP, the functional has approach some very big advantages:

  • The game state data structure is immutable, so a lot of processing can easily be done in parallel. For example, it's perfectly safe to have a rendering calling draw-screen in a different thread from the game logic - they can't possibly affect each other or see an inconsistent state. This is surprisingly difficult with a big mutable object graph......
  • You can take a snapshot of the game state at any time. Replays are trivial (any thanks to Clojure's persistent data structures, the copies take up hardly any memory since most of the data is shared). You can also run update-game to "predict the future" to help the AI evaluate different moves for example.
  • Nowhere did I have to make any difficult trade-offs to fit into the OOP paradigm, such as defining a rigid class heirarchy. In this sense the functional data structure behaves more like a flexible prototype-based system.

Finally, for people who are interested in more insights on how to manage complexity in functional vs. OOP languages, I strongly reccoomend the video of Rich Hickey's keynote speech Simple Made Easy (filmed at the Strange Loop technology conference)

I find functional programming extremely helpful in managing complexity. You tend to think about complexity in a different way though, defining it as functions that act on immutable data at different levels rather than encapsulation in an OOP sense.

For example, I recently wrote a game in Clojure, and the entire state of the game was defined in a single immutable data structure:

(def starting-game-state {:map ....
                          :player ....
                          :weather ....
                          :other-stuff ....}

And the main game loop could be defined as applying some pure functions to the game state in a loop:

 (loop [initial-state starting-game-state]
   (let [user-input (get-user-input)
         game-state (update-game initial-state user-input)]
     (draw-screen game-state)
     (if-not (game-ended? game-state) (recur game-state))))

The key function called is update-game, which runs a simulation step given a previous game state and some user input, and returns the new game state.

So where's the complexity? In my view it has been managed quite well:

  • Certainly the update-game function does a lot of work, but it is itself built up by composing other functions so it's actually a pretty simple itself. Once you go down a few levels, the functions are still pretty simple, doing something like "add an object to a map tile".
  • Certainly the game state is a big data structure. But again, it's just built up by composing lower level data structures. Also it's "pure data" rather than having any methods embedded or and class definition required (you can think of it as a very efficient immutable JSON object if you like) so there is very little boilerplate.

OOP can also manage complexity through encapsulation, but if you compare this to OOP, the functional has approach some very big advantages:

  • The game state data structure is immutable, so a lot of processing can easily be done in parallel. For example, it's perfectly safe to have a rendering calling draw-screen in a different thread from the game logic - they can't possibly affect each other or see an inconsistent state. This is surprisingly difficult with a big mutable object graph......
  • You can take a snapshot of the game state at any time. Replays are trivial (any thanks to Clojure's persistent data structures, the copies take up hardly any memory since most of the data is shared). You can also run update-game to "predict the future" to help the AI evaluate different moves for example.
  • Nowhere did I have to make any difficult trade-offs to fit into the OOP paradigm, such as defining a rigid class heirarchy. In this sense the functional data structure behaves more like a flexible prototype-based system.

I find functional programming extremely helpful in managing complexity. You tend to think about complexity in a different way though, defining it as functions that act on immutable data at different levels rather than encapsulation in an OOP sense.

For example, I recently wrote a game in Clojure, and the entire state of the game was defined in a single immutable data structure:

(def starting-game-state {:map ....
                          :player ....
                          :weather ....
                          :other-stuff ....}

And the main game loop could be defined as applying some pure functions to the game state in a loop:

 (loop [initial-state starting-game-state]
   (let [user-input (get-user-input)
         game-state (update-game initial-state user-input)]
     (draw-screen game-state)
     (if-not (game-ended? game-state) (recur game-state))))

The key function called is update-game, which runs a simulation step given a previous game state and some user input, and returns the new game state.

So where's the complexity? In my view it has been managed quite well:

  • Certainly the update-game function does a lot of work, but it is itself built up by composing other functions so it's actually a pretty simple itself. Once you go down a few levels, the functions are still pretty simple, doing something like "add an object to a map tile".
  • Certainly the game state is a big data structure. But again, it's just built up by composing lower level data structures. Also it's "pure data" rather than having any methods embedded or and class definition required (you can think of it as a very efficient immutable JSON object if you like) so there is very little boilerplate.

OOP can also manage complexity through encapsulation, but if you compare this to OOP, the functional has approach some very big advantages:

  • The game state data structure is immutable, so a lot of processing can easily be done in parallel. For example, it's perfectly safe to have a rendering calling draw-screen in a different thread from the game logic - they can't possibly affect each other or see an inconsistent state. This is surprisingly difficult with a big mutable object graph......
  • You can take a snapshot of the game state at any time. Replays are trivial (any thanks to Clojure's persistent data structures, the copies take up hardly any memory since most of the data is shared). You can also run update-game to "predict the future" to help the AI evaluate different moves for example.
  • Nowhere did I have to make any difficult trade-offs to fit into the OOP paradigm, such as defining a rigid class heirarchy. In this sense the functional data structure behaves more like a flexible prototype-based system.

Finally, for people who are interested in more insights on how to manage complexity in functional vs. OOP languages, I strongly reccoomend the video of Rich Hickey's keynote speech Simple Made Easy (filmed at the Strange Loop technology conference)

Source Link
mikera
  • 20.8k
  • 5
  • 77
  • 80

I find functional programming extremely helpful in managing complexity. You tend to think about complexity in a different way though, defining it as functions that act on immutable data at different levels rather than encapsulation in an OOP sense.

For example, I recently wrote a game in Clojure, and the entire state of the game was defined in a single immutable data structure:

(def starting-game-state {:map ....
                          :player ....
                          :weather ....
                          :other-stuff ....}

And the main game loop could be defined as applying some pure functions to the game state in a loop:

 (loop [initial-state starting-game-state]
   (let [user-input (get-user-input)
         game-state (update-game initial-state user-input)]
     (draw-screen game-state)
     (if-not (game-ended? game-state) (recur game-state))))

The key function called is update-game, which runs a simulation step given a previous game state and some user input, and returns the new game state.

So where's the complexity? In my view it has been managed quite well:

  • Certainly the update-game function does a lot of work, but it is itself built up by composing other functions so it's actually a pretty simple itself. Once you go down a few levels, the functions are still pretty simple, doing something like "add an object to a map tile".
  • Certainly the game state is a big data structure. But again, it's just built up by composing lower level data structures. Also it's "pure data" rather than having any methods embedded or and class definition required (you can think of it as a very efficient immutable JSON object if you like) so there is very little boilerplate.

OOP can also manage complexity through encapsulation, but if you compare this to OOP, the functional has approach some very big advantages:

  • The game state data structure is immutable, so a lot of processing can easily be done in parallel. For example, it's perfectly safe to have a rendering calling draw-screen in a different thread from the game logic - they can't possibly affect each other or see an inconsistent state. This is surprisingly difficult with a big mutable object graph......
  • You can take a snapshot of the game state at any time. Replays are trivial (any thanks to Clojure's persistent data structures, the copies take up hardly any memory since most of the data is shared). You can also run update-game to "predict the future" to help the AI evaluate different moves for example.
  • Nowhere did I have to make any difficult trade-offs to fit into the OOP paradigm, such as defining a rigid class heirarchy. In this sense the functional data structure behaves more like a flexible prototype-based system.