A Lens is one of the most powerful and unique features of τ (tau). It represents a named, reusable transformation or calculation over one or more Schedules.
At its core, a Lens is a virtual, zero-copy view. It doesn't store data itself; instead, it defines an expression that is computed on-the-fly from its input schedules. This makes Lenses incredibly efficient for data exploration, real-time analysis, and schema evolution without costly data duplication or migration.
You can think of a Lens as:
- A database view for temporal data.
- A live data migration that transforms data at read-time.
- A functional transformation applied across time series.
The Structure of a Lens
A Lens is defined by several key fields:
id: A unique[]const u8identifier (ULID).name: A human-readable name for the Lens.description: A brief explanation of what the Lens does.expression: The mathematical or logical formula to be executed.input_names: A list of the names of the schedules or other Lenses this Lens depends on.
1/// A lens represents a calculation or transformation identified by a unique id.
2pub const Lens = struct {
3 id: []const u8,
4 name: []const u8,
5 description: []const u8,
6 expression: []const u8,
7 input_names: std.ArrayList([]const u8),
8
9 // ... methods
10};How Lenses Work
When you apply a Lens, it generates a new, temporary Schedule. The τ engine evaluates the Lens's expression for each time point, using data from the specified input_schedules. The result of each evaluation becomes a new Tau in the output schedule.
1. Creating a Lens
Creating a Lens involves defining its metadata and the core expression.
1const allocator = std.heap.page_allocator;
2
3// Create a lens to calculate the average of two sensors.
4var avg_lens = try tau.primitives.Lens.create(
5 allocator,
6 "average_temp",
7 "Calculates the average of sensor_a and sensor_b.",
8 "(sensor_a + sensor_b) / 2",
9);
10defer avg_lens.deinit(allocator);
11
12// Specify the inputs required by the expression.
13try avg_lens.addInput(allocator, "sensor_a");
14try avg_lens.addInput(allocator, "sensor_b");2. Applying a Lens
The apply method executes the Lens against the data source (represented by the engine). The engine is responsible for fetching the required input schedules. The apply method then returns a new, computed Schedule.
1// 'engine' is an instance of the τ storage engine.
2// It knows how to fetch the 'sensor_a' and 'sensor_b' schedules.
3const avg_schedule = try avg_lens.apply(allocator, engine);
4defer avg_schedule.deinit(allocator);
5
6// avg_schedule now contains Taus where each data point is the
7// average of the corresponding points in 'sensor_a' and 'sensor_b'.Advanced Capabilities
Temporal Offsets
Lenses support a powerful syntax for temporal offsets, allowing you to perform calculations across different points in time within your expression.
- Index-based offset:
variable[-n]refers to the value ofvariablensteps in the past. - Time-based offset:
variable@-5srefers to the value ofvariable5 seconds in the past from the current Tau's timestamp.
This enables complex temporal analyses like calculating moving averages, deltas, or rates of change directly within a Lens.
1// Lens to calculate the change from the previous value.
2var delta_lens = try tau.primitives.Lens.create(
3 allocator,
4 "temp_delta",
5 "Calculates the change in temperature from the previous reading.",
6 "temp - temp[-1]", // temp at current time minus temp at previous index
7);
8defer delta_lens.deinit(allocator);
9
10try delta_lens.addInput(allocator, "temp");
11try delta_lens.addInput(allocator, "temp[-1]");Chaining Lenses
Lenses can depend on other Lenses. This allows you to build up complex, layered calculations from simpler, reusable components. The τ engine automatically resolves these dependencies and detects cyclical references to prevent infinite loops.
Cycle Detection: The apply method tracks visited Lenses to ensure that a Lens doesn't end up referencing itself in a circular dependency (e.g., A -> B -> A), which would cause an error.
Accessing Tau Metadata
Lenses can access the metadata of a Tau within an expression:
vt: Accesses thevalid_ns(valid time in nanoseconds).et: Accesses theexpiry_ns(expiry time in nanoseconds).
This is useful for creating time-aware calculations. For example, you could create a Lens that only includes data from a certain time range.
1// Expression to filter out values older than a specific timestamp
2const time_filter_expr = "if(vt > 1672531200000000000, value, null)";