Lets discuss what are we trying to achieve by applying functional programming to game development

What are our goals?

Artūras Šlajus

2022

Before we dive in...

Let's discuss what we want to achieve with all of these techniques.

We want these things:

  • Runtime stability: things should not crash in runtime.
     
  • Compile-time correctness: if it compiles, it works (correctly)!
     
  • Understandability: code should be easy to read and understand for everyone.
     
  • Maintainability: changing the code later shouldn't break things.
     
  • Testability: testing should be easy and not require various trickery.

Runtime stability

Runtime stability

Why do runtime errors happen?

Runtime errors are always failures in the code.

A runtime error happens when:

  • The programmer assumes in the code that the state of something will be X:
    • A socket will be open for writing.
    • A file will be open for reading.
    • A collection will be non-empty.
    • A state machine will be in a particular state.
    • A reference will not be null.
  • And it just isn't so in runtime.

Runtime stability

Runtime errors are completely avoidable.

In theory, if there was no way to abort the program (either by throwing an exception or just killing the process), you would be forced to handle everything.

However, it is often more practical to assume the situation is exceptional and abort the program instead of handling things.

Problems start when ordinary situations are being treated as exceptions.

Runtime stability

Functional programming helps us with this goal in a couple of ways:

  • Functions can return errors as values, making them explicit.

     
  • We can then use functional combinators to make handling them easy.


     
  • We can make illegal states irrepresentable. You can't have an error if it's impossible to construct a data structure representing an illegal state.
Either<IOError, string> readFile(string path);
// instead of
File openFile(string path);
Either<ImmutableList<IOError>, ImmutableList<string>> readFiles(
  IEnumerable<string> paths
) => paths.Select(readFile).validateAll()
// The length of an array is never negative, why is `int` used here?
public uint LengthU<A>(this A[] arr) => (uint) arr.Length;

Compile-time correctness

Compile-time correctness

However, we can avoid some of them by:

  • Using good type signatures that do not lie.


     
Option<A> RandomElement<A>(this IReadOnlyList<A> list);
// instead of
A RandomElement<A>(this IReadOnlyList<A> list)

We can't avoid all logic errors in our code.

Compile-time correctness

 

  • Using good type signatures that do not lie.
  • Constraining our types to enforce invariants.
// If you have an instance of this type, it means that 
// the inner string was a valid Unity asset ID at the 
// time of creation of this structure.
class AssetGuid {
  public readonly string id;
  
  // Private constructor
  AssetGuid(string id) => this.id;
  
  public Either<Error, AssetGuid> create(string id) => 
    /* ... */;
}

However, we can avoid some of them by:

We can't avoid all logic errors in our code.

Compile-time correctness

 

  • Using good type signatures that do not lie.
  • Constraining our types to enforce invariants.
  • Using proof values to enforce ordering.
struct HitPointsWereReplenishedProof {}

HitPointsWereReplenishedProof replenishHitPoints();
void applyAttackDamages(
  HitPointsWereReplenishedProof proof
);

 

  • Using good type signatures that do not lie.
  • Constraining our types to enforce invariants.

However, we can avoid some of them by:

We can't avoid all logic errors in our code.

Compile-time correctness

 

  • Using good type signatures that do not lie.
  • Constraining our types to enforce invariants.
  • Using proof values to enforce ordering.
  • And other techniques that we will be covering in this course.

At the end of the day the goal is simple:

  • Get out of the dreaded write-compile-run-crash-debug-fix
    cycle and replace it with write-compile-run cycle.

 

  • Using good type signatures that do not lie.
  • Constraining our types to enforce invariants.

However, we can avoid some of them by:

We can't avoid all logic errors in our code.

Understandability and maintainability

Understandability and maintainability

Code is written once, but read many times. Often, a lot later.

We need to make sure that anyone after any amount of time would be able to:

  • Read the code and understand its intentions.
  • Be able to modify it without introducing issues.
  • Ideally, without extensive research first.

To do that, we'll cover:

  • Semantic types.
  • Good vs bad function type signatures.
  • Code comments: where, what and when.
  • Type-driven system design.
  • Static traceability: how find usages in IDE is your friend.

Testability

Testability

Tests are important for:

  • Making sure we implemented the right thing.
  • Making sure that implementation will not break over time.

Pure functions are ultimately testable:

DateTime calculateLevelUpTime(
  DateTime now, Level level
) =>
  now + level * 1.minute();
  
// instead of

DateTime calculateLevelUpTime(Level level) =>
  DateTime.Now + level * 1.minute();

Next steps

Next we will start looking into the basic blocks of functional programming.

See you in the next video!

U3. What are our goals?

By Artūras Šlajus

U3. What are our goals?

  • 430