├── .gitignore
├── bin
└── HtmlAgilityPack.dll
├── docs
├── async-sequences
│ └── decor.png
├── async-sequences.fsi
└── async-sequences.fsx
├── src
├── Agents
│ ├── Agent.fs
│ ├── ConcurrentSetAgent.fs
│ ├── BatchProcessingAgent.fs
│ ├── SlidingWindowAgent.fs
│ ├── BlockingQueueAgent.fs
│ └── AutoCancelAgent.fs
├── Async.fs
├── FSharp.AsyncExtensions.sln
├── FSharp.AsyncExtensions.fsproj
├── IO.fs
├── Observable.fs
└── AsyncSeq.fs
├── samples
├── Caching.fsx
├── BlockingQueue.fsx
├── BatchProcessing.fsx
├── AutoCancel.fsx
├── MouseFollow.fsx
├── AsyncSeqObservable.fsx
├── StockStream.fsx
├── WebProxy.fsx
├── ChatServer.fsx
└── Crawler.fsx
├── README.markdown
└── License.markdown
/.gitignore:
--------------------------------------------------------------------------------
1 | *.dll
2 | *.pdb
3 | FSharp.ASyncExtensions.xml
4 | bin/release
--------------------------------------------------------------------------------
/bin/HtmlAgilityPack.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpetricek/FSharp.AsyncExtensions/HEAD/bin/HtmlAgilityPack.dll
--------------------------------------------------------------------------------
/docs/async-sequences/decor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tpetricek/FSharp.AsyncExtensions/HEAD/docs/async-sequences/decor.png
--------------------------------------------------------------------------------
/src/Agents/Agent.fs:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (Agent.fs)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 | namespace FSharp.Control
6 |
7 | /// Type alias for F# mailbox processor type
8 | type Agent<'T> = MailboxProcessor<'T>
9 |
--------------------------------------------------------------------------------
/src/Agents/ConcurrentSetAgent.fs:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (ConcurrentSetAgent.fs)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 | namespace FSharp.Control
6 |
7 | open System
8 |
9 | // ----------------------------------------------------------------------------
10 |
11 | /// Agent that implements a simple concurrent set. The agent exposes a
12 | /// member that adds value to the set and returns whether the value
13 | /// was already present.
14 | type ConcurrentSetAgent<'T>() =
15 | let agent = Agent.Start(fun agent -> async {
16 | let hashSet = new System.Collections.Generic.HashSet<_>(HashIdentity.Structural)
17 | while true do
18 | let! value, (repl:AsyncReplyChannel<_>) = agent.Receive()
19 | repl.Reply(hashSet.Add(value)) })
20 |
21 | /// Adds the specified element to the set and returns
22 | /// 'false' when it was already present in the set
23 | member x.AsyncAdd(v) = agent.PostAndAsyncReply(fun repl -> v, repl)
24 |
--------------------------------------------------------------------------------
/samples/Caching.fsx:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (AutoCancel.fsx)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 |
6 | // This example demonstrates how to use 'Async.Cache' and 'AsyncSeq.cache'
7 |
8 | #r "..\\bin\\FSharp.AsyncExtensions.dll"
9 | open FSharp.Control
10 |
11 | // The Async.Cache combinator makes it possible to create asynchronous
12 | // workflow that caches the result and performs computation only once
13 | // when called multiple times.
14 | let op =
15 | async { // Will be printed just once
16 | printfn "Evaluating..."
17 | return 42 }
18 | |> Async.Cache
19 |
20 | Async.RunSynchronously(op)
21 | Async.RunSynchronously(op)
22 |
23 |
24 | // The AsyncSeq.cache combinator has similar effect - the asynchronous
25 | // sequence can be used multiple times, but it will evaluate just once
26 | let asq =
27 | asyncSeq { for i in 0 .. 10 do
28 | printfn "Generating %d..." i
29 | yield i }
30 | |> AsyncSeq.cache
31 |
32 | AsyncSeq.iter (printfn "Consuming %d") asq
33 | |> Async.RunSynchronously
--------------------------------------------------------------------------------
/samples/BlockingQueue.fsx:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (BlockingQueue.fsx)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 |
6 | // This example demonstrates how to use 'BlockingAgent'
7 | // The agent implements producer/consumer concurrent pattern.
8 |
9 | #r "..\\bin\\FSharp.AsyncExtensions.dll"
10 | open FSharp.Control
11 |
12 | let buffer = new BlockingQueueAgent(3)
13 |
14 | // The sample uses two workflows that add/take elements
15 | // from the buffer with the following timeouts. When the producer
16 | // timout is larger, consumer will be blocked. Otherwise, producer
17 | // will be blocked.
18 | let producerTimeout = 500
19 | let consumerTimeout = 1000
20 |
21 | async {
22 | for i in 0 .. 10 do
23 | // Sleep for some time and then add value
24 | do! Async.Sleep(producerTimeout)
25 | do! buffer.AsyncAdd(i)
26 | printfn "Added %d" i }
27 | |> Async.Start
28 |
29 | async {
30 | while true do
31 | // Sleep for some time and then get value
32 | do! Async.Sleep(consumerTimeout)
33 | let! v = buffer.AsyncGet()
34 | printfn "Got %d" v }
35 | |> Async.Start
--------------------------------------------------------------------------------
/samples/BatchProcessing.fsx:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (BatchProcessing.fsx)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 |
6 | // This example demonstrates how to use 'BatchProcessingAgent'
7 | // The agent groups received messages in groups with a maximal
8 | // size and emits them with a maximal timeout.
9 |
10 | #r "..\\bin\\FSharp.AsyncExtensions.dll"
11 | open FSharp.Control
12 |
13 | open System.Drawing
14 | open System.Windows.Forms
15 |
16 | // Create simple winforms user interface with label
17 | let frm = new Form()
18 | let lbl = new Label(Font = new Font("Calibri", 20.0f), Dock = DockStyle.Fill)
19 | lbl.TextAlign <- ContentAlignment.MiddleCenter
20 | frm.Controls.Add(lbl)
21 | frm.Show()
22 |
23 | // Handle key press events but update the GUI after 5 keys
24 | // have been pressed or after 5 seconds (whichever happens first)
25 | let ag = new BatchProcessingAgent<_>(5, 5000)
26 | frm.KeyPress.Add(fun e -> ag.Enqueue(e.KeyChar))
27 | ag.BatchProduced
28 | |> Event.map (fun chars -> new System.String(chars))
29 | |> Event.scan (+) ""
30 | |> Event.add (fun str -> lbl.Text <- str)
31 |
32 |
--------------------------------------------------------------------------------
/samples/AutoCancel.fsx:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (AutoCancel.fsx)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 |
6 | // This example demonstrates how to use 'AutoCancelAgent'
7 | // The agent automatically stops its body when disposed.
8 |
9 | #r "..\\bin\\FSharp.AsyncExtensions.dll"
10 | open FSharp.Control
11 |
12 | let op = async {
13 | // Create a local agent that is disposed when the
14 | // workflow completes (using the 'use' construct)
15 | use agent = AutoCancelAgent.Start(fun agent -> async {
16 | try
17 | while true do
18 | // Wait for a message - note that we use timeout
19 | // to allow cancellation (when the operation completes)
20 | let! msg = agent.Receive(1000)
21 | match msg with
22 | | (n, reply:AsyncReplyChannel) ->
23 | // Print number and reply to the sender
24 | printfn "%d" n
25 | reply.Reply(())
26 | | _ -> ()
27 | finally
28 | // Called when the agent is disposed
29 | printfn "agent completed" })
30 |
31 | // Do some processing using the agent...
32 | for i in 0 .. 10 do
33 | do! Async.Sleep(100)
34 | do! agent.PostAndAsyncReply(fun r -> i, r)
35 |
36 | do! Async.Sleep(100)
37 | printfn "workflow completed" }
38 |
39 | Async.Start(op)
40 |
--------------------------------------------------------------------------------
/samples/MouseFollow.fsx:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (StockSlidingWindow.fsx)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 |
6 | // This example demonstrates how to implement a simple mouse following
7 | // algorithm using asynchronous sequences and Observable.window
8 |
9 | #r "..\\bin\\FSharp.AsyncExtensions.dll"
10 |
11 | open FSharp.Control
12 | open System.Drawing
13 | open System.Windows.Forms
14 |
15 | let form = new Form(Visible=true, TopMost=true)
16 |
17 | // Asynchronous sequence that returns cursor position at the 50 FPS rate
18 | let updates =
19 | asyncSeq {
20 | while true do
21 | yield form.PointToClient(Cursor.Position)
22 | do! Async.Sleep(20) }
23 | |> AsyncSeq.toObservable
24 |
25 | // Turn the updates into floating point numbers and calculate average
26 | // over sliding window containing the last 20 values
27 | updates
28 | |> Observable.map (fun me -> float32 me.X, float32 me.Y)
29 | |> Observable.windowed 20
30 | |> Observable.map (fun win ->
31 | let x = Array.averageBy fst win
32 | let y = Array.averageBy snd win
33 | x, y)
34 | // Draw an ellispe at the calculated location
35 | |> Observable.add (fun (x, y) ->
36 | use gr = form.CreateGraphics()
37 | gr.Clear(Color.White)
38 | gr.FillEllipse(Brushes.DarkOliveGreen, x - 10.0f, y - 10.0f, 20.0f, 20.0f) )
39 |
--------------------------------------------------------------------------------
/src/Async.fs:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (AsyncSeq.fs)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 | namespace FSharp.Control
6 | open System
7 |
8 | // ----------------------------------------------------------------------------
9 |
10 | []
11 | module AsyncExtensions =
12 |
13 | type Microsoft.FSharp.Control.Async with
14 |
15 | /// Creates an asynchronous workflow that runs the asynchronous workflow
16 | /// given as an argument at most once. When the returned workflow is
17 | /// started for the second time, it reuses the result of the
18 | /// previous execution.
19 | static member Cache (input:Async<'T>) =
20 | let agent = Agent>.Start(fun agent -> async {
21 | let! repl = agent.Receive()
22 | let! res = input
23 | repl.Reply(res)
24 | while true do
25 | let! repl = agent.Receive()
26 | repl.Reply(res) })
27 | async { return! agent.PostAndAsyncReply(id) }
28 |
29 | /// Starts the specified operation using a new CancellationToken and returns
30 | /// IDisposable object that cancels the computation. This method can be used
31 | /// when implementing the Subscribe method of IObservable interface.
32 | static member StartDisposable(op:Async) =
33 | let ct = new System.Threading.CancellationTokenSource()
34 | Async.Start(op, ct.Token)
35 | { new IDisposable with
36 | member x.Dispose() = ct.Cancel() }
37 |
--------------------------------------------------------------------------------
/src/Agents/BatchProcessingAgent.fs:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (BatchProcessingAgent.fs)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 | namespace FSharp.Control
6 |
7 | open System
8 |
9 | // ----------------------------------------------------------------------------
10 |
11 | /// Agent that can be used to implement batch processing. It creates groups
12 | /// of messages (added using the Enqueue method) and emits them using the
13 | /// BatchProduced event. A group is produced when it reaches the maximal
14 | /// size or after the timeout elapses.
15 | type BatchProcessingAgent<'T>(bulkSize, timeout) =
16 |
17 | let bulkEvent = new Event<'T[]>()
18 | let agent : Agent<'T> = Agent.Start(fun agent ->
19 | let rec loop remainingTime messages = async {
20 | let start = DateTime.Now
21 | let! msg = agent.TryReceive(timeout = max 0 remainingTime)
22 | let elapsed = int (DateTime.Now - start).TotalMilliseconds
23 | match msg with
24 | | Some(msg) when
25 | List.length messages = bulkSize - 1 ->
26 | bulkEvent.Trigger(msg :: messages |> List.rev |> Array.ofList)
27 | return! loop timeout []
28 | | Some(msg) ->
29 | return! loop (remainingTime - elapsed) (msg::messages)
30 | | None when List.length messages <> 0 ->
31 | bulkEvent.Trigger(messages |> List.rev |> Array.ofList)
32 | return! loop timeout []
33 | | None ->
34 | return! loop timeout [] }
35 | loop timeout [] )
36 |
37 | /// The event is triggered when a group of messages is collected. The
38 | /// group is not empty, but may not be of the specified maximal size
39 | /// (when the timeout elapses before enough messages is collected)
40 | []
41 | member x.BatchProduced = bulkEvent.Publish
42 |
43 | /// Sends new message to the agent
44 | member x.Enqueue v = agent.Post(v)
45 |
--------------------------------------------------------------------------------
/samples/AsyncSeqObservable.fsx:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (AsyncSeqObservable.fsx)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 |
6 | // This example demonstrates how to convert IObservable<'T> to AsyncSeq<'T>
7 |
8 | #r "..\\bin\\FSharp.AsyncExtensions.dll"
9 | open FSharp.Control
10 | open System.Windows.Forms
11 | open System.Threading
12 |
13 | // Create simple winforms user interface with a button and multiline text box
14 | let frm = new Form(Visible=true, TopMost=true, Width=440)
15 | let btn = new Button(Left=10, Top=10, Width=150, Text="Async Operation")
16 | let out = new TextBox(Left=10, Top=40, Width=400, Height=200, Multiline=true)
17 | frm.Controls.Add(btn)
18 | frm.Controls.Add(out)
19 |
20 | // Prints message to the displayed text box
21 | let wprint fmt =
22 | Printf.kprintf (fun s -> out.Text <- out.Text + s) fmt
23 |
24 |
25 | // The sample demonstrates two ways of converting IObservable<_> values to
26 | // asynchronous sequences. When using 'AsyncSeq.ofObservable', values that are
27 | // emitted when the asynchronous sequence is blocked are discarded. When you
28 | // click on the 'Async Operation' button, the following workflow starts
29 | // processing and drops all clicks until the body of the for loop completes
30 | let discarding =
31 | async {
32 | for click in btn.Click |> AsyncSeq.ofObservable do
33 | wprint "Sleeping (and discarding clicks)...\r\n"
34 | do! Async.Sleep(1000)
35 | wprint "Done (listening again)\r\n" }
36 |
37 | let ctsd = new CancellationTokenSource()
38 | Async.Start(discarding, ctsd.Token)
39 | ctsd.Cancel()
40 |
41 |
42 | // When using 'AsyncSeq.ofObservableBuffered', the values emitted by the
43 | // observable while the asynchronous sequence is blocked are stored in a
44 | // buffer (and will be returned as next elements).
45 | let buffering =
46 | async {
47 | for click in btn.Click |> AsyncSeq.ofObservableBuffered do
48 | wprint "Sleeping (and buffering clicks)...\r\n"
49 | do! Async.Sleep(1000)
50 | wprint "Done (ready for next value)\r\n" }
51 |
52 | let ctsb = new CancellationTokenSource()
53 | Async.Start(buffering, ctsb.Token)
54 | ctsb.Cancel()
--------------------------------------------------------------------------------
/src/Agents/SlidingWindowAgent.fs:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (SlidingWindowAgent.fs)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 | namespace FSharp.Control
6 |
7 | open System
8 |
9 | // ----------------------------------------------------------------------------
10 |
11 | /// Agent that implements the "sliding window" functionality. It collects
12 | /// messages added using the Enqueue method and emits them in overlapping
13 | /// groups of the specified size. For example, given [1,2,3,4,5...] and a
14 | /// size 3, the produced groups will be [1,2,3], [2,3,4], [3,4,5], ...
15 | type SlidingWindowAgent<'T>(windowSize, ?cancelToken) =
16 |
17 | // Event used to report groups
18 | let windowEvent = new Event<_>()
19 |
20 | // Start an agent that remembers partial windows of length
21 | // smaller than the count (new agent for every observer)
22 | let agent = Agent<'T>.Start((fun agent ->
23 | // The parameter 'lists' contains partial lists and their lengths
24 | let rec loop lists = async {
25 | // Receive the next value
26 | let! value = agent.Receive()
27 |
28 | // Add new empty list and then the new element to all lists.
29 | // Then split the lists into 'full' that should be sent
30 | // to the observer and 'partial' which need more elements.
31 | let full, partial =
32 | ((0, []) :: lists)
33 | |> List.map (fun (length, l) -> length + 1, value::l)
34 | |> List.partition (fun (length, l) -> length = windowSize)
35 |
36 | // Send all full lists to the observer (as arrays)
37 | for (_, l) in full do
38 | windowEvent.Trigger(l |> Array.ofSeq |> Array.rev)
39 | // Continue looping with incomplete lists
40 | return! loop partial }
41 |
42 | // Start with an empty list of partial lists
43 | loop []), ?cancellationToken = cancelToken)
44 |
45 | /// The event is triggered when a group of messages is collected.
46 | /// The size of the group is exactly 'count' and the values are
47 | /// returned in a fresh array.
48 | []
49 | member x.WindowProduced = windowEvent.Publish
50 |
51 | /// Sends new message to the agent
52 | member x.Enqueue v = agent.Post(v)
--------------------------------------------------------------------------------
/src/FSharp.AsyncExtensions.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 11.00
3 | # Visual Studio 2010
4 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.AsyncExtensions", "FSharp.AsyncExtensions.fsproj", "{EDE1812B-5A62-410A-9553-02499CF29317}"
5 | EndProject
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{8406B0C7-14A3-42F1-AC7A-EE26C0A9F15E}"
7 | ProjectSection(SolutionItems) = preProject
8 | ..\License.markdown = ..\License.markdown
9 | ..\README.markdown = ..\README.markdown
10 | EndProjectSection
11 | EndProject
12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{DD79E523-7072-4A67-89A0-257ECEF766D1}"
13 | ProjectSection(SolutionItems) = preProject
14 | ..\samples\AsyncSeqObservable.fsx = ..\samples\AsyncSeqObservable.fsx
15 | ..\samples\AutoCancel.fsx = ..\samples\AutoCancel.fsx
16 | ..\samples\BatchProcessing.fsx = ..\samples\BatchProcessing.fsx
17 | ..\samples\BlockingQueue.fsx = ..\samples\BlockingQueue.fsx
18 | ..\samples\Caching.fsx = ..\samples\Caching.fsx
19 | ..\samples\Crawler.fsx = ..\samples\Crawler.fsx
20 | ..\samples\MouseFollow.fsx = ..\samples\MouseFollow.fsx
21 | ..\samples\StockStream.fsx = ..\samples\StockStream.fsx
22 | ..\samples\WebProxy.fsx = ..\samples\WebProxy.fsx
23 | EndProjectSection
24 | EndProject
25 | Global
26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
27 | Debug|Any CPU = Debug|Any CPU
28 | Debug|Mixed Platforms = Debug|Mixed Platforms
29 | Debug|x86 = Debug|x86
30 | Release|Any CPU = Release|Any CPU
31 | Release|Mixed Platforms = Release|Mixed Platforms
32 | Release|x86 = Release|x86
33 | EndGlobalSection
34 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
35 | {EDE1812B-5A62-410A-9553-02499CF29317}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
36 | {EDE1812B-5A62-410A-9553-02499CF29317}.Debug|Any CPU.Build.0 = Debug|Any CPU
37 | {EDE1812B-5A62-410A-9553-02499CF29317}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
38 | {EDE1812B-5A62-410A-9553-02499CF29317}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
39 | {EDE1812B-5A62-410A-9553-02499CF29317}.Debug|x86.ActiveCfg = Debug|Any CPU
40 | {EDE1812B-5A62-410A-9553-02499CF29317}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {EDE1812B-5A62-410A-9553-02499CF29317}.Release|Any CPU.Build.0 = Release|Any CPU
42 | {EDE1812B-5A62-410A-9553-02499CF29317}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
43 | {EDE1812B-5A62-410A-9553-02499CF29317}.Release|Mixed Platforms.Build.0 = Release|Any CPU
44 | {EDE1812B-5A62-410A-9553-02499CF29317}.Release|x86.ActiveCfg = Release|Any CPU
45 | EndGlobalSection
46 | GlobalSection(SolutionProperties) = preSolution
47 | HideSolutionNode = FALSE
48 | EndGlobalSection
49 | EndGlobal
50 |
--------------------------------------------------------------------------------
/src/Agents/BlockingQueueAgent.fs:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (BlockingQueueAgent.fs)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 | namespace FSharp.Control
6 |
7 | open System
8 | open System.Collections.Generic
9 |
10 | // ----------------------------------------------------------------------------
11 |
12 | type internal BlockingAgentMessage<'T> =
13 | | Add of 'T * AsyncReplyChannel
14 | | Get of AsyncReplyChannel<'T>
15 |
16 | /// Agent that implements an asynchronous queue with blocking put
17 | /// and blocking get operation (this implements the producer-consumer
18 | /// concurrent programming pattern). The constructor takes the maximal
19 | /// size of the buffer.
20 | type BlockingQueueAgent<'T>(maxLength) =
21 | []
22 | let mutable count = 0
23 | let agent = Agent.Start(fun agent ->
24 |
25 | let queue = new Queue<_>()
26 |
27 | let rec emptyQueue() =
28 | agent.Scan(fun msg ->
29 | match msg with
30 | | Add(value, reply) -> Some(enqueueAndContinue(value, reply))
31 | | _ -> None )
32 | and fullQueue() =
33 | agent.Scan(fun msg ->
34 | match msg with
35 | | Get(reply) -> Some(dequeueAndContinue(reply))
36 | | _ -> None )
37 | and runningQueue() = async {
38 | let! msg = agent.Receive()
39 | match msg with
40 | | Add(value, reply) -> return! enqueueAndContinue(value, reply)
41 | | Get(reply) -> return! dequeueAndContinue(reply) }
42 |
43 | and enqueueAndContinue (value, reply) = async {
44 | reply.Reply()
45 | queue.Enqueue(value)
46 | count <- queue.Count
47 | return! chooseState() }
48 | and dequeueAndContinue (reply) = async {
49 | reply.Reply(queue.Dequeue())
50 | count <- queue.Count
51 | return! chooseState() }
52 | and chooseState() =
53 | if queue.Count = 0 then emptyQueue()
54 | elif queue.Count < maxLength then runningQueue()
55 | else fullQueue()
56 |
57 | // Start with an empty queue
58 | emptyQueue() )
59 |
60 | /// Asynchronously adds item to the queue. The operation ends when
61 | /// there is a place for the item. If the queue is full, the operation
62 | /// will block until some items are removed.
63 | member x.AsyncAdd(v:'T, ?timeout) =
64 | agent.PostAndAsyncReply((fun ch -> Add(v, ch)), ?timeout=timeout)
65 |
66 | /// Asynchronously gets item from the queue. If there are no items
67 | /// in the queue, the operation will block unitl items are added.
68 | member x.AsyncGet(?timeout) =
69 | agent.PostAndAsyncReply(Get, ?timeout=timeout)
70 |
71 | /// Synchronously gets item from the queue. If there are no items
72 | /// in the queue, the operation will block unitl items are added.
73 | /// This method blocks until value is available!
74 | member x.Get(?timeout) =
75 | agent.PostAndReply(Get, ?timeout=timeout)
76 |
77 | /// Gets the number of elements currently waiting in the queue.
78 | member x.Count = count
--------------------------------------------------------------------------------
/src/Agents/AutoCancelAgent.fs:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (AutoCancelAgent.fs)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 | namespace FSharp.Control
6 |
7 | open System
8 | open System.Threading
9 |
10 | // ----------------------------------------------------------------------------
11 | // See also: http://fssnip.net/64
12 |
13 | /// Wrapper for the standard F# agent (MailboxProcessor) that
14 | /// supports stopping of the agent's body using the IDisposable
15 | /// interface (the type automatically creates a cancellation token)
16 | type AutoCancelAgent<'T> private (mbox:Agent<'T>, cts:CancellationTokenSource) =
17 |
18 | /// Start a new disposable agent using the specified body function
19 | /// (the method creates a new cancellation token for the agent)
20 | static member Start(f) =
21 | let cts = new CancellationTokenSource()
22 | new AutoCancelAgent<'T>(Agent<'T>.Start(f, cancellationToken = cts.Token), cts)
23 |
24 | /// Returns the number of unprocessed messages in the message queue of the agent.
25 | member x.CurrentQueueLength = mbox.CurrentQueueLength
26 | /// Occurs when the execution of the agent results in an exception.
27 | []
28 | member x.Error = mbox.Error
29 | /// Waits for a message. This will consume the first message in arrival order.
30 | member x.Receive(?timeout) = mbox.Receive(?timeout = timeout)
31 | /// Scans for a message by looking through messages in arrival order until scanner
32 | /// returns a Some value. Other messages remain in the queue.
33 | member x.Scan(scanner, ?timeout) = mbox.Scan(scanner, ?timeout = timeout)
34 | /// Like PostAndReply, but returns None if no reply within the timeout period.
35 | member x.TryPostAndReply(buildMessage, ?timeout) =
36 | mbox.TryPostAndReply(buildMessage, ?timeout = timeout)
37 | /// Waits for a message. This will consume the first message in arrival order.
38 | member x.TryReceive(?timeout) =
39 | mbox.TryReceive(?timeout = timeout)
40 | /// Scans for a message by looking through messages in arrival order until scanner
41 | /// returns a Some value. Other messages remain in the queue.
42 | member x.TryScan(scanner, ?timeout) =
43 | mbox.TryScan(scanner, ?timeout = timeout)
44 | /// Posts a message to the message queue of the MailboxProcessor, asynchronously.
45 | member x.Post(m) = mbox.Post(m)
46 | /// Posts a message to an agent and await a reply on the channel, synchronously.
47 | member x.PostAndReply(buildMessage, ?timeout) =
48 | mbox.PostAndReply(buildMessage, ?timeout = timeout)
49 | /// Like PostAndAsyncReply, but returns None if no reply within the timeout period.
50 | member x.PostAndTryAsyncReply(buildMessage, ?timeout) =
51 | mbox.PostAndTryAsyncReply(buildMessage, ?timeout = timeout)
52 | /// Posts a message to an agent and await a reply on the channel, asynchronously.
53 | member x.PostAndAsyncReply(buildMessage, ?timeout) =
54 | mbox.PostAndAsyncReply(buildMessage, ?timeout=timeout)
55 |
56 | interface IDisposable with
57 | member x.Dispose() =
58 | (mbox :> IDisposable).Dispose()
59 | cts.Cancel()
60 |
--------------------------------------------------------------------------------
/samples/StockStream.fsx:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (StockStream.fsx)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 |
6 |
7 | // This example shows how to download stock prices from Yahooo and
8 | // return the result as an asynchronous sequence. The code that returns
9 | // the sequence downloads data from Yahoo in buffers and only downloads
10 | // the next buffer if it is required by the client (if it continues
11 | // iterating over the sequence).
12 |
13 | // Also available at: http://fssnip.net/7X
14 |
15 | #r "..\\bin\\FSharp.AsyncExtensions.dll"
16 |
17 | open System
18 | open System.Net
19 | open System.Text
20 | open FSharp.IO
21 | open FSharp.Control
22 | open Microsoft.FSharp.Control.WebExtensions
23 |
24 | // ----------------------------------------------------------------------------
25 | // Asynchronously downloading file from the web line-by-line
26 |
27 | /// Asynchronously download lines of a specified file
28 | /// (content is decuded using ASCII encoding)
29 | let downloadLines (url:string) = asyncSeq {
30 | // Create HTTP request and get response asynchronously
31 | let req = HttpWebRequest.Create(url)
32 | let! resp = req.AsyncGetResponse()
33 | let stream = resp.GetResponseStream()
34 |
35 | let str = ref ""
36 | // Download content in 1kB buffers
37 | for buffer in stream.AsyncReadSeq(1024) do
38 | // Decode buffer using ASCII and add to remaining text
39 | str := str.Value + String(Encoding.ASCII.GetChars(buffer)) + " "
40 |
41 | // Yield all lines except for the (incomplete) last one
42 | let parts = str.Value.Split([|'\n'; '\r'|], StringSplitOptions.RemoveEmptyEntries)
43 | for i in 0 .. parts.Length - 2 do
44 | yield parts.[i]
45 |
46 | // Save the unprocessed rest of text for the next iteration
47 | let rest = parts.[parts.Length - 1]
48 | str := rest.Substring(0, rest.Length - 1)
49 |
50 | // Yield the last line if it is not empty
51 | if str.Value <> "" then yield str.Value }
52 |
53 | // ----------------------------------------------------------------------------
54 | // Getting stock prices from Yahoo
55 |
56 | // Yahoo URL with historical stock prices
57 | let ystock = "http://ichart.finance.yahoo.com/table.csv?s="
58 |
59 | // Download data for MSFT and skip the header line
60 | downloadLines (ystock + "MSFT")
61 | |> AsyncSeq.skip 1
62 | |> AsyncSeq.map (fun line ->
63 | // Split line into Open, High, Low, Close values
64 | let infos = line.Split(',')
65 | float infos.[1], float infos.[2], float infos.[3], float infos.[4])
66 | // Take first 30 values and start printing asynchronously
67 | |> AsyncSeq.take 30
68 | |> AsyncSeq.iter (printfn "%A")
69 | |> Async.Start
70 |
71 | // ----------------------------------------------------------------------------
72 | // Wrap inside a reusable function
73 |
74 | /// Reusable function that downloads parsed stock prices
75 | let downloadStockPrices stock =
76 | downloadLines (ystock + stock)
77 | |> AsyncSeq.skip 1
78 | |> AsyncSeq.map (fun line ->
79 | // Split line into Open, High, Low, Close values
80 | let infos = line.Split(',')
81 | DateTime.Parse(infos.[0]),
82 | (float infos.[1], float infos.[2], float infos.[3], float infos.[4]))
83 |
--------------------------------------------------------------------------------
/src/FSharp.AsyncExtensions.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | 8.0.30703
7 | 2.0
8 | {ede1812b-5a62-410a-9553-02499cf29317}
9 | Library
10 | FSharp.AsyncExtensions
11 | FSharp.AsyncExtensions
12 | v4.0
13 | FSharp.AsyncExtensions
14 |
15 |
16 | true
17 | full
18 | false
19 | false
20 | bin\Debug\
21 | DEBUG;TRACE
22 | 3
23 | bin\Debug\FSharp.AsyncExtensions.XML
24 | --sig:sig.fsi
25 |
26 |
27 | pdbonly
28 | true
29 | true
30 | bin\Release\
31 | TRACE
32 | 3
33 | bin\Release\FSharp.AsyncExtensions.XML
34 |
35 |
36 |
37 |
38 | copy $(ProjectDir)$(OutDir)\FSharp.AsyncExtensions.* $(ProjectDir)..\bin
39 |
40 |
41 | copy $(ProjectDir)$(OutDir)\FSharp.AsyncExtensions.* $(ProjectDir)..\bin\release
42 | nuget pack $(ProjectDir)..\bin\release\FSharp.AsyncExtensions.dll.nuspec
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
71 |
--------------------------------------------------------------------------------
/src/IO.fs:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (IO.fs)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 |
6 | namespace FSharp.IO
7 | open FSharp.Control
8 | // ----------------------------------------------------------------------------
9 | // Extensions that simplify working with Stream using async sequences
10 |
11 | []
12 | module IOExtensions =
13 | type System.IO.Stream with
14 | /// Asynchronously reads the stream in chunks of a specified size
15 | /// and returns the result as an asynchronous sequence.
16 | member x.AsyncReadSeq(?bufferSize) =
17 | let bufferSize = defaultArg bufferSize 1024
18 | let buffer = Array.zeroCreate bufferSize
19 | let rec loop () = asyncSeq {
20 | let! count = x.AsyncRead(buffer, 0, bufferSize)
21 | if count > 0 then
22 | yield Array.sub buffer 0 count
23 | yield! loop() }
24 | loop ()
25 |
26 | /// Asynchronously writes all data specified by the
27 | /// given asynchronous sequence to the stream.
28 | member x.AsyncWriteSeq(input : AsyncSeq) = async {
29 | for data in input do
30 | do! x.AsyncWrite(data) }
31 |
32 | // ----------------------------------------------------------------------------
33 | // Extensions that simplify working with HttpListener and related types
34 |
35 | namespace FSharp.Net
36 | open System.IO
37 | open System.Net
38 | open System.Text
39 | open System.Threading
40 |
41 | open FSharp.IO
42 | open FSharp.Control
43 |
44 | []
45 | module HttpExtensions =
46 |
47 | type System.Net.HttpListener with
48 | /// Asynchronously waits for an incoming request and returns it.
49 | member x.AsyncGetContext() =
50 | Async.FromBeginEnd(x.BeginGetContext, x.EndGetContext)
51 |
52 | /// Starts HttpListener on the specified URL. The 'handler' function is
53 | /// called (in a new thread pool thread) each time an HTTP request is received.
54 | static member Start(url, handler, ?cancellationToken) =
55 | let server = async {
56 | use listener = new HttpListener()
57 | listener.Prefixes.Add(url)
58 | listener.Start()
59 | while true do
60 | let! context = listener.AsyncGetContext()
61 | Async.Start
62 | ( handler (context.Request, context.Response),
63 | ?cancellationToken = cancellationToken) }
64 | Async.Start(server, ?cancellationToken = cancellationToken)
65 |
66 | type System.Net.HttpListenerRequest with
67 | /// Asynchronously reads the 'InputStream' of the request and converts it to a string
68 | member request.AsyncInputString = async {
69 | use tmp = new MemoryStream()
70 | for data in request.InputStream.AsyncReadSeq(16 * 1024) do
71 | tmp.Write(data, 0, data.Length)
72 | tmp.Seek(0L, SeekOrigin.Begin) |> ignore
73 | use sr = new StreamReader(tmp)
74 | return sr.ReadToEnd() }
75 |
76 |
77 | type System.Net.HttpListenerResponse with
78 | /// Sends the specified string as a reply in UTF 8 encoding
79 | member response.AsyncReply(s:string) = async {
80 | let buffer = Encoding.UTF8.GetBytes(s)
81 | response.ContentLength64 <- int64 buffer.Length
82 | let output = response.OutputStream
83 | do! output.AsyncWrite(buffer,0,buffer.Length)
84 | output.Close() }
85 |
86 | /// Sends the specified data as a reply with the specified content type
87 | member response.AsyncReply(typ, buffer:byte[]) = async {
88 | response.ContentLength64 <- int64 buffer.Length
89 | let output = response.OutputStream
90 | response.ContentType <- typ
91 | do! output.AsyncWrite(buffer,0,buffer.Length)
92 | output.Close() }
--------------------------------------------------------------------------------
/docs/async-sequences.fsi:
--------------------------------------------------------------------------------
1 | namespace FSharp.Control
2 | type AsyncSeq<'T> = Async>
3 | and AsyncSeqInner<'T> =
4 | | Nil
5 | | Cons of 'T * AsyncSeq<'T>
6 |
7 | module AsyncSeq = begin
8 | []
9 | val empty<'T> : AsyncSeq<'T>
10 | val singleton : 'T -> AsyncSeq<'T>
11 | val append : AsyncSeq<'T> -> AsyncSeq<'T> -> AsyncSeq<'T>
12 |
13 | []
14 | // [snippet:builder]
15 | type AsyncSeqBuilder =
16 | // Waits for the result of a single asynchronous
17 | // operation and then continues generating the sequence
18 | member Bind : Async<'T> * ('T -> AsyncSeq<'U>) -> AsyncSeq<'U>
19 |
20 | // For every element of the input (asynchronous) sequence,
21 | // yield all elements generated by the body of the for loop
22 | member For : AsyncSeq<'T> * ('T -> AsyncSeq<'TResult>) -> AsyncSeq<'TResult>
23 | member For : seq<'T> * ('T -> AsyncSeq<'TResult>) -> AsyncSeq<'TResult>
24 |
25 | // Yield single/zero elements and concatenation of sequences
26 | member Yield : 'T -> AsyncSeq<'T>
27 | member Zero : unit -> AsyncSeq<'T>
28 | member Combine : AsyncSeq<'T> * AsyncSeq<'T> -> AsyncSeq<'T>
29 |
30 | type Microsoft.FSharp.Control.AsyncBuilder with
31 | // For every element of the input asynchronous sequence,
32 | // perform the specified asynchronous workflow
33 | member For : AsyncSeq<'T> * ('T -> Async) -> Async
34 | // [/snippet]
35 |
36 | val asyncSeq : AsyncSeqBuilder
37 | val internal tryNext : AsyncSeq<'a> -> Async,exn>>
38 | val internal tryWith : AsyncSeq<'T> -> (exn -> AsyncSeq<'T>) -> AsyncSeq<'T>
39 | val internal tryFinally : AsyncSeq<'T> -> (unit -> unit) -> AsyncSeq<'T>
40 | val collect :
41 | ('T -> AsyncSeq<'TResult>) -> AsyncSeq<'T> -> AsyncSeq<'TResult>
42 | val mapAsync : ('T -> Async<'TResult>) -> AsyncSeq<'T> -> AsyncSeq<'TResult>
43 | val chooseAsync : ('T -> Async<'R option>) -> AsyncSeq<'T> -> AsyncSeq<'R>
44 | val filterAsync : ('T -> Async) -> AsyncSeq<'T> -> AsyncSeq<'T>
45 | val lastOrDefault : 'T -> AsyncSeq<'T> -> Async<'T>
46 | val firstOrDefault : 'T -> AsyncSeq<'T> -> Async<'T>
47 | val iterAsync : ('T -> Async) -> AsyncSeq<'T> -> Async
48 | val pairwise : AsyncSeq<'T> -> AsyncSeq<'T * 'T>
49 |
50 | // [snippet:fold/scan/take]
51 | // Aggregate all elements of async sequence using async function
52 | // and return the result (as an asynchronous workflow)
53 | val foldAsync : ('S -> 'T -> Async<'S>) -> 'S -> AsyncSeq<'T> -> Async<'S>
54 |
55 | // Aggregate elements and yield immediate results as an async sequence
56 | // (The input is accessed lazily as the result sequence is evaluated)
57 | val scanAsync : ('S -> 'T -> Async<'S>) -> 'S -> AsyncSeq<'T> -> AsyncSeq<'S>
58 |
59 | // Same as previous functions, but the aggregating function is synchronous
60 | val fold : ('S -> 'T -> 'S) -> 'S -> AsyncSeq<'T> -> Async<'S>
61 | val scan : ('S -> 'T -> 'S) -> 'S -> AsyncSeq<'T> -> AsyncSeq<'S>
62 |
63 | // Synchronous and asynchronous version of a function that returns
64 | // elements of the input sequence (lazily) while a predicate holds
65 | val takeWhileAsync : ('T -> Async) -> AsyncSeq<'T> -> AsyncSeq<'T>
66 | val takeWhile : ('T -> bool) -> AsyncSeq<'T> -> AsyncSeq<'T>
67 | // [/snippet]
68 |
69 | val map : ('T -> 'a) -> AsyncSeq<'T> -> AsyncSeq<'a>
70 | val iter : ('T -> unit) -> AsyncSeq<'T> -> Async
71 | val choose : ('T -> 'a option) -> AsyncSeq<'T> -> AsyncSeq<'a>
72 | val filter : ('T -> bool) -> AsyncSeq<'T> -> AsyncSeq<'T>
73 | val ofSeq : seq<'T> -> AsyncSeq<'T>
74 | type internal BufferMessage<'T> =
75 | | Get of AsyncReplyChannel<'T>
76 | | Put of 'T
77 | val ofObservableBuffered : System.IObservable<'a> -> AsyncSeq<'a>
78 | val ofObservable : System.IObservable<'a> -> AsyncSeq<'a>
79 | val toBlockingSeq : AsyncSeq<'T> -> seq<'T>
80 | val cache : AsyncSeq<'T> -> AsyncSeq<'T>
81 | val zip : AsyncSeq<'T1> -> AsyncSeq<'T2> -> AsyncSeq<'T1 * 'T2>
82 | val takeWhileAsync : ('T -> Async) -> AsyncSeq<'T> -> AsyncSeq<'T>
83 | val skipWhileAsync : ('T -> Async) -> AsyncSeq<'T> -> AsyncSeq<'T>
84 | val takeWhile : ('T -> bool) -> AsyncSeq<'T> -> AsyncSeq<'T>
85 | val skipWhile : ('T -> bool) -> AsyncSeq<'T> -> AsyncSeq<'T>
86 | val take : int -> AsyncSeq<'T> -> AsyncSeq<'T>
87 | val skip : int -> AsyncSeq<'T> -> AsyncSeq<'T>
88 | end
89 | module Seq = begin
90 | val ofAsyncSeq : AsyncSeq<'T> -> seq<'T>
91 | end
92 | module AsyncSeqExtensions = begin
93 | val asyncSeq : AsyncSeq.AsyncSeqBuilder
94 | type AsyncBuilder with
95 | member For : seq:AsyncSeq<'T> * action:('T -> Async) -> Async
96 | end
97 |
98 | module IOExtensions = begin
99 | // [snippet:extensions]
100 | type System.IO.Stream with
101 | // Read the entire stream as an asynchronous
102 | // sequence in chunks of the specified size
103 | member AsyncReadSeq : ?bufferSize:int -> AsyncSeq
104 |
105 | type System.IO.Stream with
106 | // Asynchronously write all data from an
107 | // asynchronous sequence to the current stream.
108 | member AsyncWriteSeq : AsyncSeq -> Async
109 | // [/snippet]
110 | end
111 |
112 |
--------------------------------------------------------------------------------
/docs/async-sequences.fsx:
--------------------------------------------------------------------------------
1 | namespace FSharp
2 | #r @"C:\Tomas\Projects\FSharp\FSharp.AsyncExtensions\bin\FSharp.AsyncExtensions.dll"
3 | #r @"C:\Tomas\Projects\FSharp\FSharp.AsyncExtensions\bin\HtmlAgilityPack.dll"
4 |
5 | open FSharp.Control
6 | open System.Net
7 | open System
8 |
9 | module Control =
10 | // [snippet:Definition]
11 | /// Asynchronous computation that produces either end of a sequence
12 | /// (Nil) or the next value together with the rest of the sequence.
13 | type AsyncSeq<'T> = Async>
14 | and AsyncSeqInner<'T> =
15 | | Nil
16 | | Cons of 'T * AsyncSeq<'T>
17 | // [/snippet]
18 |
19 | type LazySeq<'T> = Lazy>
20 | and LazySeqInner<'T> =
21 | | Nil
22 | | Cons of 'T * LazySeq<'T>
23 |
24 | let rec nums n : LazySeq =
25 | lazy LazySeqInner.Cons(n, nums (n + 1))
26 |
27 | let rec map f (ls : LazySeq<_>) =
28 | lazy match ls.Value with
29 | | Nil -> Nil
30 | | Cons(h, t) -> LazySeqInner.Cons(f h, map f t)
31 |
32 | module Samples =
33 |
34 | // [snippet:computation expressions #1]
35 | // When accessed, generates numbers 1 and 2. The number
36 | // is returned 1 second after value is requested.
37 | let oneTwo = asyncSeq {
38 | do! Async.Sleep(1000)
39 | yield 1
40 | do! Async.Sleep(1000)
41 | yield 2 }
42 | // [/snippet]
43 |
44 | // [snippet:computation expressions #2]
45 | let urls =
46 | [ "http://bing.com"; "http://yahoo.com";
47 | "http://google.com"; "http://msn.com" ]
48 |
49 | // Asynchronous sequence that returns URLs and lengths
50 | // of the downloaded HTML. Web pages from a given list
51 | // are downloaded synchronously in sequence.
52 | let pages = asyncSeq {
53 | use wc = new WebClient()
54 | for url in urls do
55 | try
56 | let! html = wc.AsyncDownloadString(Uri(url))
57 | yield url, html.Length
58 | with _ ->
59 | yield url, -1 }
60 | // [/snippet]
61 |
62 | // [snippet:using from async]
63 | // Asynchronous workflow that prints results
64 | async {
65 | for url, length in pages do
66 | printfn "%s (%d)" url length }
67 | |> Async.Start
68 | // [/snippet]
69 |
70 | // [snippet:combinators]
71 | // Print URL of pages that are smaller than 50k
72 | let printPages =
73 | pages
74 | |> AsyncSeq.filter (fun (_, len) -> len < 50000)
75 | |> AsyncSeq.map fst
76 | |> AsyncSeq.iter (printfn "%s")
77 |
78 | printPages |> Async.Start
79 | // [/snippet]
80 |
81 | // [snippet:combinators internals]
82 | /// Return elements for which the predicate returns true
83 | let filter f (input : AsyncSeq<'T>) = asyncSeq {
84 | for v in input do
85 | if f v then yield v }
86 |
87 | /// Return elements for which the asynchronous predicate returns true
88 | let filterAsync f (input : AsyncSeq<'T>) = asyncSeq {
89 | for v in input do
90 | let! b = f v
91 | if b then yield v }
92 | // [/snippet]
93 |
94 | module Crawler =
95 |
96 | open System.Text.RegularExpressions
97 | open HtmlAgilityPack
98 |
99 | /// Asynchronously download the document and parse the HTML
100 | let downloadDocument url = async {
101 | try let wc = new WebClient()
102 | let! html = wc.AsyncDownloadString(Uri(url))
103 | let doc = new HtmlDocument()
104 | doc.LoadHtml(html)
105 | return Some doc
106 | with _ -> return None }
107 |
108 | /// Extract all links from the document that start with "http://"
109 | let extractLinks (doc:HtmlDocument) =
110 | try
111 | [ for a in doc.DocumentNode.SelectNodes("//a") do
112 | if a.Attributes.Contains("href") then
113 | let href = a.Attributes.["href"].Value
114 | if href.StartsWith("http://") then
115 | let endl = href.IndexOf('?')
116 | yield if endl > 0 then href.Substring(0, endl) else href ]
117 | with _ -> []
118 |
119 | /// Extract the of the web page
120 | let getTitle (doc:HtmlDocument) =
121 | let title = doc.DocumentNode.SelectSingleNode("//title")
122 | if title <> null then title.InnerText.Trim() else "Untitled"
123 |
124 | // ----------------------------------------------------------------------------
125 |
126 | // [snippet:crawler #1]
127 | /// Crawl the internet starting from the specified page.
128 | /// From each page follow the first not-yet-visited page.
129 | let rec randomCrawl url =
130 | let visited = new System.Collections.Generic.HashSet<_>()
131 |
132 | // Visits page and then recursively visits all referenced pages
133 | let rec loop url = asyncSeq {
134 | if visited.Add(url) then
135 | let! doc = downloadDocument url
136 | match doc with
137 | | Some doc ->
138 | // Yield url and title as the next element
139 | yield url, getTitle doc
140 | // For every link, yield all referenced pages too
141 | for link in extractLinks doc do
142 | yield! loop link
143 | | _ -> () }
144 | loop url
145 | // [/snippet]
146 |
147 | // [snippet:crawler #2]
148 | // Use AsyncSeq combinators to print the titles of the first 10
149 | // web sites that are from other domains than bing.com
150 | randomCrawl "http://news.bing.com"
151 | |> AsyncSeq.filter (fun (url, title) -> url.Contains("bing.com") |> not)
152 | |> AsyncSeq.map snd
153 | |> AsyncSeq.take 10
154 | |> AsyncSeq.iter (printfn "%s")
155 | |> Async.Start
156 | // [/snippet]
--------------------------------------------------------------------------------
/samples/WebProxy.fsx:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------
2 | // F# async extensions (AutoCancel.fsx)
3 | // (c) Tomas Petricek, 2011, Available under Apache 2.0 license.
4 | // ----------------------------------------------------------------------------
5 |
6 | // This example demonstrates how to implement simple HTTP proxy
7 |
8 | #r "..\\bin\\FSharp.AsyncExtensions.dll"
9 | open FSharp.Control
10 | open FSharp.IO
11 | open FSharp.Net
12 |
13 | open System
14 | open System.Net
15 | open System.Threading
16 |
17 | let root = "http://msdn.microsoft.com"
18 | let proxy = "http://localhost:8082/"
19 |
20 | // ----------------------------------------------------------------------------
21 | // Simple web proxy implemented using 'HttpListener'. This version downloads
22 | // the entire web page as a string and then writes it to the response stream.
23 |
24 | let cts1 = new CancellationTokenSource()
25 | HttpListener.Start(proxy, (fun (req, resp) -> async {
26 | // Download the web page
27 | let url = root + req.Url.PathAndQuery
28 | let wc = new WebClient()
29 | let! html = wc.AsyncDownloadString(Uri(url))
30 |
31 | // Replace URLs and send to the response stream
32 | let html = html.Replace(root, proxy)
33 | do! resp.AsyncReply(html) }), cancellationToken = cts1.Token)
34 |
35 | // Now go to: http://localhost:8082/en-us/fsharp
36 | cts1.Cancel()
37 |
38 | // ----------------------------------------------------------------------------
39 | // Better version of a proxy - this time, we read data from the input stream
40 | // in chunks and write them to the response stream as they arive.
41 |
42 | let cts2 = new CancellationTokenSource()
43 | HttpListener.Start(proxy, (fun (req, resp) -> async {
44 | // Initialize the download
45 | let url = root + req.Url.PathAndQuery
46 | let targetReq = HttpWebRequest.Create(url)
47 | use! targetResp = targetReq.AsyncGetResponse()
48 | use stream = targetResp.GetResponseStream()
49 |
50 | // Copy data until we read the entire input
51 | let count = ref 1
52 | let buffer = Array.zeroCreate 4096
53 | while !count > 0 do
54 | let! read = stream.AsyncRead(buffer, 0, buffer.Length)
55 | do! resp.OutputStream.AsyncWrite(buffer, 0, read)
56 | count := read
57 | resp.Close() }), cancellationToken = cts2.Token)
58 |
59 | cts2.Cancel()
60 |
61 | // ----------------------------------------------------------------------------
62 | // Proxy that copies data in chunks can be easily implemented using
63 | // asynchronous sequences. We read all data as asynchronous sequence and
64 | // write them to the output (Even simpler version could use 'AsyncWriteSeq'
65 | // to write all input buffers to the output stream).
66 |
67 | let cts3 = new CancellationTokenSource()
68 | HttpListener.Start(proxy, (fun (req, resp) -> async {
69 | // Initialize the download
70 | let url = root + req.Url.PathAndQuery
71 | let targetReq = HttpWebRequest.Create(url)
72 | use! targetResp = targetReq.AsyncGetResponse()
73 | use stream = targetResp.GetResponseStream()
74 |
75 | // Iterate over chunks read as an asynchronous sequence
76 | // and write them to the output stream
77 | for buffer in stream.AsyncReadSeq(4096) do
78 | do! resp.OutputStream.AsyncWrite(buffer, 0, buffer.Length)
79 | resp.Close() }), cancellationToken = cts3.Token)
80 |
81 | cts3.Cancel()
82 |
83 | // ----------------------------------------------------------------------------
84 | // A more sophisticated version of proxy that caches web
85 | // pages using a simple agent.
86 |
87 | type CacheMessage =
88 | | TryGet of string * AsyncReplyChannel