F#でのC#コードの書き換え
-
05-07-2019 - |
質問
F#をいじって、このC#バージョン(C ++ wikiエントリからコピー)に基づいて基本的なラグランジュ補間関数を作成しようとしました:
double Lagrange(double[] pos, double[] val, double desiredPos)
{
double retVal = 0;
for (int i = 0; i < val.Length; ++i)
{
double weight = 1;
for (int j = 0; j < val.Length; ++j)
{
// The i-th term has to be skipped
if (j != i)
{
weight *= (desiredPos - pos[j]) / (pos[i] - pos[j]);
}
}
retVal += weight * val[i];
}
return retVal;
}
F#と関数型プログラミングに関する限られた知識を使用して思いついたのは次のとおりです。
let rec GetWeight desiredPos i j (pos : float[]) weight =
match i with
| i when j = pos.Length -> weight
| i when i = j -> GetWeight desiredPos i (j+1) pos weight
| i -> GetWeight desiredPos i (j+1) pos (weight * (desiredPos - pos.[j])/(pos.[i] - pos.[j]) )
let rec Lagrange (pos : float[]) (vals : float[]) desiredPos result counter =
match counter with
| counter when counter = pos.Length -> result
| counter -> Lagrange pos vals desiredPos (result + (GetWeight desiredPos counter 0 pos 1.0)* vals.[counter]) (counter+1)
同じ C#コードに基づいて、より優れた/よりきれいなF#バージョンを提供できますか?
解決
シーケンスの折りたたみは、ループをアキュムレータに置き換える一般的な方法です。
let Lagrange(pos:_[], v:_[], desiredPos) =
seq {0 .. v.Length-1}
|> Seq.fold (fun retVal i ->
seq {for j in 0 .. pos.Length-1 do if i <> j then yield j}
|> Seq.fold (fun w j -> w * (desiredPos - pos.[j]) / (pos.[i] - pos.[j])) 1.0
|> (fun weight -> weight * v.[i] + retVal)) 0.0
他のヒント
機能的ソリューションをmakesくする部分は、インデックスを意味するi番目の要素をスキップすることです。すべてのいインデックス処理が分離されるように、それを再利用可能な関数に引き出します。私は私のRoundRobinを呼び出します。
let RoundRobin l = seq {
for i in {0..Seq.length l - 1} do
yield (Seq.nth i l, Seq.take i l |> Seq.append <| Seq.skip (i+1) l)
}
ただし、効率的なバージョンを作成したい場合は、はるかにいかもしれません。
Seqモジュールでproduct
が見つからなかったため、独自に作成しました。
let prod (l : seq<float>) = Seq.reduce (*) l
コードの作成は非常に簡単です:
let Lagrange pos value desiredPos = Seq.sum (seq {
for (v,(p,rest)) in Seq.zip value (RoundRobin pos) do
yield v * prod (seq { for p' in rest do yield (desiredPos - p') / (p - p') })
})
RoundRobinは、pos [i]が内側のループの残りのposに含まれないようにします。 val
配列を含めるには、ラウンドロビン方式のpos
配列で圧縮しました。
ここでの教訓は、インデックス付けは機能的なスタイルでは非常に見苦しいということです。
また、クールなトリックを発見しました:|> Seq.append <|
は、シーケンスを追加するための挿入構文を提供します。ただし、^
ほど良くありません。
これは命令型コードとして問題なく動作すると思います:
let LagrangeI(pos:_[], v:_[], desiredPos) =
let mutable retVal = 0.0
for i in 0..v.Length-1 do
let mutable weight = 1.0
for j in 0..pos.Length-1 do
// The i-th term has to be skipped
if j <> i then
weight <- weight * (desiredPos - pos.[j]) / (pos.[i] - pos.[j])
retVal <- retVal + weight * v.[i]
retVal
しかし、機能的にしたい場合は、いくつかの折り畳み(インデックスを一緒に運ぶ必要があることが多いのでmapiとともに)はうまく機能します:
let LagrangeF(pos:_[], v:_[], desiredPos) =
v |> Seq.mapi (fun i x -> i, x)
|> Seq.fold (fun retVal (i,vi) ->
let weight =
pos |> Seq.mapi (fun j x -> j<>i, x)
|> Seq.fold (fun weight (ok, posj) ->
if ok then
weight * (desiredPos - posj) / (pos.[i] - posj)
else
weight) 1.0
retVal + weight * vi) 0.0
ここでは数学がわからないので、いくつかのランダムな値を使用してテストし(うまくいけば)失敗したことを確認します:
let pos = [| 1.0; 2.0; 3.0 |]
let v = [|8.0; 4.0; 9.0 |]
printfn "%f" (LagrangeI(pos, v, 2.5)) // 5.375
printfn "%f" (LagrangeF(pos, v, 2.5)) // 5.375
これは非再帰的なソリューションです。アルゴリズムにはインデックスが必要なので少しファンキーですが、うまくいけばF#の関数がどのように構成されるかを示しています:
let Lagrange (pos : float[]) (vals : float[]) desiredPos =
let weight pos desiredPos (i,v) =
let w = pos |> Array.mapi (fun j p -> j,p)
|> Array.filter (fun (j,p) -> i <> j)
|> Array.fold (fun acc (j,p) -> acc * (desiredPos - p)/(pos.[i] - p)) 1.
w * v
vals |> Array.mapi (fun i v -> i,v)
|> Array.sumBy (weight pos desiredPos)
let rec GetWeight desiredPos i j (pos : float[]) weight =
if j = pos.Length then weight
elif i = j then GetWeight desiredPos i (j+1) pos weight
else GetWeight desiredPos i (j+1) pos (weight * (desiredPos - pos.[j])/(pos.[i] - pos.[j]) )
let rec Lagrange (pos : float[]) (vals : float[]) desiredPos result counter =
if counter = pos.Length then result
else Lagrange pos vals desiredPos (result + (GetWeight desiredPos counter 0 pos 1.0)* vals.[counter]) (counter+1)
個人的には、単純なif / elif / elseコンストラクトは、次のようなオーバーヘッドなしでここでより良く見えると思います
match i with
|i when i=...
単にいじくり回している場合は、関数カリー化とタプルパイプ演算子を使用するBrianのバージョンに似ています。
let Lagrange(pos:_[], v:_[], desiredPos) =
let foldi f state = Seq.mapi (fun i x -> i, x) >> Seq.fold f state
(0.0, v) ||> foldi (fun retVal (i, posi) ->
(1.0, pos) ||> foldi (fun weight (j, posj) ->
if j <> i then
(desiredPos - posj) / (posi - posj)
else
1.0)
|> (fun weight -> weight * posi + retVal))
私の試み:
let Lagrange(p:_[], v, desiredPos) =
let Seq_multiply = Seq.fold (*) 1.0
let distance i j = if (i=j) then 1.0 else (desiredPos-p.[j])/(p.[i]-p.[j])
let weight i = p |> Seq.mapi (fun j _ -> distance i j) |> Seq_multiply
v |> Seq.mapi (fun i vi -> (weight i)*vi) |> Seq.sum
内部ループを関数にしてリファクタリングします。また、コードをよりわかりやすく、<!> quot;わかりやすい<!> quot;にすることができます。いくつかの意味のある関数を定義します。
また、この書き換えにより、元のコード(および他のすべてのバリアント)のバグが強調表示されます。距離関数は実際には次のようになります。
let distance i j = if (p.[i]=p.[j]) then 1.0 else (desiredPos-p.[j])/(p.[i]-p.[j])
一般的なゼロによる除算エラーを回避します。これにより、汎用のインデックスレスソリューションが実現します。
let Lagrange(p, v, desiredPos) =
let distance pi pj = if (pi=pj) then 1.0 else (desiredPos-pj)/(pi-pj)
let weight pi vi = p |> Seq.map (distance pi) |> Seq.fold (*) vi
Seq.map2 weight p v |> Seq.sum