The Unity-Monads project introduces a garbage-free implementation of the Result Monad and an optional DataMediator, designed for use in Unity projects. This project follows the principles of Railway Oriented Programming (ROP) to achieve clean, reliable, and decoupled code while optimizing for performance.
-
Garbage-Free Unity Code
- Critical for performance-sensitive Unity projects, where reducing garbage collection (GC) avoids frame drops and lag.
- The implementation avoids boxing/unboxing entirely, which eliminates GC overhead often caused by interfaces.
-
Result<T> and Result Monads
- Introduces two distinct classes:
Result
for operations with no success value.Result<T>
for operations returning a success value.
- While both represent success or failure states, splitting them avoids the garbage generated by shared interfaces like
ISingleMessage<T>
in other libraries.
- Introduces two distinct classes:
-
Railway Oriented Programming (ROP)
- Inspired by functional programming practices (e.g., F#'s ROP).
- ROP focuses on clear, linear flows of success and failure, making code easy to write, read, and debug.
- Reference: F# for Fun and Profit - ROP.
-
Decoupled System Code
- The optional DataMediator allows you to disconnect system logic from GameObject callers.
- This is similar to an EventBus or Mediator pattern but optimized to work seamlessly with the
Result<T>
monads.
-
Option<T> Operations Built-In
- Null safety is inherently supported by treating
null
success values as failures (usingNullReference
).
- Null safety is inherently supported by treating
-
Iterative and Digestible Code
- Designed for single developers or teams to build iteratively.
- ROP makes it easy to revisit handlers and understand their behavior without extensive comments.
In Unity, garbage collection (GC) can significantly impact performance, especially in scenarios where garbage is generated every frame. This can cause visible stuttering, dropped frames, and lag.
The Result Monads and DataMediator in this project are optimized to:
- Minimize heap allocations.
- Eliminate boxing/unboxing.
- Operate in a garbage-free manner when used correctly.
Note: Using lambda functions in the provided extension methods (e.g.,
OnSuccess
,Map
) will generate small amounts of garbage. This is usually acceptable for occasional calls but should be avoided in per-frame operations.
The project is organized to showcase the Result Monads and DataMediator in both isolated and real-world scenarios.
Unity-Monads/
│
├── Monads/ # Core Result Monad implementation and extensions
│ ├── Result.cs # Result without success values
│ ├── Result_T.cs # Result with success values
│ ├── ResultExtensions.cs # Extension methods for functional usage
│ ├── ResultLinqExtensions.cs # LINQ-like operations optimized for garbage-free execution
│ └── Failures/ # Predefined Failure types (e.g., NotFound, InvalidState)
│
├── Mediator/ # Optional DataMediator implementation
│ ├── DataMediator.cs # Garbage-free 'mediator' pattern
│ └── MediatorMessageAttribute.cs # Tags a struct as a Mediator message
│ └── MediatorHandlerAttribute.cs # Tags a method as a Mediator handler
│
├── Scenes/ # Example scenes showcasing the concepts
│ ├── GarbageTest.unity # Scene to validate garbage-free performance using the Unity Profiler
│ ├── SimpleUses.unity # Basic examples of Result monads and ROP logic
│ └── RealWorldUses.unity # Advanced example combining DataMediator and Result monads
│
└── README.md # Project documentation (you are here)
-
GarbageTest
- A performance test scene showcasing all Monad functions and extensions.
- Use the Unity Profiler to verify that no garbage is generated when the monads are used correctly.
-
SimpleUses
- A straightforward demonstration of Railway Oriented Programming logic using booleans and color effects on sprites.
- Primed for developers new to the Result monad pattern.
-
RealWorldUses
- A comprehensive example demonstrating the combined power of the Result Monads and DataMediator.
- Features:
- Multiple decoupled systems working together.
- Clean separation of concerns.
- Digestible logic once familiar with ROP and the Mediator pattern.
- While intentionally overblown, this example highlights the practical scalability and clarity of the patterns.
using Monads;
// Example: A method that validates user input
Result<string> ValidateInput(string input)
=> string.IsNullOrWhiteSpace(input)
? new InvalidInput("Username", "Username cannot be empty")
: input;
// Usage
var result = ValidateInput("Nova");
result.Match(
success => Debug.Log($"Success: {success}"),
failure => Debug.LogError($"Failure: {failure}")
);
The DataMediator finds all MediatorMessage attributes and connects them to their corresponding handler(s) during startup.
If the method the [MediatorHandler]
attribute is attached to is a void method, the DataMediator automatically connects the message to be an 'event-style' message used with Publish
.
If the method handler returns a value, the DataMediator connects the message to be a 'request-style' message used with Send
.
using Monads;
// Registering a handler
[MediatorHandler]
Result<int> AddNumbers(AddNumbersRequest request)
=> Result.Success(request.A + request.B);
// Sending a message
var result = DataMediator.Instance.Send<AddNumbersRequest, Result<int>>(new AddNumbersRequest(3, 5));
result.Match(
success => Debug.Log($"Result: {success}"),
failure => Debug.LogError($"Error: {failure}")
);
// Message struct
[MediatorMessage]
public readonly struct AddNumbersRequest
{
public int A { get; }
public int B { get; }
public AddNumbersRequest(int a, int b) => (A, B) = (a, b);
}
// our message we want to send
[MediatorMessage]
public readonly struct MoveUnit
{
public readonly GameObject Unit;
public readonly Vector2Int Destination;
public MoveUnit(GameObject unit, Vector2Int destination)
{
Unit = unit;
Destination = destination;
}
}
// sending our message from anywhere in our code
// this happens to live in our PlayerController behaviour which maps user input to messages
DataMediator.Instance
.Send<MoveUnit, Result<MoveUnitResponse>>(new MoveUnit(ControlledUnit, newUnitPosition))
.OnSuccess(response => {
ControlledUnit.transform.position = response.LandingPosition.ToV3();
});
// where we want to process our messages, in a totally separate class called "UnitSystem"
[MediatorHandler]
public Result<MoveUnitResponse> Handle(MoveUnit message)
{
var obstacleDetected = DataMediator.Instance.Send<DetectObstacle, Result<GameObject>>(new DetectObstacle(message.Destination));
if (obstacleDetected)
return new PathBlocked(message.Destination);
var lootDetected = DataMediator.Instance.Send<DetectLoot, Result<GameObject>>(new DetectLoot(message.Destination));
if (lootDetected)
{
DataMediator.Instance.Publish(new GetLoot(
unit: message.Unit,
loot: lootDetected.SuccessValue));
return new MoveUnitResponse(
landingPosition: message.Destination,
actionPoints: AP_PICKUP_LOOT);
}
var unitDetected = DataMediator.Instance.Send<DetectUnit, Result<GameObject>>(new DetectUnit(message.Destination));
if (unitDetected)
{
//TODO: attack other unit, because we moved into their grid position
return new PathBlocked(message.Destination);
}
return new MoveUnitResponse(
landingPosition: message.Destination,
actionPoints: AP_MOVE_ONE);
}