3

私は自分で F# を学んでおり (これは楽しみのためであり、仕事や学校のためではありません)、Windows Phone アプリの複数の市場にわたるレビューの数をカウントする単純なパーサーを作成しようとしています。これまでのコードが醜いことは間違いありませんが、それを改善して関数型プログラミングのパラダイムに従おうとしています。私は C、C++、C# の世界から来たので、かなり大変です。

  • C の世界から来て、null 値が好きです。関数型プログラミング / F# では null の使用が推奨されていないことはわかっていますが、使用しない方法がわかりません。たとえば、関数 parse には null チェックがあります。どうすればそうしないのですか?

  • 現在、私のコードは最初のページのレビュー数のみをカウントしていますが、アプリに 10 件を超えるレビューがあり、結果として複数のページが存在する可能性があります。すべてのページを再帰的に処理するにはどうすればよいですか (関数 downloadReviews または parse)。

  • このコードを完全に非同期に拡張するにはどうすればよいでしょうか?

以下は私がこれまでに持っているコードです。上記の質問に加えて、誰かが私を助けて、私のコードの全体的な構造を改善する方法を教えてくれたら本当にうれしいです。

open System
open System.IO
open System.Xml
open System.Xml.Linq
open Printf

type DownloadPageResult = {
    Uri: System.Uri;
    ErrorOccured: bool;
    Source: string;
}

type ReviewData = {
    CurrentPageUri: System.Uri;
    NextPageUri: System.Uri;
    NumberOfReviews: int;
}

module ReviewUrl = 
    let getBaseUri path =
        new Uri(sprintf "http://cdn.marketplaceedgeservice.windowsphone.com/%s" path)

    let getUri country locale appId =
        getBaseUri(sprintf "/v8/ratings/product/%s/reviews?os=8.0.0.0&cc=%s&oc=&lang=%s&hw=520170499&dm=Test&chunksize=10" appId country locale)

let downloadPage (uri: System.Uri) =
    try
        use webClient = new System.Net.WebClient()
        printfn "%s" (uri.ToString())
        webClient.Headers.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
        webClient.Headers.Add("Accept-Encoding", "zip,deflate,sdch")
        webClient.Headers.Add("Accept-Language", "en-US,en;q=0.8,fr;q=0.6")
        webClient.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1482.0 Safari/537.36")
        { Uri = uri; Source = webClient.DownloadString(uri); ErrorOccured = false }
    with error -> { Uri = uri; Source = String.Empty; ErrorOccured = true }

let downloadReview country locale appId =
    let uri = ReviewUrl.getUri country locale appId
    downloadPage uri

let parse(pageResult: DownloadPageResult) =
    if pageResult.ErrorOccured then { CurrentPageUri = pageResult.Uri; NextPageUri = null; NumberOfReviews = 0 }
    else 
        let reader = new StringReader(pageResult.Source)
        let doc = XDocument.Load(reader)
        let ns = XNamespace.Get("http://www.w3.org/2005/Atom")

        let nextUrl = query { for link in doc.Descendants(ns + "link") do
                              where (link.Attribute(XName.Get("rel")).Value = "next")
                              select link.Value
                              headOrDefault }

        if nextUrl = null then
            { CurrentPageUri = pageResult.Uri; NextPageUri = null; NumberOfReviews = doc.Descendants(ns + "entry") |> Seq.length } 
        else
            { CurrentPageUri = pageResult.Uri; NextPageUri = ReviewUrl.getBaseUri(nextUrl); NumberOfReviews = doc.Descendants(ns + "entry") |> Seq.length } 

let downloadReviews(locale: string) =
    let appId = "4e08377c-1240-4f80-9c35-0bacde2c66b6"
    let country = locale.Substring(3)
    let pageResult = downloadReview country locale appId
    let parseResult = parse pageResult
    parseResult


[<EntryPoint>]
let main argv = 
    let locales = [| "en-US"; "en-GB"; |]
    let results = locales |> Array.map downloadReviews

    printfn "%A" results
    0
4

3 に答える 3

2

コードを非同期にする一般的なパターンは、I/O コストのかかる操作 (コール ツリーのどこか下) を見つけて、そこから「上」に移動し、必要なポイントに到達するまで、それを使用するすべてのコードも非同期にすることです。ブロック。

あなたの例では、プリミティブ操作はダウンロードであるため、downloadPage非同期にすることから始めます。

let downloadPage (uri: System.Uri) = async {
    try
        use webClient = new System.Net.WebClient()
        printfn "%s" (uri.ToString())
        // (Headers omitted)
        let! source = webClient.AsyncDownloadString(uri)
        return { Uri = uri; Source = source; ErrorOccured = false }
    with error -> 
        return { Uri = uri; Source = String.Empty; ErrorOccured = true } }

