(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)