El operador @ permite acceder a una version pasada. La propiedad .history ofrece la linea de tiempo completa. Pero la pregunta temporal mas comun no es "cual era el valor?" -- sino "cambio el valor?" Los desarrolladores no quieren el historial en bruto. Quieren respuestas: Subio el precio? Cuanto? Que porcentaje? Alguna vez tuvo un valor especifico?
Las sesiones 083 a 088 implementaron seis funciones nativas auxiliares que transforman el modelo temporal de FLIN de un mecanismo de almacenamiento en una herramienta analitica. Estas funciones -- field_changed, calculate_delta, percent_change, changed_from, value_changed y field_history -- responden las preguntas que las aplicaciones realmente formulan.
El patron que escribiamos constantemente
Antes de que existieran las funciones auxiliares de comparacion, detectar un cambio de campo requeria un patron de codigo recurrente:
flinold_price = (product @ -1).price
new_price = product.price
price_changed = old_price != new_price
delta = new_price - old_price
pct = (delta / old_price) * 100Cinco lineas para una sola comparacion. Multiplique eso por cada campo que desee monitorear en cada entidad de su aplicacion, y el codigo repetitivo se vuelve abrumador. Peor aun, este patron tiene casos extremos: Que pasa si no hay version anterior? Que pasa si el valor antiguo es cero (division por cero en el cambio porcentual)? Que pasa si el campo contiene una cadena en lugar de un numero?
Las funciones auxiliares de comparacion encapsulan esta logica en llamadas a una sola funcion que manejan correctamente todos los casos extremos.
Las seis funciones
1. field_changed(entity, field_name) -- Deteccion booleana de cambios
La pregunta temporal mas basica: "Cambio este campo desde la ultima version?"
flinentity Product {
name: text
price: float
stock: int
}
product = Product { name: "Widget", price: 50.00, stock: 100 }
save product
product.price = 55.00
save product
field_changed(product, "price") // true
field_changed(product, "name") // false
field_changed(product, "stock") // falseLa implementacion recupera la version anterior usando find_at_version(version - 1), extrae el campo nombrado de ambas versiones (actual y anterior) y los compara usando el metodo existente values_equal().
rustfn native_field_changed(&mut self) -> VMResult<()> {
let field_name_val = self.pop()?;
let entity_val = self.pop()?;
let field_name = match &field_name_val {
Value::Object(id) => self.get_string(*id)?.to_string(),
Value::Text(s) => s.clone(),
_ => return Err(RuntimeError::TypeError { /* ... */ }),
};
// Get current field value
let current_value = get_entity_field(&entity_val, &field_name);
// Get previous version's field value
let previous = self.find_at_version(&type_name, entity_id, version - 1);
let previous_value = previous
.map(|v| v.fields.get(&field_name).cloned())
.flatten();
// Compare
let changed = !self.values_equal_opt(¤t_value, &previous_value);
self.push(Value::Bool(changed));
Ok(())
}Un desafio de diseno fue la extraccion de parametros de texto. En la VM de FLIN, los valores de texto pueden ser Value::Object (una cadena asignada en el heap) o Value::Text (una cadena corta en linea). La funcion debe manejar ambos, siguiendo el patron establecido por native_url_encode():
rustlet field_name = match &field_name_val {
Value::Object(id) => self.get_string(*id)?.to_string(),
Value::Text(s) => s.clone(),
_ => return Err(RuntimeError::TypeError {
expected: "text".to_string(),
found: format!("{:?}", field_name_val),
}),
};Este patron de doble ruta se aplico a las tres funciones que aceptan parametros de texto: field_changed, changed_from y field_history.
2. calculate_delta(old, new) -- Diferencia numerica
Calcula la diferencia aritmetica entre dos valores. Maneja comparaciones entero/entero, flotante/flotante y de tipos mixtos.
flinold_price = (product @ -1).price // 50.00
new_price = product.price // 55.00
delta = calculate_delta(old_price, new_price) // 5.00El tipo de retorno depende de los tipos de entrada: si ambos son enteros, el resultado es un entero. Si alguno es flotante, el resultado es un flotante. Esto preserva la precision del tipo -- un delta entero de 5 es mas util que un delta flotante de 5.0 cuando ambas entradas son enteros.
rustfn native_calculate_delta(&mut self) -> VMResult<()> {
let new_val = self.pop()?;
let old_val = self.pop()?;
let result = match (&old_val, &new_val) {
(Value::Int(old), Value::Int(new)) => Value::Int(new - old),
(Value::Float(old), Value::Float(new)) => Value::Float(new - old),
(Value::Int(old), Value::Float(new)) => Value::Float(new - *old as f64),
(Value::Float(old), Value::Int(new)) => Value::Float(*new as f64 - old),
_ => return Err(RuntimeError::TypeError { /* ... */ }),
};
self.push(result);
Ok(())
}3. percent_change(old, new) -- Diferencia porcentual
Calcula ((new - old) / old) * 100. Siempre devuelve un flotante. Maneja el caso extremo de division por cero: si el valor antiguo es cero y el nuevo tambien es cero, el resultado es cero por ciento. Si el valor antiguo es cero y el nuevo no lo es, la funcion devuelve un error en lugar de infinito.
flinpct = percent_change(50.00, 55.00) // 10.0
pct = percent_change(100, 90) // -10.0
pct = percent_change(0.0, 0.0) // 0.04. changed_from(entity, field_name, expected_value) -- Verificacion del valor anterior
Una pregunta especifica: "El valor anterior de este campo era igual a X?" Util para detectar transiciones especificas.
flin// Did the price just change from $55?
changed_from(product, "price", 55.00) // true
// Did the stock just change from 100?
changed_from(product, "stock", 100) // false (stock unchanged)La implementacion encuentra la version anterior, extrae el campo nombrado y lo compara con el valor esperado usando values_equal().
5. value_changed(entity, field_name) -- Alias de field_changed
Un alias que proporciona una lectura mas natural en ciertos contextos. value_changed(product, "price") se lee como "cambio el valor?" mientras que field_changed(product, "price") se lee como "cambio el campo?" Ambas hacen exactamente lo mismo.
rustfn native_value_changed(&mut self) -> VMResult<()> {
self.native_field_changed() // Direct delegation
}6. field_history(entity, field_name) -- Linea de tiempo de un solo campo
Devuelve una lista de todos los valores historicos para un campo especifico, extraidos del historial completo de versiones. Esto es mas eficiente que .history cuando solo le interesa un campo.
flinprices = field_history(product, "price")
// [50.00, 55.00, 55.00, 60.00]
stocks = field_history(product, "stock")
// [100, 100, 90, 85]La implementacion recupera el historial completo a traves de database.get_history(), itera a traves de cada version, extrae el campo nombrado y construye una lista. El valor de la version actual se agrega al final.
rustfn native_field_history(&mut self) -> VMResult<()> {
let field_name = extract_text(&field_name_val)?;
let entity = extract_entity(&entity_val)?;
let history = self.database
.get_history(&entity.entity_type, entity.id)
.unwrap_or_default();
let mut values = Vec::new();
for version in history {
if let Some(val) = version.fields.get(&field_name) {
values.push(val.clone());
}
}
// Add current value
if let Some(val) = entity.fields.get(&field_name) {
values.push(val.clone());
}
self.push(create_list(values));
Ok(())
}Registro y verificacion de tipos
Las seis funciones se registraron como funciones nativas integradas en la VM con indices consecutivos:
rustregister(self, "field_changed", 2, 61);
register(self, "calculate_delta", 2, 62);
register(self, "percent_change", 2, 63);
register(self, "changed_from", 3, 64);
register(self, "value_changed", 2, 65);
register(self, "field_history", 2, 66);Y tipadas en el verificador de tipos:
rust"field_changed" => FlinType::Function {
params: vec![FlinType::Unknown, FlinType::Text],
ret: Box::new(FlinType::Bool),
min_arity: 2,
has_rest: false,
}
"percent_change" => FlinType::Function {
params: vec![FlinType::Unknown, FlinType::Unknown],
ret: Box::new(FlinType::Float),
min_arity: 2,
has_rest: false,
}
"field_history" => FlinType::Function {
params: vec![FlinType::Unknown, FlinType::Text],
ret: Box::new(FlinType::List(Box::new(FlinType::Unknown))),
min_arity: 2,
has_rest: false,
}El tipo de parametro Unknown se usa para entidades porque las funciones de comparacion funcionan con cualquier tipo de entidad. El verificador de tipos valida la firma de la funcion en los puntos de llamada pero no restringe que tipos de entidad se pueden pasar.
Un ejemplo completo
Aqui hay un patron del mundo real que combina multiples funciones auxiliares de comparacion para construir un panel de analisis de productos:
flinentity Product {
name: text
price: float
stock: int
}
product = Product { name: "Widget", price: 50.00, stock: 100 }
save product
product.price = 55.00
save product
product.stock = 90
save product
product.price = 60.00
product.stock = 85
save product
// Change detection
price_changed = field_changed(product, "price") // true
name_changed = field_changed(product, "name") // false
// Delta calculation
old_price = (product @ -1).price
new_price = product.price
delta = calculate_delta(old_price, new_price) // 5.0
pct = percent_change(old_price, new_price) // 9.09
// Historical analysis
price_timeline = field_history(product, "price") // [50, 55, 55, 60]
stock_timeline = field_history(product, "stock") // [100, 100, 90, 85]
// View integration
<div class="analytics-panel">
<h2>Product Analytics</h2>
<div class="metric">
<h3>Price</h3>
<span class="value">${new_price}</span>
{if price_changed}
<span class="badge green">
Changed: +${delta} ({pct}%)
</span>
{else}
<span class="badge gray">No change</span>
{/if}
</div>
<div class="history">
<h3>Price History</h3>
{for price in price_timeline}
<span class="history-point">${price}</span>
{/for}
</div>
</div>Este panel requeriria una tabla de historial de precios, un sistema de seguimiento de cambios y logica de calculo de porcentajes en un framework tradicional. En FLIN, es un solo componente de pagina sin infraestructura adicional.
Por que funciones nativas en lugar de funciones de biblioteca
Podriamos haber implementado estas funciones auxiliares como funciones de biblioteca de FLIN en lugar de funciones nativas de la VM. Un enfoque de biblioteca usaria la propia sintaxis de FLIN:
flinfn field_changed(entity, field_name: text) -> bool {
old = entity @ -1
if old {
return entity[field_name] != old[field_name]
}
return false
}Elegimos la implementacion nativa por tres razones:
Rendimiento. Las funciones nativas se ejecutan en Rust, accediendo directamente a las estructuras de datos internas de la VM. Una funcion de biblioteca tendria que pasar por el interprete de bytecode, con cada acceso @ y busqueda de campo generando multiples despachos de opcodes. Para funciones auxiliares de comparacion llamadas frecuentemente en bucles (iterando sobre el historial para detectar cambios), la diferencia de rendimiento es significativa.
Manejo de casos extremos. La implementacion nativa puede acceder a los internos de la entidad que el codigo FLIN no puede: el numero de version en bruto, el almacenamiento de historial de la base de datos y el heap de la VM. Esto permite el manejo correcto de casos extremos como entidades no guardadas, entidades destruidas y entidades con una sola version.
Mensajes de error. Las funciones nativas pueden producir mensajes de error precisos que referencian los tipos y valores reales involucrados, en lugar de errores de tiempo de ejecucion genericos de FLIN.
Problemas encontrados
Nombre de metodo duplicado
El primer intento de implementacion creo un metodo values_equal() para comparar tipos Option<Value>. Pero un metodo values_equal(&Value, &Value) ya existia en la VM. El compilador rechazo el duplicado. La solucion fue renombrar el nuevo metodo a values_equal_opt() y hacer que delegara al metodo existente para valores desenvueltos.
Orden de argumentos en la pila
La VM de FLIN empuja los argumentos de funcion de izquierda a derecha pero los extrae de derecha a izquierda. Para changed_from(entity, field, value), el orden de extraccion es: valor primero, campo segundo, entidad tercero. Equivocarse en esto produce errores sutiles donde la entidad se trata como el valor y viceversa -- sin error de tipo, solo resultados incorrectos.
Impacto en el modelo temporal
La sesion 083 llevo TEMP-6 (Comparaciones Temporales) del veinte por ciento al cien por ciento. Combinado con sesiones anteriores, esta fue la septima categoria completada:
| Categoria completada | Tareas |
|---|---|
| TEMP-1: Core Soft Delete | 5/5 |
| TEMP-2: Temporal Access | 18/18 |
| TEMP-3: Temporal Keywords | 14/14 |
| TEMP-4: History Queries | 22/22 |
| TEMP-5: Time Arithmetic | 12/12 |
| TEMP-6: Temporal Comparisons | 10/10 |
| TEMP-11: Integration Tests | 27/27 |
Progreso general: ciento veintiuna de ciento sesenta tareas (setenta y cinco punto seis por ciento). El modelo temporal estaba tres cuartas partes completo.
Las funciones auxiliares de comparacion fueron la ultima pieza necesaria para hacer util el modelo temporal de FLIN para aplicaciones reales. El almacenamiento y acceso son infraestructura. El filtrado y ordenamiento son funciones avanzadas. Pero la deteccion de cambios, el calculo de deltas y las comparaciones porcentuales son la capa analitica que transforma los datos historicos en inteligencia de negocio.
Comparacion con otros enfoques
En los frameworks web tradicionales, la logica de comparacion temporal esta dispersa por toda la aplicacion:
Rails:
ruby# In the model
def price_changed?
previous_version = versions.last&.reify
return false unless previous_version
price != previous_version.price
end
def price_delta
prev = versions.last&.reify&.price || 0
price - prev
end
def price_percent_change
prev = versions.last&.reify&.price
return 0 unless prev && prev > 0
((price - prev).to_f / prev * 100).round(2)
endTres metodos por campo, por modelo. Para una entidad con cinco campos rastreables, eso son quince metodos -- mas pruebas unitarias para cada uno.
Django:
pythondef get_price_change(product):
history = product.history.order_by('-history_date')
if history.count() < 2:
return None
current = history[0]
previous = history[1]
return {
'changed': current.price != previous.price,
'delta': current.price - previous.price,
'percent': ((current.price - previous.price) / previous.price) * 100,
}Una funcion que consulta la tabla de historial, extrae dos registros, realiza calculos y devuelve un diccionario. La logica es correcta pero verbosa, especifica del framework y debe escribirse para cada entidad y campo.
FLIN:
flinfield_changed(product, "price")
calculate_delta(old_price, new_price)
percent_change(old_price, new_price)Tres llamadas a funciones. Sin metodos de modelo. Sin consultas a tablas de historial. Sin manejo de casos extremos. Las funciones trabajan con cualquier entidad, cualquier campo, y manejan todos los casos extremos (versiones faltantes, division por cero, coercion de tipos) internamente.
La diferencia de productividad no es incremental -- es categorica. Lo que toma docenas de lineas en otros frameworks toma una linea en FLIN.
Quinientas cuarenta y seis lineas en tres archivos. Seis funciones. Cero regresiones. Y el modelo temporal de FLIN se convirtio no solo en un sistema de almacenamiento, sino en un motor analitico.
Esta es la Parte 6 de la serie "Como construimos FLIN" sobre el modelo temporal, documentando las funciones auxiliares de comparacion que transforman los datos temporales en perspectivas analiticas.
Navegacion de la serie: - [046] Every Entity Remembers Everything: The Temporal Model - [047] Version History and Time Travel Queries - [048] Temporal Integration: From Bugs to 100% Test Coverage - [049] Destroy and Restore: Soft Deletes Done Right - [050] Temporal Filtering and Ordering - [051] Temporal Comparison Helpers (usted esta aqui) - [052] Version Metadata Access - [053] Time Arithmetic: Adding Days, Comparing Dates - [054] Tracking Accuracy and Validation - [055] The Temporal Model Complete: What No Other Language Has