でコードをラップしasync { ... }、using の非同期バージョンを呼び出し、DownloadStringusinglet!で結果を返す必要がありますreturn(両方の分岐で)。

downloadReview次に、 andのような関数を作成する必要があります(ここでも、それらを async ブロックでラップし、 usingまたは usingdownloadReviewsなどの他の非同期操作を呼び出します)。downloadPagelet!return!

最後に、コンソール アプリケーションを作成している場合はブロックする必要がありますが、異なるロケールのダウンロードを並行して実行できます。downloadReviews非同期であると仮定します:

let locales = [| "en-US"; "en-GB"; |]
let results = 
  locales 
  |> Array.map downloadReviews   // Build an array of asynchronous computations
  |> Async.Parallel              // Compose them into a single, parallel computation
  |> Async.RunSynchronously      // Run the computation and wait

他の質問に答えるためnullに、上記の例での使用はおそらく問題ないと思います (それを返す LINQ を呼び出しているため、それを回避する簡単な方法はありません)。代わりにオプション タイプを使用することは実際には可能ですが、少し注意が必要です。興味がある場合は、このスニペットを参照してください。

また、F# データ ライブラリHttp.AsyncRequestのメソッドを使用すると、複雑な HTTP 要求を作成するためのもう少し簡単な方法が得られます (ただし、私はそのライブラリの貢献者の 1 人なので、偏見があります!)

于 2013-06-04T00:54:56.767 に答える
2

Tomasが言ったように、非同期ベースのバージョンを作成する方が「機能的」ですDownloadString(または、それを処理するために彼の FSharp.Data ライブラリを使用するだけです)。

FSharp.DataExtCoreasyncMaybeと組み合わせて、ExtCore のまたはasyncChoiceワークフローを利用することもできます。これらのワークフローは、通常のワークフローに加えて、非常に使いやすいエラー処理を提供しasyncます。

とにかく、私はあなたのコードをクリーンアップするのに数分を費やしました。大したことではありませんが、いくつかの場所でコードを簡素化します。

open System
open System.IO
open System.Xml
open System.Xml.Linq
open Printf

type DownloadPageResult = {
    Uri : System.Uri;
    ErrorOccured : bool;
    Source : string;
}

type ReviewData = {
    CurrentPageUri : System.Uri;
    NextPageUri : System.Uri option;
    NumberOfReviews : uint32;
}

module ReviewUrl =
    let baseUri = Uri ("http://cdn.marketplaceedgeservice.windowsphone.com/", UriKind.Absolute)

    let getUri country locale (appId : System.Guid) =
        let localUri =
            let appIdStr = appId.ToString "D"
            sprintf "/v8/ratings/product/%s/reviews?os=8.0.0.0&cc=%s&oc=&lang=%s&hw=520170499&dm=Test&chunksize=10" appIdStr country locale
        Uri (baseUri, localUri)

let downloadPage (uri : System.Uri) =
    try
        use webClient = new System.Net.WebClient()
        printfn "%s" (uri.ToString())
        webClient.Headers.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
        webClient.Headers.Add("Accept-Encoding", "zip,deflate,sdch")
        webClient.Headers.Add("Accept-Language", "en-US,en;q=0.8,fr;q=0.6")
        webClient.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1482.0 Safari/537.36")
        { Uri = uri; Source = webClient.DownloadString uri; ErrorOccured = false }
    with error ->
        { Uri = uri; Source = String.Empty; ErrorOccured = true }


let parse (pageResult : DownloadPageResult) =
    if pageResult.ErrorOccured then
        { CurrentPageUri = pageResult.Uri; NextPageUri = None; NumberOfReviews = 0u }
    else 
        use reader = new StringReader (pageResult.Source)
        let doc = XDocument.Load reader
        let ns = XNamespace.Get "http://www.w3.org/2005/Atom"

        let nextUrl =
            query {
            for link in doc.Descendants(ns + "link") do
            where (link.Attribute(XName.Get("rel")).Value = "next")
            select link.Value
            headOrDefault }

        {   CurrentPageUri = pageResult.Uri;
            NextPageUri =
                if System.String.IsNullOrEmpty nextUrl then None
                else Some <| Uri (ReviewUrl.baseUri, nextUrl);
            NumberOfReviews =
                doc.Descendants (ns + "entry") |> Seq.length |> uint32; }

let downloadReviews (locale : string) =
    System.Guid "4e08377c-1240-4f80-9c35-0bacde2c66b6"
    |> ReviewUrl.getUri (locale.Substring 3) locale
    |> downloadPage
    |> parse


[<EntryPoint>]
let main argv =
    let locales = [| "en-US"; "en-GB"; |]
    let results = locales |> Array.map downloadReviews

    printfn "%A" results
    0
于 2013-06-04T02:16:46.647 に答える