Chaining Operations Tutorial
Building complex flows
Master the art of composing complex workflows using NOPE-PRO's powerful chaining capabilities.
Understanding Operation Chains
In NOPE-PRO, operations form a pipeline where data flows from one operation to the next, with each operation potentially transforming the data or switching between success and failure tracks.
1Input → [Op1] → [Op2] → [Op3] → Output
2 ↓ ↓ ↓
3 Error Error Error
Basic Chaining Methods
Map - Transform Success Values
Map
transforms the success value without changing the error track:
1public class UserService
2{
3 public Result<UserDto, string> GetUserDto(int userId)
4 {
5 return LoadUser(userId) // Result<User, string>
6 .Map(user => new UserDto // Transform User → UserDto
7 {
8 Id = user.Id,
9 DisplayName = user.FirstName + " " + user.LastName,
10 Level = CalculateLevel(user.Experience)
11 });
12 }
13
14 // Multiple maps can be chained
15 public Result<string, string> GetUserDisplayInfo(int userId)
16 {
17 return LoadUser(userId)
18 .Map(user => user.Profile) // User → Profile
19 .Map(profile => profile.Bio) // Profile → string
20 .Map(bio => bio.Substring(0, 100)); // Truncate
21 }
22
23 private Result<User, string> LoadUser(int userId)
24 {
25 // Simulate loading user from a database or API
26 if (userId <= 0)
27 {
28 return Result<User, string>.Failure("Invalid user ID");
29 }
30
31 // Example user data
32 var user = new User
33 {
34 Id = userId,
35 FirstName = "John",
36 LastName = "Doe",
37 Experience = 1500,
38 Profile = new Profile { Bio = "This is a sample bio for the user." }
39 };
40
41 return Result<User, string>.Success(user);
42 }
43
44 private int CalculateLevel(int experience)
45 {
46 // Simple level calculation based on experience
47 return experience / 1000; // Example: 1000 XP = Level 1, 2000 XP = Level 2, etc.
48 }
49}
Bind - Chain Operations That Return Results
Bind
is used when the next operation also returns a Result:
1public class OrderService
2{
3 public Result<Order, string> ProcessOrder(string orderId)
4 {
5 return LoadOrder(orderId) // Result<Order, string>
6 .Bind(order => ValidateOrder(order)) // Result<Order, string>
7 .Bind(order => CheckInventory(order)) // Result<Order, string>
8 .Bind(order => ChargePayment(order)) // Result<Order, string>
9 .Bind(order => ShipOrder(order)); // Result<Order, string>
10 }
11
12 private Result<Order, string> ValidateOrder(Order order)
13 {
14 if (order.Items.Count == 0)
15 return "Order has no items";
16
17 if (order.TotalAmount <= 0)
18 return "Invalid order amount";
19
20 return order;
21 }
22
23 private Result<Order, string> CheckInventory(Order order)
24 {
25 foreach (var item in order.Items)
26 {
27 if (!IsInStock(item.ProductId, item.Quantity))
28 return $"Item {item.ProductId} is out of stock";
29 }
30 return order;
31 }
32
33 private Result<Order, string> ChargePayment(Order order)
34 {
35 if (!ProcessPayment(order.TotalAmount))
36 return "Payment failed";
37
38 return order;
39 }
40
41 private Result<Order, string> ShipOrder(Order order)
42 {
43 if (!Ship(order))
44 return "Shipping failed";
45
46 return order;
47 }
48
49 private Result<Order, string> LoadOrder(string orderId)
50 {
51 // Simulate loading order from a database or file
52 var order = new Order
53 {
54 OrderId = orderId,
55 Items = new List<OrderItem>
56 {
57 new OrderItem { ProductId = "prod1", Quantity = 2 },
58 new OrderItem { ProductId = "prod2", Quantity = 1 }
59 },
60 TotalAmount = 100.0m
61 };
62 return order;
63 }
64
65 private bool IsInStock(string productId, int quantity)
66 {
67 // Simulate inventory check
68 return true; // Assume all items are in stock for simplicity
69 }
70
71 private bool ProcessPayment(decimal amount)
72 {
73 // Simulate payment processing
74 return true; // Assume payment is always successful for simplicity
75 }
76
77 private bool Ship(Order order)
78 {
79 // Simulate shipping
80 Debug.Log($"Order {order.OrderId} shipped successfully.");
81 return true; // Assume shipping is always successful for simplicity
82 }
83}
Tap - Side Effects
Tap
executes side effects without changing the Result:
1public Result<GameState, string> SaveGame(GameState state)
2{
3 return ValidateGameState(state)
4 .Tap(s => Debug.Log($"Saving game at level {s.Level}"))
5 .Tap(s => BackupCurrentSave())
6 .Map(s => SerializeState(s))
7 .Bind(json => WriteToFile(json))
8 .Tap(_ => Debug.Log("Save completed"))
9 .Map(_ => state); // Return original state
10}
Ensure - Add Validation
Ensure
adds validation that can convert success to failure:
1public Result<Purchase, string> ProcessPurchase(Purchase purchase)
2{
3 return ValidatePurchase(purchase)
4 .Ensure(p => p.Amount > 0, "Amount must be positive")
5 .Ensure(p => p.Amount <= 10000, "Amount exceeds maximum")
6 .Ensure(p => IsValidCurrency(p.Currency), "Invalid currency")
7 .Ensure(p => !IsBlacklisted(p.UserId), "User is blacklisted")
8 .Bind(p => ProcessPayment(p));
9}
Advanced Chaining Patterns
Pattern 1: Transform and Validate
1public class DataProcessor
2{
3 public Result<ProcessedData, string> ProcessRawData(string rawData)
4 {
5 return ParseRawData(rawData)
6 .Map(data => CleanData(data))
7 .Ensure(data => data.Records.Any(), "No records found")
8 .Map(data => EnrichData(data))
9 .Ensure(data => data.IsValid, "Data validation failed")
10 .Bind(data => SaveProcessedData(data));
11 }
12
13 private Result<RawData, string> ParseRawData(string raw)
14 {
15 return Result.Of(
16 () => JsonConvert.DeserializeObject<RawData>(raw),
17 ex => $"Parse error: {ex.Message}"
18 );
19 }
20
21 private CleanedData CleanData(RawData data)
22 {
23 return new CleanedData
24 {
25 Records = data.Records
26 .Where(r => !string.IsNullOrEmpty(r.Id))
27 .Select(r => new CleanRecord
28 {
29 Id = r.Id.Trim(),
30 Value = SanitizeValue(r.Value),
31 Timestamp = r.Timestamp ?? DateTime.Now
32 })
33 .ToList()
34 };
35 }
36}
Pattern 2: Conditional Branching
1public class PaymentProcessor
2{
3 public Result<PaymentResult, string> ProcessPayment(PaymentRequest request)
4 {
5 return ValidateRequest(request)
6 .Bind(req =>
7 {
8 // Branch based on payment method
9 return req.Method switch
10 {
11 PaymentMethod.CreditCard => ProcessCreditCard(req),
12 PaymentMethod.PayPal => ProcessPayPal(req),
13 PaymentMethod.Crypto => ProcessCrypto(req),
14 _ => Result<PaymentResult, string>.Failure("Unsupported payment method")
15 };
16 })
17 .Tap(result => LogPayment(result))
18 .Map(result =>
19 {
20 // Apply discounts after successful payment
21 if (request.HasDiscount)
22 result.FinalAmount *= 0.9m;
23 return result;
24 });
25 }
26}
Pattern 3: Accumulating Results
1public class ValidationPipeline
2{
3 public Result<ValidationReport, string> ValidateCompletely(DataPackage package)
4 {
5 var report = new ValidationReport();
6
7 return ValidateStructure(package)
8 .Tap(_ => report.StructureValid = true)
9 .Bind(_ => ValidateReferences(package))
10 .Tap(_ => report.ReferencesValid = true)
11 .Bind(_ => ValidateBusinessRules(package))
12 .Tap(_ => report.BusinessRulesValid = true)
13 .Bind(_ => ValidatePermissions(package))
14 .Tap(_ => report.PermissionsValid = true)
15 .Map(_ => report)
16 .MapError(error =>
17 {
18 report.Error = error;
19 return $"Validation failed: {error}";
20 });
21 }
22}
Pattern 4: Parallel Processing with Combination
1public class ParallelProcessor
2{
3 public Result<CombinedResult, string> ProcessInParallel(InputData input)
4 {
5 // Start independent operations
6 var analysisResult = AnalyzeData(input);
7 var validationResult = ValidateData(input);
8 var enrichmentResult = EnrichData(input);
9
10 // Combine results
11 return Result.CombineValues(analysisResult, validationResult, enrichmentResult)
12 .Map(tuple => new CombinedResult
13 {
14 Analysis = tuple.Item1,
15 Validation = tuple.Item2,
16 Enrichment = tuple.Item3
17 })
18 .Ensure(combined => combined.IsConsistent(),
19 "Combined results are inconsistent");
20 }
21}
Complex Chain Examples
Example 1: Multi-Stage Data Pipeline
1public class DataPipeline
2{
3 public Result<Report, PipelineError> GenerateReport(string dataSourceId)
4 {
5 return LoadDataSource(dataSourceId)
6 .EnableDebug("ReportGeneration")
7 .MapError(e => new PipelineError(PipelineStage.Loading, e))
8 .Bind(source => ExtractData(source)
9 .MapError(e => new PipelineError(PipelineStage.Extraction, e)))
10 .Bind(data => TransformData(data)
11 .MapError(e => new PipelineError(PipelineStage.Transformation, e)))
12 .Bind(transformed => ValidateData(transformed)
13 .MapError(e => new PipelineError(PipelineStage.Validation, e)))
14 .Map(validated => AggregateData(validated))
15 .Map(aggregated => GenerateReport(aggregated))
16 .Tap(report => CacheReport(report))
17 .Ensure(report => report.DataPoints > 0,
18 new PipelineError(PipelineStage.Generation, "Empty report"));
19 }
20
21 public class PipelineError
22 {
23 public PipelineStage Stage { get; }
24 public string Details { get; }
25
26 public PipelineError(PipelineStage stage, string details)
27 {
28 Stage = stage;
29 Details = details;
30 }
31
32 public override string ToString() => $"[{Stage}] {Details}";
33 }
34}
Example 2: Game Quest System
1public class QuestSystem
2{
3 public Result<QuestReward, string> CompleteQuest(Player player, string questId)
4 {
5 return FindQuest(questId)
6 .Bind(quest => ValidateQuestCompletion(player, quest))
7 .Tap(quest => LogQuestCompletion(player.Id, quest.Id))
8 .Bind(quest => CalculateRewards(player, quest))
9 .Tap(rewards => ApplyRewards(player, rewards))
10 .Bind(rewards => UnlockNextQuests(player, questId)
11 .Map(_ => rewards))
12 .Tap(rewards => NotifyPlayer(player, rewards))
13 .Finally(result =>
14 {
15 // Always save progress, even on failure
16 SavePlayerProgress(player);
17 return result;
18 });
19 }
20
21 private Result<Quest, string> ValidateQuestCompletion(Player player, Quest quest)
22 {
23 return Result.Success<Quest, string>(quest)
24 .Ensure(q => player.Level >= q.RequiredLevel,
25 $"Required level: {quest.RequiredLevel}")
26 .Ensure(q => HasCompletedPrerequisites(player, q),
27 "Prerequisites not met")
28 .Ensure(q => CheckObjectives(player, q),
29 "Quest objectives not completed");
30 }
31
32 private Result<QuestReward, string> CalculateRewards(Player player, Quest quest)
33 {
34 var baseReward = quest.BaseReward;
35
36 return ApplyBonuses(player, baseReward)
37 .Map(reward => ApplyDifficultyMultiplier(reward, player.Difficulty))
38 .Map(reward => ApplyFirstTimeBonus(reward, player, quest))
39 .Ensure(reward => reward.Experience > 0 || reward.Gold > 0,
40 "Invalid reward calculation");
41 }
42}
Example 3: Authentication Flow
1public class AuthenticationService
2{
3 public async UniTask<Result<AuthSession, AuthError>> AuthenticateAsync(
4 string username,
5 string password)
6 {
7 return await ValidateCredentials(username, password)
8 .Bind(creds => CheckUserExistsAsync(creds.Username))
9 .Bind(user => VerifyPasswordAsync(user, password))
10 .Ensure(user => !user.IsLocked,
11 new AuthError(AuthErrorType.AccountLocked))
12 .Ensure(user => user.EmailVerified,
13 new AuthError(AuthErrorType.EmailNotVerified))
14 .Bind(user => CreateSessionAsync(user))
15 .Tap(session => LogAuthenticationAsync(session))
16 .Tap(session => SendLoginNotificationAsync(session))
17 .MapError(error =>
18 {
19 LogFailedAuthentication(username, error);
20 return error;
21 })
22 .EnableDebug($"Auth_{username}");
23 }
24
25 private Result<Credentials, AuthError> ValidateCredentials(
26 string username,
27 string password)
28 {
29 if (string.IsNullOrWhiteSpace(username))
30 return new AuthError(AuthErrorType.InvalidUsername);
31
32 if (password.Length < 8)
33 return new AuthError(AuthErrorType.WeakPassword);
34
35 return new Credentials { Username = username, Password = password };
36 }
37}
Chaining with Maybe
Maybe values can also be chained:
1public class InventorySystem
2{
3 public string GetItemDescription(string itemId)
4 {
5 return FindItem(itemId) // Maybe<Item>
6 .Map(item => item.Details) // Maybe<ItemDetails>
7 .Map(details => details.Description) // Maybe<string>
8 .Map(desc => LocalizeText(desc)) // Maybe<string>
9 .Or("No description available"); // string
10 }
11
12 public Maybe<CraftingResult> TryCrafting(string recipeId, Inventory inventory)
13 {
14 return FindRecipe(recipeId) // Maybe<Recipe>
15 .Where(recipe => HasIngredients(inventory, recipe))
16 .Map(recipe => ConsumeIngredients(inventory, recipe))
17 .Map(recipe => CreateItem(recipe.OutputItemId))
18 .Tap(result => LogCrafting(result));
19 }
20}
Building Custom Chain Extensions
Create your own chain methods:
1public static class CustomResultExtensions
2{
3 // Retry operation N times
4 public static Result<T, E> Retry<T, E>(
5 this Result<T, E> result,
6 Func<Result<T, E>> operation,
7 int maxAttempts)
8 {
9 var currentResult = result;
10 var attempts = 1;
11
12 while (currentResult.IsFailure && attempts < maxAttempts)
13 {
14 currentResult = operation();
15 attempts++;
16 }
17
18 return currentResult;
19 }
20
21 // Log with context
22 public static Result<T, E> LogContext<T, E>(
23 this Result<T, E> result,
24 string context)
25 {
26 return result.Tap(
27 value => Debug.Log($"[{context}] Success: {value}"),
28 error => Debug.LogError($"[{context}] Error: {error}")
29 );
30 }
31
32 // Time operation
33 public static Result<T, E> Timed<T, E>(
34 this Result<T, E> result,
35 out float elapsedMs)
36 {
37 var stopwatch = System.Diagnostics.Stopwatch.StartNew();
38 var timedResult = result;
39 stopwatch.Stop();
40 elapsedMs = (float)stopwatch.Elapsed.TotalMilliseconds;
41 return timedResult;
42 }
43}
44
45// Usage
46var result = LoadData()
47 .Retry(() => LoadData(), maxAttempts: 3)
48 .LogContext("DataLoading")
49 .Timed(out var elapsed)
50 .Tap(_ => Debug.Log($"Operation took {elapsed}ms"));
Performance Tips
1. Avoid Unnecessary Allocations
1// Bad - creates intermediate objects
2public Result<string, string> ProcessName(string input)
3{
4 return Result.Success<string, string>(input)
5 .Map(s => s.Trim())
6 .Map(s => s.ToLower())
7 .Map(s => s.Replace(" ", "-"));
8}
9
10// Good - combine operations
11public Result<string, string> ProcessName(string input)
12{
13 return Result.Success<string, string>(input)
14 .Map(s => s.Trim().ToLower().Replace(" ", "-"));
15}
2. Use Ensure for Multiple Validations
1// Bad - multiple Ensure calls
2result
3 .Ensure(x => x.A > 0, "A must be positive")
4 .Ensure(x => x.B > 0, "B must be positive")
5 .Ensure(x => x.C > 0, "C must be positive");
6
7// Good - combine validations
8result.Ensure(x => x.A > 0 && x.B > 0 && x.C > 0,
9 "All values must be positive");
3. Short-Circuit Expensive Operations
1public Result<Data, string> ProcessExpensiveData(Input input)
2{
3 // Quick validations first
4 return ValidateInput(input) // Fast
5 .Ensure(i => !cache.Contains(i.Id), "Already processed") // Fast
6 .Bind(i => ExpensiveOperation(i)) // Only runs if previous succeed
7 .Bind(d => AnotherExpensiveOperation(d));
8}
Debugging Chains
Use visual debugging to understand complex chains:
1public Result<Output, string> ComplexChain(Input input)
2{
3 return Step1(input)
4 .EnableDebug("ComplexChain") // Enable visual tracking
5 .Bind(r1 => Step2(r1))
6 .Map(r2 => Transform(r2))
7 .Ensure(r3 => Validate(r3), "Validation failed")
8 .Bind(r4 => Step3(r4))
9 .Finally(result =>
10 {
11 // Inspect the flow in the debugger
12 Debug.Log($"Chain completed: {result.IsSuccess}");
13 return result;
14 });
15}
Summary
You've learned:
- ✅ Core chaining methods (Map, Bind, Tap, Ensure)
- ✅ Advanced chaining patterns
- ✅ Building complex workflows
- ✅ Chaining with Maybe types
- ✅ Creating custom extensions
- ✅ Performance optimization