¿Hay alguna otra forma idiomática de pasar información de estado?

Tengo la necesidad de procesar una secuencia de datos de ticks históricos de un marco de tiempo de milisegundos. Se requiere la capacidad de filtrar en ticks de apertura de ciertos intervalos de tiempo (por hora, minuto, etc.). La secuencia puede tener espacios mayores que el intervalo, por lo que el primer tick después de dicho intervalo debe elegirse como el de apertura; de lo contrario, el tick de apertura es el que está más cerca del comienzo del calendario del intervalo de tiempo correspondiente.

Lo primero que se me ocurre es lo siguiente con estado función de filtrado opensTimespan:Timespan->(Timestamp->bool) que captura el intervalo de tiempo de cada marca de apertura de brecha o apertura de intervalo en un cierre para pasar entre invocaciones:

let opensTimespan (interval: Timespan)=
    let lastTakenId = ref -1L  // Timestamps are positive
    fun (tickAt: Timestamp) -> 
        let tickId = tickAt / interval in
            if tickId <> !lastTakenId then lastTakenId := tickId; true
            else false

y se puede aplicar así:

let hourlyTicks = readTicks @"EURUSD-history.zip" "EURUSD-2012-04.csv"
                  |> Seq.filter (opensTimespan HOUR) |> Seq.toList

Esto funciona bien, pero opensTimespan tener el efecto secundario definitivamente no es idiomático.

Una alternativa puede ser usar el hecho de que la decisión sobre un tick es abrir o no requiere solo el par de marcas de tiempo del yo y el anterior para llegar a lo siguiente apátrida función de filtrado opensTimespanF:Timespan->Timestamp*Timestamp->bool:

let opensTimespanF interval (ticksPair: Timestamp*Timestamp) =
    fst ticksPair/ interval <> snd ticksPair/ interval

que se puede aplicar como:

let hourlyTicks= 
    seq {
        yield 0L;
        yield! readTicks @"EURUSD-history.zip" "EURUSD-2012-04.csv"
    }
    |> Seq.pairwise |> Seq.filter (opensTimespanF HOUR)
    |> Seq.map snd
    |> Seq.toList

Este enfoque, que es puramente funcional, produce resultados equivalentes con solo una leve penalización de rendimiento (~11 %).

¿Qué otra(s) forma(s) de abordar esta tarea de manera puramente funcional me puede faltar?

Gracias por su atención.

preguntado el 22 de mayo de 12 a las 16:05

Diría que si el efecto secundario está confinado dentro de la función opensTimespan, puede que no sea 'idiomático', pero ciertamente es aceptable para el rendimiento. Las funciones principales de F# hacen lo mismo. -

2 Respuestas

Una solución puramente funcional sería utilizar el fold función. los fold La función se utiliza para procesar una secuencia (o una lista) y acumular algún estado. En su ejemplo, el estado es el lastTakenId y también la lista de elementos que desea devolver, para que pueda usar el estado de tipo Timestamp * (Timestamp list):

let hourlyTicks = 
  readTicks @"EURUSD-history.zip" "EURUSD-2012-04.csv" 
  |> Seq.fold (fun (lastTakenId, res) tickAt ->
      // Similar to the body of your stateful function - 'lastTakenId' is the last
      // state and 'tickAt' is the current value. The 'res' list stores 
      // all returned elements
      let tickId = tickAt / HOUR 
      if tickId <> lastTakenId then  
        // We return new state for 'lastTakenId' and append current element to result
        (tickId, tickAt::res)
      else 
        // Here, we skip element, so we return the original state and original list
        (lastTakenId, res) ) (-1L, []) // Initial state: -1 and empty list of results

  // Take the second part of the state (the result list) and
  // reverse it, because it was accumulated in the opposite order
  |> snd |> List.rev

Aparte, no estoy del todo seguro acerca de su otra solución pura: no creo que haga exactamente lo mismo que la primera (pero no tengo los datos para probar), porque solo está comparando dos adyacentes elementos (entonces, tal vez, en el primero, ¿puede omitir varios elementos?)

contestado el 22 de mayo de 12 a las 17:05

Tomas, mis dos soluciones (y la tuya y la de Stephen también) son equivalentes en cuanto a resultados y en contra de mis pruebas unitarias. El tuyo es MUY efectivo en cuanto al tiempo: agrega solo un 4% a las minas con estado. Aunque mientras tanto mejoré las minas cambiando de la ventana deslizante a la comparación de tuplas de ticks a través de Seq.pairwise todavía conlleva una penalización de tiempo del 11%. Mi única preocupación es la consideración del espacio: los datos de prueba tienen un valor de ~ 8 mil ticks que producen 507 por hora. Sería interesante ver si la lista intermedia y la necesidad de revertir afectarían negativamente su sugerencia si se aplicara a algunos años de datos de ticks. - gen belitski

Al igual que la solución de Tomas (de hecho, usé la suya como punto de partida, comentarios y todo), excepto usar Exploración secuencial que le permite evitar List.rev y produce resultados a pedido (por lo tanto, podríamos manejar flujos de ticks infinitos, por ejemplo).

let hourlyTicks = 
  readTicks @"EURUSD-history.zip" "EURUSD-2012-04.csv" 
  |> Seq.scan (fun (lastTakenId,_) tickAt ->
      // Similar to the body of your stateful function - 'lastTakenId' is the last state
      // and 'tickAt' is the current value.
      let tickId = tickAt / HOUR 
      if tickId <> lastTakenId then  
        // We return new state for 'lastTakenId' and yield current 
        // element to the "scan stream"
        (tickId, Some(tickAt))
      else 
        // Here, we skip element, so we return the original tick id and 
        // yield None to the "scan stream"
        (lastTakenId, None) ) (-1L, None) // Initial state: -1 and None

  //yield all the snd elements of the "scan stream" where Option.isSome
  |> Seq.choose snd

(descargo de responsabilidad: no probé esto porque no tengo todas las dependencias asumidas en su pregunta).

Actualizar en respuesta al comentario

Me pregunto si la penalización de rendimiento que está viendo se debe al encuadre/desencuadre de los valores en el acumulador. Me interesaría saber si lo siguiente muestra una mejora:

open System
open System.Collections.Generic
let hourlyTicks3 = 
  readTicks @"EURUSD-history.zip" "EURUSD-2012-04.csv" 
  |> Seq.scan (fun (kvp:KeyValuePair<_,_>) tickAt ->
      let lastTakenId = kvp.Key
      // Similar to the body of your stateful function - 'lastTakenId' is the last state
      // and 'tickAt' is the current value.
      let tickId = tickAt / HOUR 
      if tickId <> lastTakenId then  
        // We return new state for 'lastTakenId' and yield current 
        // element to the "scan stream"
        KeyValuePair<_,_>(tickId, Nullable<_>(tickAt))
      else 
        // Here, we skip element, so we return the original tick id and 
        // yield "null" to the "scan stream"
        KeyValuePair<_,_>(lastTakenId, Nullable<_>()) ) (KeyValuePair<_,_>(-1L, Nullable<_>())) // Initial state: -1 and "null"
  //yield all Values of KeyValuePair.Value elements of the "scan stream" where Nullable.HasValue
  |> Seq.filter (fun kvp -> kvp.Value.HasValue)
  |> Seq.map (fun kvp -> kvp.Value.Value)

contestado el 22 de mayo de 12 a las 21:05

Stephen, muy hermosa aplicación de Seq.scan! Sin embargo, conlleva una penalización de rendimiento sustancial. A modo de comparación he añadido Seq.toList (como en mis dos tomas originales que también están basadas en secuencias) y produjo un enorme aumento del 28% sobre el estado, en comparación con solo el 4% del basado en pliegues de Tomas. - gen belitski

@GeneBelitski - ¡Gracias! Agregué una actualización a mi respuesta con una idea para mejorar el rendimiento. - Esteban Swensen

Sí, el tiempo ha mejorado y ahora está a la par con mi penalización de tiempo original sin estado ~15% (me mudé a Seq.pairwise del Seq.windowed 2 desde entonces, que afeitó ~ 4% de penalización). Es probable que tenga razón sobre la causa adicional de boxing/unboxing, ya que ahora puedo observar notablemente menos gen0 GC. ¡Gracias! - gen belitski

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.