Happy New Year 2016 around the World Behind the scenes of my #FsAdvent project
Just like last year and the year before, I wanted to participate in the #FsAdvent event, where someone writes a blog post about something they did with F# during December. Thanks to Sergey Tihon for the organization of the English version and the Japanese F# community for coming up with the idea a few years ago!
As my blog post ended up on 31 December, I wanted to do something that would fit well with the theme of ending of 2015 and starting of the new year 2016 and so I decided to write a little interactive web site that tracks the "Happy New Year" tweets live across the globe. This is partly inspired by Happy New Year Tweets from Twitter in 2014, but rather than analyzing data in retrospect, you can watch 2016 come live!
Without further ado, here are the important links:
-
Happy New Year 2016 around the World live web site!
(It will stay alive for a few days around 31 December 2015, but not forever.) -
F# source code for the project on GitHub
(Feel free to modify it and use it for other events!) - Continute reading if you want to learn about how it works!
Before we get to the technical details, here is a brief screenshot showing the project live:
Overview
On the front-end side, the web site displays three different things - it shows live tweets on a map, it shows live tweets in a feed (below on the right) and it shows a word cloud with most common phrases. Everything is updated live using a three web socket connections with the server.
On the back-end side, the server uses Twitter Streaming API to receive "Happy New Year" tweets as they happen. It then uses various techniques for getting locations of some tweets so that they can appear on the map and it calculates statistics (e.g. for the word cloud) on the fly.
If you look at the source code, pretty much all back-end is implemented in a single F# script file. For the front-end, I didn't do anything fancy and hacked together some JavaScript using the great D3-based Datamaps library for the map.
There are a couple of nice things in the code including the connection to Twitter, F# type providers (as always), agents for reactive programming and Suave web server for implementing web sockets.
Getting a stream of tweets
To get the tweets, I'm using the F# Data Toolbox library,
which comes with a nice Twitter API wrapper built using F# type providers. As a single-user
application (all is happening on the server), we can directly provider the application
access token & secret and connect to the Twitter directly. Then we can use the
twitter.Streaming.FilterTweets
method to search for tweets that contain any of the known
"Happy New Year" phrases:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
|
The search.TweetReceived
event will be triggered when a new tweet happens. The status
object
has a bunch of properties (inferred by a type provider). It turns out that event status.Text
is
optional and so parsing the tweets involves a lot of pattern matching:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: |
|
The code is slightly simplified, but it is pretty representative. Now we have a value
liveTweets
of type IObservable<Tweet>
which is an event that is triggered every time
we get a new (not completely silly) tweet.
Geolocating tweets and users
The hardest bit turns out to be getting good tweets for the map. Not a lot of tweets come with GPS coordinates and so I had to do a couple of tricks. When more people start tweeting around the New Year, we should be able to use mostly tweets with GPS coordinates, but there are some backup strategies:
- If a tweet has GPS coordinates, use this as the location
- Every now and then use MapQuest or Bing to geolocate the user based on their location in the profile
- If we didn't produce enough tweets using (1) or (2), locate tweet based on the language of the phrase and put it in some place where a previous tweet with the same phrase appeared.
In priciple, geolocating users based on their profile would work good enough, but all the geolocation services have rate limits that are easy to hit when the site is running live and so I added (3) as the last resort. If I had more time, I would probably try to build an index with country and city names, which would likely cover enough tweets (at least from users with a reasonable text in their "location").
Tweets with GPS coordinates
All of the methods report tweets to a "replay" agent (see below) that replays the tweets with
a specified delay. This is done using replay.AddEvent
at the end of the pipeline. For
tweets with GPS coordinates, we simply copy the already provided data to InferredLocation
(coordinates) and InferredArea
(text):
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
|
Geolocating tweets using Bing and MapQuest
For locating tweets based on the user's location, we will be calling Bing and MapQuest APIs.
This is done using type providers (see below) and wrapped in a nice MapQuest.locate
and
Bing.locate
functions. We also need to limit rate at which we use these - the following
geolocates one tweet per 5 seconds using MapQuest:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: |
|
Time zones and geolocating with type providers
As in every F# project, I'm making a heavy use of F# Data type providers when calling REST-based geolocation services. As a bonus, I also needed to find time zones of countries of the world, which can be done by extracting the information from List of time zones by country Wikipedia page using the HTML type provider.
Extracting time zone information
The HTML type provider gives us access to the tables on the Wikipedia page and so we can get the country
and time zones just by writing r.Country
and r.''Time Zone''
(using backticks to wrap the space).
As far as I know, Datamaps does not easily let me display multiple time zones per country and so I just
pick the middle time zone:
1: 2: 3: 4: 5: 6: 7: 8: 9: |
|
There are a few explicitly defined countries in the actual source code for countries where the middle time zone is very wrong and for countries that are named differently on Wikipedia.
Geolocating using MapQuest
Both Bing and MapQuest provide a nice REST end-point that we can call using the JSON type provider.
To compose the sample URL, we need to use the Literal
attribute and append a key (which is stored in
a separate config file). The JSON type provider infers the type from the response and gives us nice typed
access to the results:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: |
|
As usual, using the JSON type provider for calling REST APIs makes things very easy.
The Results
property is inferred to be an array of records and information such as
loc.LatLng.Lat
is also statically typed.
Reactive programming with F# agents
The project does quite a lot of interesting reactive event processing. In F#, you can,
of course, use Reactive Extensions (Rx), but I always found Rx
a bit hard to use because they lack simple underlying primitives (more about this in
my rant on library design). F# comes with
a simple set of primitives in the Observable
module which covers some 80% of what you
need and you can easily implement additional primitives using F# agents.
For example, the following is a simple agent that I wrote to limit the rate of requests. The idea is that the agent will emit an event it receives and then it will ignore all other events for the specified number of milliseconds:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: |
|
Agents are the much needed lower level primitive of the Reactive Extensions. You can
quite easily express any logic you need using just a state machine encoded as a recursive
asynchronous loop. The implementation then wraps the agent in a higher-level primitive
Observable.limitRate
that was used in the earlier snippet.
Handling websockets with Suave
One more nice thing in the project is the handling of web sockets. The server serves
static files from the web
sub-directory, but it also communicates with the front-end
via three web sockets (for the map, feed and wordcloud). When a client connects, we
simply want to start sending updates to it from one of the IObservable<T>
events that
we defined earlier (e.g. by serializing tweets from liveTweets
as JSON).
To do this, I first defined a helper socketOfObservable
, which uses Suave's socket { .. }
computation builder and repeatedly awaits an update from the specified updates
and
reports it to via the socket:
1: 2: 3: 4: 5: 6: 7: 8: |
|
The main server is then composed from a number of web parts - the first three handle the communication via web sockets, the fourth one returns information about time zones that we downloaded from Wikipedia and the last two serve static files:
1: 2: 3: 4: 5: 6: 7: 8: |
|
Summary
The main part of the project in app.fsx
is some 350 lines long and I find it pretty amazing
how much you can do in this small number of lines. If you're writing a project like this in F#,
you get to use a number of nice libraries including F# Data Toolbox
for the Twitter API, Suave.io for the web server and F# Data type
providers for calling REST APIs. Finally, I deployed the
service using Azure VM, but you could also use MBrace which can host
web servers in a cluster,
or any other hosting - all the libraries I'm using are cross-platform.
If you're reading this around December 31, 2015 then definitely check out the project running live. I didn't plan to turn this into a reusable application, but who knows! :-) If you want to use it for tracking tweets related to some other events you can find the full source on GitHub under the Apache license - and also get in touch if you have some interesting use for this work!
namespace FSharp
--------------------
namespace Microsoft.FSharp
namespace FSharp.Data
--------------------
namespace Microsoft.FSharp.Data
from Suave
from Suave
from Suave.Http
from Suave.Sockets
from Suave
{Tweeted: DateTime;
Text: string;
OriginalArea: string;
UserName: string;
UserScreenName: string;
PictureUrl: string;
OriginalLocation: (decimal * decimal) option;
Phrase: int;
IsRetweet: bool;
GeoLocationSource: string;
...}
Full name: Happy-new-year-tweets.Tweet
Information we collect about tweets. The `Inferred` fields are calculated later
by geolocating the user, all other information is filled when tweet is received
type DateTime =
struct
new : ticks:int64 -> DateTime + 10 overloads
member Add : value:TimeSpan -> DateTime
member AddDays : value:float -> DateTime
member AddHours : value:float -> DateTime
member AddMilliseconds : value:float -> DateTime
member AddMinutes : value:float -> DateTime
member AddMonths : months:int -> DateTime
member AddSeconds : value:float -> DateTime
member AddTicks : value:int64 -> DateTime
member AddYears : value:int -> DateTime
...
end
Full name: System.DateTime
--------------------
DateTime()
(+0 other overloads)
DateTime(ticks: int64) : unit
(+0 other overloads)
DateTime(ticks: int64, kind: DateTimeKind) : unit
(+0 other overloads)
DateTime(year: int, month: int, day: int) : unit
(+0 other overloads)
DateTime(year: int, month: int, day: int, calendar: Globalization.Calendar) : unit
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : unit
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: DateTimeKind) : unit
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: Globalization.Calendar) : unit
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int) : unit
(+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, kind: DateTimeKind) : unit
(+0 other overloads)
Tweet.Text: string
--------------------
namespace System.Text
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = String
Full name: Microsoft.FSharp.Core.string
Full name: Microsoft.FSharp.Core.option<_>
val decimal : value:'T -> decimal (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.decimal
--------------------
type decimal = Decimal
Full name: Microsoft.FSharp.Core.decimal
--------------------
type decimal<'Measure> = decimal
Full name: Microsoft.FSharp.Core.decimal<_>
val int : value:'T -> int (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.int
--------------------
type int = int32
Full name: Microsoft.FSharp.Core.int
--------------------
type int<'Measure> = int
Full name: Microsoft.FSharp.Core.int<_>
Full name: Microsoft.FSharp.Core.bool
Full name: Happy-new-year-tweets.root
static val DirectorySeparatorChar : char
static val AltDirectorySeparatorChar : char
static val VolumeSeparatorChar : char
static val InvalidPathChars : char[]
static val PathSeparator : char
static member ChangeExtension : path:string * extension:string -> string
static member Combine : [<ParamArray>] paths:string[] -> string + 3 overloads
static member GetDirectoryName : path:string -> string
static member GetExtension : path:string -> string
static member GetFileName : path:string -> string
...
Full name: System.IO.Path
IO.Path.Combine(path1: string, path2: string) : string
IO.Path.Combine(path1: string, path2: string, path3: string) : string
IO.Path.Combine(path1: string, path2: string, path3: string, path4: string) : string
Full name: Happy-new-year-tweets.phrases
"С Новым Годом"; "あけまして おめでとう ございます"; "新年快乐"; "Щасливого Нового Року"; "שנה טובה"
"yeni yılınız kutlu olsun"; "feliz año nuevo"; "happy new year"; "Καλή Χρονιά";"godt nyttår"
"bon any nou"; "felice anno nuovo"; "sretna nova godina"; "godt nytår"; "gelukkig nieuwjaar"
"Frohes neues Jahr"; "urte berri on"; "bonne année"; "boldog új évet"; "gott nytt år"
"szczęśliwego nowego roku"; "blwyddyn newydd dda"; "feliz ano novo"; "sugeng warsa enggal"
Full name: Happy-new-year-tweets.ctx
AccessToken = Config.TwitterAccessToken; AccessSecret = Config.TwitterAccessSecret }
Full name: Happy-new-year-tweets.twitter
type Twitter =
new : context:TwitterContext -> Twitter
member RequestRawData : url:string * query:(string * string) list -> string
member Connections : Connections
member Search : Search
member Streaming : Streaming
member Timelines : Timelines
member Users : Users
static member Authenticate : consumer_key:string * consumer_secret:string -> TwitterConnector
static member AuthenticateAppOnly : consumer_key:string * consumer_secret:string -> Twitter
static member TwitterWeb : unit -> WebBrowser
Full name: FSharp.Data.Toolbox.Twitter.Twitter
--------------------
new : context:TwitterContext -> Twitter
Full name: Happy-new-year-tweets.search
Full name: Happy-new-year-tweets.liveTweets
module Observable
from AsyncHelpers
--------------------
module Observable
from Microsoft.FSharp.Control
Full name: Microsoft.FSharp.Control.Observable.choose
union case Opcode.Text: Opcode
--------------------
namespace System.Text
OriginalLocation = origLocation; Phrase = getPhrase text
InferredArea = None; InferredLocation = None;
IsRetweet = isRT status; GeoLocationSource = "NA"
Full name: Microsoft.FSharp.Control.Observable.add
Full name: AsyncHelpers.Observable.limitRate
Limits the rate of emitted messages to at most one per the specified number of milliseconds
Full name: AsyncHelpers.Observable.mapAsyncIgnoreErrors
Behaves like `Observable.map`, but does not stop when error happens
Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.async
module Option
from Suave.Utils
--------------------
module Option
from Microsoft.FSharp.Core
Full name: Microsoft.FSharp.Core.Option.map
Full name: Suave.Http.choose
Full name: Microsoft.FSharp.Core.Operators.id
Full name: Happy-new-year-tweets.TimeZones
Full name: FSharp.Data.HtmlProvider
<summary>Typed representation of an HTML file.</summary>
<param name='Sample'>Location of an HTML sample file or a string containing a sample HTML document.</param>
<param name='PreferOptionals'>When set to true, inference will prefer to use the option type instead of nullable types, `double.NaN` or `""` for missing values. Defaults to false.</param>
<param name='IncludeLayoutTables'>Includes tables that are potentially layout tables (with cellpadding=0 and cellspacing=0 attributes)</param>
<param name='MissingValues'>The set of strings recogized as missing values. Defaults to `NaN,NA,#N/A,:,-,TBA,TBD`.</param>
<param name='Culture'>The culture used for parsing numbers and dates. Defaults to the invariant culture.</param>
<param name='Encoding'>The encoding used to read the sample. You can specify either the character set name or the codepage number. Defaults to UTF8 for files, and to ISO-8859-1 the for HTTP requests, unless `charset` is specified in the `Content-Type` response header.</param>
<param name='ResolutionFolder'>A directory that is used when resolving relative file references (at design time and in hosted execution).</param>
<param name='EmbeddedResource'>When specified, the type provider first attempts to load the sample from the specified resource
(e.g. 'MyCompany.MyAssembly, resource_name.html'). This is useful when exposing types generated by the type provider.</param>
Full name: Happy-new-year-tweets.reg
type Regex =
new : pattern:string -> Regex + 1 overload
member GetGroupNames : unit -> string[]
member GetGroupNumbers : unit -> int[]
member GroupNameFromNumber : i:int -> string
member GroupNumberFromName : name:string -> int
member IsMatch : input:string -> bool + 1 overload
member Match : input:string -> Match + 2 overloads
member Matches : input:string -> MatchCollection + 1 overload
member Options : RegexOptions
member Replace : input:string * replacement:string -> string + 5 overloads
...
Full name: System.Text.RegularExpressions.Regex
--------------------
Regex(pattern: string) : unit
Regex(pattern: string, options: RegexOptions) : unit
Full name: Happy-new-year-tweets.timeZones
Regex.Matches(input: string, startat: int) : MatchCollection
type LiteralAttribute =
inherit Attribute
new : unit -> LiteralAttribute
Full name: Microsoft.FSharp.Core.LiteralAttribute
--------------------
new : unit -> LiteralAttribute
Full name: Happy-new-year-tweets.MapQuestSample
Full name: Happy-new-year-tweets.MapQuest
Full name: FSharp.Data.JsonProvider
<summary>Typed representation of a JSON document.</summary>
<param name='Sample'>Location of a JSON sample file or a string containing a sample JSON document.</param>
<param name='SampleIsList'>If true, sample should be a list of individual samples for the inference.</param>
<param name='RootName'>The name to be used to the root type. Defaults to `Root`.</param>
<param name='Culture'>The culture used for parsing numbers and dates. Defaults to the invariant culture.</param>
<param name='Encoding'>The encoding used to read the sample. You can specify either the character set name or the codepage number. Defaults to UTF8 for files, and to ISO-8859-1 the for HTTP requests, unless `charset` is specified in the `Content-Type` response header.</param>
<param name='ResolutionFolder'>A directory that is used when resolving relative file references (at design time and in hosted execution).</param>
<param name='EmbeddedResource'>When specified, the type provider first attempts to load the sample from the specified resource
(e.g. 'MyCompany.MyAssembly, resource_name.json'). This is useful when exposing types generated by the type provider.</param>
<param name='InferTypesFromValues'>If true, turns on additional type inference from values.
(e.g. type inference infers string values such as "123" as ints and values constrained to 0 and 1 as booleans.)</param>
Full name: Happy-new-year-tweets.locate
Full name: Config.MapQuestKey
type HttpUtility =
new : unit -> HttpUtility
static member HtmlAttributeEncode : s:string -> string + 1 overload
static member HtmlDecode : s:string -> string + 1 overload
static member HtmlEncode : s:string -> string + 2 overloads
static member JavaScriptStringEncode : value:string -> string + 1 overload
static member ParseQueryString : query:string -> NameValueCollection + 1 overload
static member UrlDecode : str:string -> string + 3 overloads
static member UrlDecodeToBytes : str:string -> byte[] + 3 overloads
static member UrlEncode : str:string -> string + 3 overloads
static member UrlEncodeToBytes : str:string -> byte[] + 3 overloads
...
Full name: System.Web.HttpUtility
--------------------
HttpUtility() : unit
HttpUtility.UrlEncode(str: string) : string
HttpUtility.UrlEncode(str: string, e: Encoding) : string
HttpUtility.UrlEncode(bytes: byte [], offset: int, count: int) : string
Loads JSON from the specified uri
JsonProvider<...>.Load(reader: IO.TextReader) : JsonProvider<...>.Root
Loads JSON from the specified reader
JsonProvider<...>.Load(stream: IO.Stream) : JsonProvider<...>.Root
Loads JSON from the specified stream
from Microsoft.FSharp.Collections
Full name: Microsoft.FSharp.Collections.Seq.choose
Full name: Microsoft.FSharp.Collections.Seq.map
type RateLimitAgent<'T> =
new : timeout:float -> RateLimitAgent<'T>
member AddEvent : event:'T -> unit
member EventOccurred : IEvent<'T>
Full name: Happy-new-year-tweets.RateLimitAgent<_>
Limits the rate of emitted messages to at most
one per the specified number of milliseconds
--------------------
new : timeout:float -> RateLimitAgent<'T>
module Event
from Microsoft.FSharp.Control
--------------------
type Event<'T> =
new : unit -> Event<'T>
member Trigger : arg:'T -> unit
member Publish : IEvent<'T>
Full name: Microsoft.FSharp.Control.Event<_>
--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate)> =
new : unit -> Event<'Delegate,'Args>
member Trigger : sender:obj * args:'Args -> unit
member Publish : IEvent<'Delegate,'Args>
Full name: Microsoft.FSharp.Control.Event<_,_>
--------------------
new : unit -> Event<'T>
--------------------
new : unit -> Event<'Delegate,'Args>
type MailboxProcessor<'Msg> =
interface IDisposable
new : body:(MailboxProcessor<'Msg> -> Async<unit>) * ?cancellationToken:CancellationToken -> MailboxProcessor<'Msg>
member Post : message:'Msg -> unit
member PostAndAsyncReply : buildMessage:(AsyncReplyChannel<'Reply> -> 'Msg) * ?timeout:int -> Async<'Reply>
member PostAndReply : buildMessage:(AsyncReplyChannel<'Reply> -> 'Msg) * ?timeout:int -> 'Reply
member PostAndTryAsyncReply : buildMessage:(AsyncReplyChannel<'Reply> -> 'Msg) * ?timeout:int -> Async<'Reply option>
member Receive : ?timeout:int -> Async<'Msg>
member Scan : scanner:('Msg -> Async<'T> option) * ?timeout:int -> Async<'T>
member Start : unit -> unit
member TryPostAndReply : buildMessage:(AsyncReplyChannel<'Reply> -> 'Msg) * ?timeout:int -> 'Reply option
...
Full name: Microsoft.FSharp.Control.MailboxProcessor<_>
--------------------
new : body:(MailboxProcessor<'Msg> -> Async<unit>) * ?cancellationToken:Threading.CancellationToken -> MailboxProcessor<'Msg>
Full name: Happy-new-year-tweets.RateLimitAgent`1.EventOccurred
Triggered when an event happens
Full name: Happy-new-year-tweets.RateLimitAgent`1.AddEvent
Send an event to the agent
Full name: Happy-new-year-tweets.socketOfObservable
module WebSocket
from Suave
--------------------
type WebSocket =
new : connection:Connection -> WebSocket
member read : unit -> Async<Choice<(Opcode * byte [] * bool),Error>>
member send : opcode:Opcode -> bs:byte [] -> fin:bool -> Async<Choice<unit,Error>>
Full name: Suave.WebSocket.WebSocket
--------------------
new : connection:Connection -> WebSocket
Full name: Suave.Sockets.Control.SocketMonad.socket
module Async
from Suave.Utils
--------------------
type Async
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
static member Ignore : computation:Async<'T> -> Async<unit>
static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
static member Sleep : millisecondsDueTime:int -> Async<unit>
static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>>
static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit
static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit>
static member SwitchToNewThread : unit -> Async<unit>
static member SwitchToThreadPool : unit -> Async<unit>
static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
static member CancellationToken : Async<CancellationToken>
static member DefaultCancellationToken : CancellationToken
Full name: Microsoft.FSharp.Control.Async
--------------------
type Async<'T>
Full name: Microsoft.FSharp.Control.Async<_>
Creates an asynchronous workflow that will be resumed when the
specified observables produces a value. The workflow will return
the value produced by the observable.
module SocketOp
from Suave.Sockets
--------------------
type SocketOp<'a> = Async<Choice<'a,Error>>
Full name: Suave.Sockets.SocketOp<_>
Full name: Suave.Sockets.SocketOp.ofAsync
from Suave.Utils
Full name: Suave.Utils.UTF8.bytes
Full name: Happy-new-year-tweets.part
Full name: Suave.Http.Applicatives.path
Full name: Suave.WebSocket.handShake
from Suave.Http
Full name: Suave.Http.Successful.OK
from Suave.Http
Full name: Suave.Http.Files.browseFile
Full name: Suave.Http.Files.browse
Published: Wednesday, 30 December 2015, 7:09 PM
Author: Tomas Petricek
Typos: Send me a pull request!
Tags: f#, data journalism, thegamma, data science, visualization