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