DevLog 20250510 Dealing with Lambda

DevLog 20250510 Dealing with Lambda

BackerLeader posted Originally published at dev.to 2 min read

(The technical term is "Closure")

The essence of lambda calculus is with captures - be it simple values or object references. At the site of anonymous function declaration, it's being captured and (the reference) is saved inside the lambda until being invoked.

// C# – Capturing a local variable in a LINQ query
int factor = 3;
int[] numbers = { 1, 2, 3, 4, 5 };
var scaled = numbers
    .Select(n => n * factor)   // ‘factor’ is captured from the enclosing scope
    .ToArray();
Console.WriteLine(string.Join(", ", scaled));  // 3, 6, 9, 12, 15

In C++ this is more explicit, and the capturing process is more obvious:

// C++20 – Capturing a local variable in a ranges pipeline
#include <iostream>
#include <vector>
#include <ranges>

int main() {
    int factor = 3;
    std::vector<int> numbers = { 1, 2, 3, 4, 5 };

    // Capture ‘factor’ by value in the lambda
    auto scaled = numbers 
        | std::views::transform([factor](int n) { return n * factor; });

    for (int x : scaled)
        std::cout << x << " ";   // 3 6 9 12 15
}

What happens when an object reference is disposed, as in the case of IDisposable? It will simply throw an error.

using System;
using System.IO;

class Program
{
    static void Main()
    {
        // Create and use a MemoryStream
        var ms = new MemoryStream();
        ms.WriteByte(0x42);                // OK: writes a byte

        // Dispose the stream
        ms.Dispose();                      // Unmanaged buffer released

        try
        {
            // Any further operation is invalid
            ms.WriteByte(0x24);            // <-- throws ObjectDisposedException
        }
        catch (ObjectDisposedException ex)
        {
            Console.WriteLine($"Cannot use disposed object: {ex.GetType().Name}");
        }
    }
}

IDisposable capture runtime exception

An important distinction is events or plain callbacks that requires no return value, which can be implemented quite plainly.

Divooka procedural context custom event

To achieve the same in a dataflow context, some kind of GUI (or "graph-native") support is needed, and to the caller, it's clear (in the language of C#) it's taking a delegate as argument, as in the case of LINQ Select.

Example of "Apply" with delegate input

public static System.Collections.Generic.IEnumerable<TResult> Select<TSource,TResult>(this System.Collections.Generic.IEnumerable<TSource> source, Func<TSource,int,TResult> selector);

To implement capturing, the most natural way is to ensure it happens "in-place" - directly on the graph. With this approach, we don’t need specialized nodes for every kind of function; we can instead rely on existing language constructs to handle the rest.

Final implementation

Demo Function

The last bit is actually inspired by Haskell, where functions are first-class and can be composed:

-- A simple two‑argument function
add :: Int -> Int -> Int
add x y = x + y

-- Partially apply 'add' to “capture” the first argument
addFive :: Int -> Int
addFive = add 5

main :: IO ()
main = do
  print (addFive 10)          -- 15
  print (map (add 3) [1,2,3]) -- [4,5,6]

See a demo of usage here, reposted below:

Image description

If you read this far, tweet to the author to show them you care. Tweet a Thanks

Great write-up—really appreciate the blend of C#, C++, and Haskell to explain closures and captures! It made the concept click nicely. One question though: in more complex graphs or reactive systems, how do you suggest managing disposal or lifecycle of captured references safely?

Hi Ben, thanks for the reply! Really great question you have here. As any managed language, the referenced objects eventually gets GC collected unless there is a dangling reference and thus a memory leak. Specifically in the case of captured lambda, it creates a closure and the closure keeps the objects live. If however the object is disposed for some reason (explicitly or due to out of local scope for using in C#), it usually means a runtime exception (ObjectDisposedException in C#) if the underlying object is no longer available.

using (var rw = new ResourceWrapper())
{
    Action a = () => rw.DoWork();
    // … you can call a() here and it will work
}

// Compiler rewrites the above roughly as:
var rw = new ResourceWrapper();
try
{
    Action a = () => rw.DoWork();
    // … 
}
finally
{
    if (rw != null) 
((IDisposable)rw).Dispose();
}
  • At the closing brace of the using, Dispose() is invoked on the instance (here, rw), even though the lambda a still “points” at it.
  • The closure object that holds rw remains alive (and keeps that instance rooted), but the instance itself has already had its cleanup logic run.
  • Any subsequent a() call is simply calling DoWork() on a disposed object - so you’ll get ObjectDisposedException.

Illustration

using (var rw = new ResourceWrapper())
{
    Action a = () => rw.DoWork();
    a();               // ✅ “Working...”
    // end of using → rw.Dispose() is called here
    a();               // ⚠️ ObjectDisposedException
}
// Even though 'a' is still in scope, the resource was disposed

More Posts

DevLog 20250724: C# Scripting, Pure 2, Updated + Some Recap

Methodox - Jul 24

DevLog 20250706: Analyzing (C#) Project Dependencies

Methodox - Jul 6

DevLog 20250816: Divooka Language Specification (Preliminary Look)

Methodox - Aug 16

DevLog 20250805: Divooka Node Graph File Formats

Methodox - Aug 5

DevLog 2025801: Procedural Context Type

Methodox - Aug 1
chevron_left