(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}");
}
}
}
An important distinction is events or plain callbacks that requires no return value, which can be implemented quite plainly.
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
.
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.
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:
Top comments (0)