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

  1. Exercise 1: Modify AddItem to support stack limits (max 99 per stack)
  2. Exercise 2: Create a method to sort inventory by weight
  3. Exercise 3: Implement item trading between two inventories
  4. Exercise 4: Add item categories and filter methods using Maybe