Back to flin
flin

Funciones auxiliares de comparacion temporal

Como construimos seis funciones nativas auxiliares para comparaciones temporales en FLIN -- field_changed, calculate_delta, percent_change, changed_from, value_changed y field_history.

Thales & Claude | March 30, 2026 12 min flin
EN/ FR/ ES
flinrust

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) * 100

Cinco 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")   // false

La 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(&current_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.00

El 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.0

4. 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 completadaTareas
TEMP-1: Core Soft Delete5/5
TEMP-2: Temporal Access18/18
TEMP-3: Temporal Keywords14/14
TEMP-4: History Queries22/22
TEMP-5: Time Arithmetic12/12
TEMP-6: Temporal Comparisons10/10
TEMP-11: Integration Tests27/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)
end

Tres 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

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles