2016-11-21 39 views
5

我正在尋找一種慣用的方法來編程F#中的過濾器。爲了清楚起見,我將過濾器稱爲隨時間推移使用一系列測量值併產生不斷變化的估計值的函數。這意味着該功能能夠保持狀態。例如,在Python中,可以使用協程來以非常乾淨的方式維護狀態。過濾的慣用方法

我在尋找的是在F#中編程過濾器的慣用方法。鑑於我的頭腦被OOP和程序原理徹底污染,當然我想出了一些課程來表達它們。 F#中是否存在一種更習慣於過濾的方法,可能會打開功能範式的其他好處?

open System 
open MathNet.Numerics.LinearAlgebra 
open MathNet.Numerics.Random 
open MathNet.Numerics.Distributions 
open MathNet.Numerics.Statistics 
open FSharp.Charting 

type ScalarKalman (A : float, H : float, Q : float, R : float) = class 

    let mutable A = A 
    let mutable H = H 
    let mutable Q = Q 
    let mutable R = R 

    let mutable p = 0. 
    let mutable x = 0. 
    let mutable k = 0. 

    let mutable result = 0. 

    member this.X 
     with get() = x 
     and set(value) = x <- value 

    member this.P 
     with get() = p 
     and set(value) = p <- value 

    member this.K 
     with get() = k 
     and set(value) = k <- value 

    member this.update(newVal : float) = 
     let xp = A * this.X 
     let Pp = A * this.P * A + Q 
     this.K <- Pp * H/(H * Pp * H + R) 
     this.X <- xp + this.K * (newVal - H * xp) 
     this.P <- Pp - this.K * H * Pp 

end 

let n = 100 
let obsv = [|for i in 0 .. n do yield 0.|] 
let smv = [|for i in 0 .. n do yield 0.|] 
let kal = new ScalarKalman(1., 1., 0., 5.) 
kal.P <- 4. 
kal.X <- 6. 
for i in 0 .. n do 
    obsv.[i] <- Normal.Sample(10., 5.) 
    kal.update(obsv.[i]) 
    smv.[i] <- kal.X 

Chart.Combine([obsv |> Chart.FastLine 
       smv |> Chart.FastLine]) |> Chart.Show 

回答

8

在你的情況,術語「功能性」和「F#慣用的」將包括兩兩件事:不變的數據和代碼數據的分離。

不可變數據:你將不得不表示濾波器的參數的一個數據結構(即AHQ,和R),並且表示濾波器的當前狀態的另一結構(即XK,和P)。兩者都是不變的。而不是改變國家,你會產生一個新的。

從代碼中分離數據:過濾器本身將包含一個函數,該函數接受參數,當前狀態,下一個觀測值併產生下一個狀態。這個下一個狀態將隨着下一個觀察值被反饋回函數,從而產生下一個+1狀態,依此類推。參數始終保持不變,所以它們可以使用部分應用程序(參見下文)僅傳入一次。

一旦你有這樣的功能,你可以將它作爲一個「滾動投影」「應用」到觀察列表中 - 如上所述 - 將每個觀察結果與最後一個狀態一起輸入函數,產生下一個狀態。這種「滾動投影」操作在功能編程中是非常普遍的事情,通常稱爲scan。 F#確實爲所有標準集提供的scan實現 - listseq

由於scan結果,你將有過濾器的連續狀態的列表。現在剩下要做的就是從每個狀態中撈出X值。

下面是完整的解決方案:

module ScalarKalman = 

    type Parameters = { A : float; H : float; Q : float; R : float } 
    type State = { K: float; X: float; P: float } 

    let initState (s: State) = s 

    let getX s = s.X 

    let update parms state newVal = 
     let xp = parms.A * state.X 
     let Pp = parms.A * state.P * parms.A + parms.Q 
     let newK = Pp * parms.H/(parms.H * Pp * parms.H + parms.R) 
     { K = newK 
     X = xp + newK * (newVal - parms.H * xp) 
     P = Pp - newK * parms.H * Pp } 


let n = 100 
let obsv = [for i in 0 .. n -> Normal.Sample(10., 5.)] 
let kal = ScalarKalman.update { A = 1.; H = 1.; Q = 0.; R = 5. } 
let initialState = ScalarKalman.initState { X = 6.; P = 4.; K = 0. } 

let smv = 
    obsv 
    |> List.scan kal initialState 
    |> List.map ScalarKalman.getX 

了一份關於設計
注意在模塊中聲明的initState功能。這個函數在表面看起來可能很愚蠢,但它有着重要的含義:它允許我通過名稱指定狀態字段而不需要模塊,從而避免命名空間污染。此外,消費代碼現在看起來更具可讀性:它說明了它的功能,不需要任何評論。

另一種常見的方法來這是模塊,其消耗代碼然後可以經由with語法修改在申報了「基礎」狀態:

module ScalarKalman = 
    ... 
    let zeroState = { K = 0.; X = 0.; P = 0. } 

... 
let initialState = { ScalarKalman.zeroState with X = 6.; P = 4. } 

上集合
F#列表是一個註記可以處理少量數據和小型處理管道,但隨着這兩個維度的增長而變得昂貴。如果您正在處理大量流數據,並且/或者如果您連續應用多個過濾器,則最好使用惰性序列 - seq。爲此,只需將List.scanList.map分別替換爲Seq.scanSeq.map即可。如果你這樣做,你會得到一個懶惰的序列作爲最終的結果,然後你需要以某種方式消費 - 將其轉換爲列表,打印出來,發送給下一個組件,或者無論你的大背景是什麼意思。