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