Basic Usage
Step-by-step introduction to Result and Maybe
This hands-on tutorial will teach you how to use NOPE-PRO's core features through practical examples.
Tutorial Overview
We'll build a simple inventory system that demonstrates:
- Creating and using
Result<T,E>
- Handling errors gracefully
- Using
Maybe<T>
for optional values - Chaining operations
- Visual debugging
Part 1: Setting Up
First, create a new script called InventoryManager.cs
:
1using UnityEngine;
2using System.Collections.Generic;
3using NOPE.Runtime.Core;
4using NOPE.Runtime.Core.Result;
5using NOPE.Runtime.Core.Maybe;
6using NOPE.PRO.VisualDebugger;
7
8public class InventoryManager : MonoBehaviour
9{
10 [System.Serializable]
11 public class Item
12 {
13 public string id;
14 public string name;
15 public int quantity;
16 public float weight;
17 }
18
19 private Dictionary<string, Item> inventory = new Dictionary<string, Item>();
20 private const float MAX_WEIGHT = 100f;
21
22 void Start()
23 {
24 // We'll add our code here
25 }
26}
Part 2: Your First Result Method
Let's create a method to add items to the inventory:
1// Traditional approach (DON'T DO THIS!)
2public bool AddItemTraditional(Item item)
3{
4 try
5 {
6 if (item == null) throw new ArgumentNullException();
7 if (item.quantity <= 0) throw new ArgumentException("Invalid quantity");
8 if (GetTotalWeight() + item.weight > MAX_WEIGHT) return false;
9
10 inventory[item.id] = item;
11 return true;
12 }
13 catch
14 {
15 return false;
16 }
17}
18
19// NOPE-PRO approach (DO THIS!)
20public Result<Item, string> AddItem(Item item)
21{
22 // Validation chain
23 return ValidateItem(item)
24 .Ensure(i => i.quantity > 0, "Quantity must be positive")
25 .Ensure(i => GetTotalWeight() + i.weight <= MAX_WEIGHT, "Too heavy!")
26 .Map(i => {
27 inventory[i.id] = i;
28 Debug.Log($"Added {i.name} x{i.quantity}");
29 return i;
30 });
31}
32
33private Result<Item, string> ValidateItem(Item item)
34{
35 if (item == null)
36 return "Item cannot be null";
37
38 if (string.IsNullOrEmpty(item.id))
39 return "Item must have an ID";
40
41 return item; // Implicit conversion to Success
42}
43
44private float GetTotalWeight()
45{
46 float total = 0;
47 foreach (var item in inventory.Values)
48 total += item.weight * item.quantity;
49 return total;
50}
Part 3: Using the Result
Now let's use our AddItem method:
1void Start()
2{
3 // Create test items
4 var sword = new Item { id = "sword_01", name = "Iron Sword", quantity = 1, weight = 5f };
5 var potion = new Item { id = "potion_01", name = "Health Potion", quantity = 10, weight = 0.5f };
6 var heavyArmor = new Item { id = "armor_01", name = "Heavy Armor", quantity = 1, weight = 95f };
7
8 // Add items with visual debugging
9 AddItem(sword)
10 .EnableDebug("AddSword")
11 .Match(
12 onSuccess: item => { Debug.Log($"✅ Successfully added {item.name}"); return Unit.Value; },
13 onFailure: error => { Debug.LogError($"❌ Failed to add sword: {error}"); return Unit.Value; }
14 );
15
16 AddItem(potion)
17 .EnableDebug("AddPotion")
18 .Tap(item => Debug.Log($"Inventory now has {inventory.Count} items"))
19 .Match(
20 onSuccess: _ => Unit.Value, // Do nothing on success
21 onFailure: error => { Debug.LogError(error); return Unit.Value; }
22 );
23
24 // This will fail due to weight limit
25 AddItem(heavyArmor)
26 .EnableDebug("AddHeavyArmor")
27 .MapError(error => $"Armor rejected: {error}")
28 .Match(
29 onSuccess: item => { Debug.Log($"✅ Added {item.name}"); return Unit.Value; },
30 onFailure: error => { Debug.LogWarning(error); return Unit.Value; }
31 );
32}
Part 4: Using Maybe<T>
Let's add a method to find items in the inventory:
1// Find item by ID
2public Maybe<Item> FindItem(string itemId)
3{
4 return inventory.TryGetValue(itemId, out var item)
5 ? Maybe<Item>.From(item)
6 : Maybe<Item>.None;
7}
8
9// Find and modify item quantity
10public Result<Item, string> UseItem(string itemId, int amount)
11{
12 return FindItem(itemId)
13 .ToResult($"Item {itemId} not found")
14 .Ensure(item => item.quantity >= amount, "Not enough items")
15 .Map(item => {
16 item.quantity -= amount;
17 if (item.quantity == 0)
18 inventory.Remove(itemId);
19 return item;
20 });
21}
Using these methods:
1void DemoMaybe()
2{
3 // Try to find an item
4 var maybeSword = FindItem("sword_01");
5
6 // Pattern matching
7 maybeSword.Match(
8 onValue: sword => { Debug.Log($"Found {sword.name}"); return Unit.Value; },
9 onNone: () => { Debug.Log("Sword not found"); return Unit.Value; }
10 );
11
12 // Chain operations
13 FindItem("potion_01")
14 .Where(item => item.quantity > 5) // Filter
15 .Map(item => item.quantity) // Transform
16 .Execute(qty => Debug.Log($"Have {qty} potions"));
17
18 // Use item with error handling
19 UseItem("potion_01", 3)
20 .EnableDebug("UsePotion")
21 .Match(
22 onSuccess: item => { Debug.Log($"Used potion, {item.quantity} remaining"); return Unit.Value; },
23 onFailure: error => { Debug.LogError(error); return Unit.Value; }
24 );
25}
Part 5: Combining Results
Let's create a method that performs multiple operations:
1public Result<string, string> TransferItems(string fromPlayerId, string toPlayerId, string itemId, int quantity)
2{
3 // Combine multiple operations
4 return LoadPlayerInventory(fromPlayerId)
5 .Bind(fromInv => fromInv.RemoveItem(itemId, quantity))
6 .Bind(item => LoadPlayerInventory(toPlayerId)
7 .Bind(toInv => toInv.AddItem(item)))
8 .Map(item => $"Transferred {quantity}x {item.name}");
9}
10
11// Craft item from multiple ingredients
12public Result<Item, string> CraftItem(string recipeId)
13{
14 var recipe = GetRecipe(recipeId);
15
16 // Check all ingredients are available
17 var ingredientResults = recipe.ingredients
18 .Select(ing => FindItem(ing.itemId)
19 .ToResult($"Missing {ing.itemId}")
20 .Ensure(item => item.quantity >= ing.required,
21 $"Need {ing.required} {ing.itemId}"))
22 .ToArray();
23
24 return Result.CombineValues(ingredientResults)
25 .Bind(_ => ConsumeIngredients(recipe.ingredients))
26 .Bind(_ => CreateCraftedItem(recipeId));
27}
Part 6: Async Operations
If you have UniTask enabled, you can use async operations:
1#if NOPE_UNITASK
2using Cysharp.Threading.Tasks;
3
4public async UniTask<Result<SaveData, string>> SaveInventoryAsync()
5{
6 return await Result.Of(() => SerializeInventory(), ex => ex.Message)
7 .EnableDebug("SaveInventory")
8 .Bind(data => SaveToCloudAsync(data))
9 .Tap(data => LogAnalyticsAsync("inventory_saved"))
10 .MapError(error => $"Save failed: {error}");
11}
12
13private string SerializeInventory()
14{
15 return JsonUtility.ToJson(inventory);
16}
17
18private async UniTask<Result<SaveData, string>> SaveToCloudAsync(string data)
19{
20 // Simulate async save
21 await UniTask.Delay(1000);
22
23 if (UnityEngine.Random.value > 0.1f) // 90% success rate
24 return new SaveData { json = data, timestamp = Time.time };
25 else
26 return "Cloud service unavailable";
27}
28#endif
Part 7: Error Recovery Patterns
Here are common patterns for handling errors:
1// Pattern 1: Provide default value
2public Item GetItemOrDefault(string itemId)
3{
4 return FindItem(itemId)
5 .Or(new Item { id = "empty", name = "Empty", quantity = 0, weight = 0 })
6 .Value;
7}
8
9// Pattern 2: Transform errors
10public Result<Item, UserFriendlyError> AddItemUserFriendly(Item item)
11{
12 return AddItem(item)
13 .MapError(error => new UserFriendlyError
14 {
15 Title = "Cannot Add Item",
16 Message = error,
17 Icon = "⚠️"
18 });
19}
20
21// Pattern 3: Retry on failure
22public Result<Item, string> AddItemWithRetry(Item item, int maxRetries = 3)
23{
24 var result = AddItem(item);
25 var attempts = 1;
26
27 while (result.IsFailure && attempts < maxRetries)
28 {
29 Debug.Log($"Retry attempt {attempts}");
30 CleanupInventory(); // Make space
31 result = AddItem(item);
32 attempts++;
33 }
34
35 return result;
36}
37
38// Pattern 4: Fallback chain
39public Result<Item, string> GetItemFromAnywhere(string itemId)
40{
41 return FindItemInInventory(itemId)
42 .OrElse(() => FindItemInBank(itemId))
43 .OrElse(() => FindItemInShop(itemId))
44 .OrElse(() => Result<Item, string>.Failure($"Item {itemId} not found anywhere"));
45}
Part 8: Visual Debugging
Enable visual debugging to see your flows:
1void DebugExample()
2{
3 // Complex operation with full debugging
4 var complexFlow = LoadSaveFile()
5 .EnableDebug("ComplexInventoryFlow")
6 .Bind(ParseInventoryData)
7 .Map(MigrateOldFormat)
8 .Bind(ValidateAllItems)
9 .Tap(data => Debug.Log($"Loaded {data.items.Count} items"))
10 .Bind(ApplyToInventory)
11 .Finally(result => {
12 if (result.IsSuccess)
13 Debug.Log("✅ Inventory loaded successfully");
14 else
15 Debug.LogError($"❌ Failed: {result.Error}");
16
17 return result;
18 });
19}
open Window → NOPE → Flow Debugger for detailed inspection.
Complete Example
Here's a complete working example you can copy and test:
1using UnityEngine;
2using System.Collections.Generic;
3using System.Linq;
4using NOPE.Runtime.Core;
5using NOPE.Runtime.Core.Result;
6using NOPE.Runtime.Core.Maybe;
7using NOPE.PRO.VisualDebugger;
8
9public class InventoryTutorial : MonoBehaviour
10{
11 [System.Serializable]
12 public class Item
13 {
14 public string id;
15 public string name;
16 public int quantity;
17 public float weight;
18 }
19
20 private Dictionary<string, Item> inventory = new Dictionary<string, Item>();
21 private const float MAX_WEIGHT = 100f;
22
23 void Start()
24 {
25 Debug.Log("=== NOPE-PRO Inventory Tutorial ===");
26
27 // Demo all features
28 DemoBasicUsage();
29 DemoMaybeUsage();
30 DemoErrorHandling();
31 DemoCombining();
32 }
33
34 void DemoBasicUsage()
35 {
36 Debug.Log("\n--- Basic Usage ---");
37
38 var sword = new Item { id = "sword_01", name = "Iron Sword", quantity = 1, weight = 5f };
39
40 AddItem(sword)
41 .EnableDebug("AddSword")
42 .Match(
43 onSuccess: item => { Debug.Log($"✅ Added {item.name}"); return Unit.Value; },
44 onFailure: error => { Debug.LogError($"❌ {error}"); return Unit.Value; }
45 );
46 }
47
48 void DemoMaybeUsage()
49 {
50 Debug.Log("\n--- Maybe Usage ---");
51
52 FindItem("sword_01")
53 .Map(item => item.name)
54 .Execute(name => Debug.Log($"Found item: {name}"))
55 .ExecuteNoValue(() => Debug.Log("Item not found"));
56 }
57
58 void DemoErrorHandling()
59 {
60 Debug.Log("\n--- Error Handling ---");
61
62 var invalidItem = new Item { id = "", name = "Invalid", quantity = -5, weight = 10f };
63
64 AddItem(invalidItem)
65 .EnableDebug("AddInvalid")
66 .MapError(e => $"Expected error: {e}")
67 .Match(
68 onSuccess: _ => { Debug.LogError("Should have failed!"); return Unit.Value; },
69 onFailure: error => { Debug.Log(error); return Unit.Value; }
70 );
71 }
72
73 void DemoCombining()
74 {
75 Debug.Log("\n--- Combining Results ---");
76
77 var potion = new Item { id = "potion_01", name = "Health Potion", quantity = 5, weight = 0.5f };
78 var shield = new Item { id = "shield_01", name = "Wooden Shield", quantity = 1, weight = 8f };
79
80 var results = Result.CombineValues(
81 AddItem(potion),
82 AddItem(shield)
83 );
84
85 results
86 .EnableDebug("AddMultiple")
87 .Match(
88 onSuccess: items => {
89 var (p, s) = items;
90 Debug.Log($"✅ Added both {p.name} and {s.name}");
91 return Unit.Value;
92 },
93 onFailure: error => { Debug.LogError($"❌ {error}"); return Unit.Value; }
94 );
95 }
96
97 // Core methods
98 public Result<Item, string> AddItem(Item item)
99 {
100 return ValidateItem(item)
101 .Ensure(i => i.quantity > 0, "Quantity must be positive")
102 .Ensure(i => GetTotalWeight() + (i.weight * i.quantity) <= MAX_WEIGHT, "Too heavy!")
103 .Map(i => {
104 if (inventory.ContainsKey(i.id))
105 inventory[i.id].quantity += i.quantity;
106 else
107 inventory[i.id] = i;
108 return i;
109 });
110 }
111
112 private Result<Item, string> ValidateItem(Item item)
113 {
114 if (item == null)
115 return "Item cannot be null";
116
117 if (string.IsNullOrEmpty(item.id))
118 return "Item must have an ID";
119
120 return item;
121 }
122
123 public Maybe<Item> FindItem(string itemId)
124 {
125 return inventory.TryGetValue(itemId, out var item)
126 ? Maybe<Item>.From(item)
127 : Maybe<Item>.None;
128 }
129
130 private float GetTotalWeight()
131 {
132 return inventory.Values.Sum(item => item.weight * item.quantity);
133 }
134}
Summary
You've learned how to:
- ✅ Create and use
Result<T,E>
for error handling - ✅ Use
Maybe<T>
for optional values - ✅ Chain operations with Map, Bind, and Tap
- ✅ Handle errors with Match and MapError
- ✅ Combine multiple Results
- ✅ Enable visual debugging
- ✅ Apply common error recovery patterns
Practice Exercises
- Exercise 1: Modify AddItem to support stack limits (max 99 per stack)
- Exercise 2: Create a method to sort inventory by weight
- Exercise 3: Implement item trading between two inventories
- Exercise 4: Add item categories and filter methods using Maybe