using Programming;

A Blog about some of the intrinsics related to programming and how one can get the best out of various languages.

Importing F# to the SQLCLR (T-SQL)

Bringing F# into the SQLCLR

It's been some-time since my last post, and don't worry, we're still going to continue the IMAP server. I've been swamped at work, and as a result, haven't had the time to properly dedicate to writing these posts (especially that series, which is a complex topic).

Excuses aside, today we're going to talk about something I recently did for work, which is integrating F# into the SQLCLR (part of Microsoft SQL Server).

For those who don't know, the SQLCLR is a feature of SQL Server that allows one to import .NET assemblies as user-defined functions, or stored procedures. On it's own it doesn't sound impressive, but the SQLCLR allows us to significantly improve performance in some cases, and moderately improve it in others.

I won't go into detail explaining the SQLCLR, a gentleman by the name of Soloman Rutzky does that quite well. I'll let his "Stairway to SQLCLR" give you the introduction.

No, what I'll do today is show you how to import F# into the SQLCLR, instead of just C# or VB.NET. The process is about as straightforward as Soloman describes, but there are a few "gotcha's", so I'm going to include those in our discussion here today.

Without further ado, let's get started.

First: create the project and add the System.Data reference

The first step is obviously to create a project to hold our SQL code. The project should be an F# Class Library, in .NET Framework (I'm using 4.7.1 and F# Core 4.4.3.0). You'll want a module for the functions, and in that module you'll want to open Microsoft.SqlServer.Server, and System.Data.SqlTypes.

Once we've done that, we'll build a function. There are a few rules to creating a function in .NET that can be seen by SQL Server:

  1. The function must have the SqlFunction attribute;
  2. All inputs must be tupled;
  3. All input and output types must be a Sql[Something] type (SqlDouble, SqlInt, etc.);

So, for our example we're going to use a real-world example from my work: distance calculation from two geo-coded points.

To do this, we'll build a function that takes 4 double values: two Latitude/Longitude value sets.

let calculateDistance (fromLat : SqlDouble, fromLon : SqlDouble, toLat : SqlDouble, toLon : SqlDouble) : SqlDouble

That's the signature we'll use, next, we want to define how SQL should treat the function:

[<SqlFunction(
    IsDeterministic = true,
    IsPrecise = false,
    SystemDataAccess = SystemDataAccessKind.None,
    DataAccess = DataAccessKind.None)>]

This is where life gets special, so let me explain them piece-by-piece:

  • SqlFunction: this is just the attribute we use, there is also SqlProcedure for stored procedures;
  • IsDeterministic = true: this value should ONLY be set to true if the function is deterministic, that is, given any input value, it returns one and exactly one output, and that two calls to the function with the same input will result in the same output;
  • IsPrecise = false: this value should ONLY be set to true if the function uses the DECIMAL or NUMERIC types, and does precise mathematical calculations;
  • SystemDataAccess = SystemDataAccessKind.None: I'll be completely honest with you, I don't know what the difference between this and DataAccess are, but if you do any reading/writing to/from SQL, you should set it to Read, otherwise, probably use None (there's a small performance cost to setting this to Read, I leave it to you to decide whether or not to do so);
  • DataAccess = DataAccessKind.None: see above;

So basically, what we did here is define a function and tell SQL what it should expect the function to do. One of the most impotant parts is the IsDeterministic flag: this tells SQL that if it called the function for a set of values, it can reuse that result for any subsequent calls with the same set of values. This means it can memoize the results. If your function has side-effects, do not set this flag to true, or you will get weird results. Basically, if your function is truly "pure" (no side-effects), mark it with IsDeterministic = true.

Next: write the code

Alright, so we've covered the hard parts, next, we write the function.

My version of this function used some logic that was specific to my workplace, so I'm going to remove it and we'll write a vanilla function:

let constMod = 1.852 / 1.61 * 60.
let divPi180 = Math.PI / 180.
let div180Pi = 180. / Math.PI

[<SqlFunction(
    IsDeterministic = true,
    IsPrecise = false,
    SystemDataAccess = SystemDataAccessKind.None,
    DataAccess = DataAccessKind.None)>]
let calculateDistance (fromLat : SqlDouble, fromLon : SqlDouble, toLat : SqlDouble, toLon : SqlDouble) : SqlDouble =
    let fromLat = fromLat.Value
    let fromLon = fromLon.Value
    let toLat = toLat.Value
    let toLon = toLon.Value

    let fromLat = fromLat * divPi180
    let toLat = toLat * divPi180
    let fromLon = fromLon * divPi180
    let toLon = toLon * divPi180

    constMod *
    (Math.Acos
        ((Math.Sin toLon) * (Math.Sin fromLon) +
         (Math.Cos toLon) * (Math.Cos fromLon) * (Math.Cos (toLat - fromLat))))
    |> SqlDouble

This should be self-explanatory: we basically convert the data and do some simple math on it.

Third: enable SQLCLR

Alright, so that's that entirety of our .NET code.

Now, we need to enable the SQLCLR, because it's disabled by default.

The SQLCLR can be enabled through GUI or T-SQL, I prefer to do it through GUI because I typo a lot.

To enable it:

  1. Right click your server in SSMS;
  2. Click "Facets";
  3. In the "Facet" dropdown select "Surface Area Configuration";
  4. Change "ClrIntegrationEnabled" to "True";
  5. Click "OK";

Easy enough.

Fourth: trust the assembly, and import it

This is one spot where things aren't completely awesome: the FSharp.Core library isn't built to natively support a "SAFE" import to SQLCLR, so we have to trust it first.

To trust the assemblies, we'll want to get a SHA2_512 hash of them, and optionally, a description.

I, personally, don't care so much about the description at the moment, so I'll leave that out and let you locate it if you like. Instead, I'm just going to demonstrate how to hash it and trust it.

We need to trust FSharp.Core, and then our assembly:

DECLARE @hash AS BINARY(64) = (SELECT HASHBYTES('SHA2_512', (SELECT * FROM OPENROWSET (BULK 'C:\path\to\bin\dir\FSharp.Core.dll', SINGLE_BLOB) AS [Data])))
EXEC sp_add_trusted_assembly @hash

Then, our assembly:

DECLARE @hash AS BINARY(64) = (SELECT HASHBYTES('SHA2_512', (SELECT * FROM OPENROWSET (BULK 'C:\path\to\bin\dir\MyAssembly.dll', SINGLE_BLOB) AS [Data])))
EXEC sp_add_trusted_assembly @hash

Easy enough.

Because FSharp.Core isn't built for native SQL Server support (which, if anyone want's to fix, I've included the error at the end of this article), we have to add it with PERMISSION_SET = UNSAFE, which is, well...unsafe.

So, to load our assembly, we need a name, and the path:

CREATE ASSEMBLY [MyAssembly]
AUTHORIZATION dbo
FROM 'C:\path\to\bin\dir\MyAssembly.dll'
WITH PERMISSION_SET = SAFE

Not particularly hard. The name ([MyAssembly]) is not restricted to anything other than the regular NVARCHAR(128) for sysname, it does not need to match anything from the DLL, but probably easier if it does.

Finally: create the function

Alright, so our assembly is imported, we have it available, the last part is creating the function.

To create the function, we start it off like a normal T-SQL UDF:

CREATE FUNCTION CalculateDistance
(
    @fromLat FLOAT,
    @fromLon FLOAT,
    @toLat FLOAT,
    @toLon FLOAT
)
RETURNS FLOAT

If you've ever written a T-SQL Scalar-Valued UDF, this should look familiar. We build the signature exactly as we defined it in F#, and that part is super important: the signature cannot vary at all.

Next, we write the UDF:

AS EXTERNAL NAME [MyAssembly].[MyAssembly.Namespace.ModuleName].calculateDistance

The EXTERNAL NAME is a three part name:

  1. The assembly name as specified in CREATE ASSEMBLY;
  2. The assembly namespace and module name, the fully-qualified name of the first outer-container of the function we need;
  3. The function name itself;

Once you've created the function, we're literally all done. You can now call directly into your CLR code:

SELECT dbo.CalculateDistance(@fromLat, @fromLon, @toLat, @toLon)

Demonstrations!

For those who want to see the performance difference, the original T-SQL function is:

CREATE FUNCTION CalculateDistanceUdf
(
    @fromLat FLOAT,
    @fromLon FLOAT,
    @toLat FLOAT,
    @toLon FLOAT
)
RETURNS FLOAT
WITH SCHEMABINDING
AS 
BEGIN
    RETURN (1.852 / 1.61) *
        60 *
        DEGREES(
            ACOS(
                SIN(RADIANS(@toLon)) *
                SIN(RADIANS(@fromLon)) +
                COS(RADIANS(@toLon)) *
                COS(RADIANS(@fromLon)) *
                COS(RADIANS(@toLat) - RADIANS(@fromLat))))
END

The WITH SCHEMABINDING is a hint to try to tell SQL Server to mark the function deterministic, and it is as verified with SELECT OBJECTPROPERTY(OBJECT_ID('[dbo].[CalculateDistanceUdf]'), 'IsDeterministic'), but it still performs significantly slower than the SQLCLR alternative.

I borrowed the test from this article to run mine, and wrote them as follows:

CREATE TABLE Numbers (
    Num INT NOT NULL,
    CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED (Num)
)
GO

WITH N1(C) AS (SELECT 0 UNION ALL SELECT 0),
N2(C) AS (SELECT 0 FROM N1 AS T1 CROSS JOIN N1 AS T2),
N3(C) AS (SELECT 0 FROM N2 AS T1 CROSS JOIN N2 AS T2),
N4(C) AS (SELECT 0 FROM N3 AS T1 CROSS JOIN N3 AS T2),
N5(C) AS (SELECT 0 FROM N4 AS T1 CROSS JOIN N4 AS T2),
N6(C) AS (SELECT 0 FROM N4 AS T1 CROSS JOIN N4 AS T2 CROSS JOIN N3 AS T3),
Nums(Num) AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM N6)
INSERT INTO Numbers(Num) SELECT Num FROM Nums
GO

This inserts 1048576 rows to the Numbers table, so it's a good-sized test.

Then we can run each of the following three tests:

DECLARE @fromLat AS FLOAT = 100
DECLARE @fromLon AS FLOAT = 100
DECLARE @toLat AS FLOAT = 120
DECLARE @toLon AS FLOAT = 120

SELECT MAX(dbo.CalculateDistance(Num / @fromLat, Num / @fromLon, Num / @toLat, Num / @toLon)) FROM Numbers
GO

DECLARE @fromLat AS FLOAT = 100
DECLARE @fromLon AS FLOAT = 100
DECLARE @toLat AS FLOAT = 120
DECLARE @toLon AS FLOAT = 120

SELECT MAX(dbo.CalculateDistanceUdf(Num / @fromLat, Num / @fromLon, Num / @toLat, Num / @toLon)) FROM Numbers
GO

DECLARE @fromLat AS FLOAT = 100
DECLARE @fromLon AS FLOAT = 100
DECLARE @toLat AS FLOAT = 120
DECLARE @toLon AS FLOAT = 120

SELECT MAX
    (
        (1.852 / 1.61) *
        60 *
        DEGREES(
            ACOS(
                SIN(RADIANS(Num / @toLon)) *
                SIN(RADIANS(Num / @fromLon)) +
                COS(RADIANS(Num / @toLon)) *
                COS(RADIANS(Num / @fromLon)) *
                COS(RADIANS(Num / @toLat) - RADIANS(Num / @fromLat)))))
FROM Numbers
GO

You can run these each individually to time them. My times were roughly 645ms for the SQLCLR, 3369ms for the T-SQL UDF, and 703ms for the inline T-SQL. As you can see, the SQLCLR function is faster than the inline T-SQL, and let's us encapsulate the logic in a single function. (This actually came about as an issue because we have the calculation there copied-and-pasted over several dozen queries, often 3-8x per query.)

So, that said, in this type of situation (raw math) there's no reason to use T-SQL for the task, and for something reasonably complex like this, no reason not to abstract it. Dump the code in .NET, write your unit tests, and then deploy the assembly to the SQL server.

Now, that said, there are times I wouldn't use a SQLCLR function, such as when the math is ultra simple: i.e. * 3, and there are times when a table-valued UDF would be far superior, so I don't want to make the suggestion that this will always help, just that it's another thing you can try, and it might actually surprise you.


For anyone curious, attempting to create an assembly in F# throws the following warning:

Warning: The Microsoft .NET Framework assembly 'fsharp.core, version=4.4.3.0, culture=neutral, publickeytoken=b03f5f7f11d50a3a, processorarchitecture=msil.' you are registering is not fully tested in the SQL Server hosted environment and is not supported. In the future, if you upgrade or service this assembly or the .NET Framework, your CLR integration routine may stop working. Please refer SQL Server Books Online for more details.

And using a PERMISSION_SET of EXTERNAL_ACCESS or SAFE throws the following error:

CREATE ASSEMBLY failed because type 'Microsoft.FSharp.Collections.FSharpMap`2' in safe assembly 'FSharp.Core' has a static field 'empty'. Attributes of static fields in safe assemblies must be marked readonly in Visual C#, ReadOnly in Visual Basic, or initonly in Visual C++ and intermediate language.

Constructing an IMAP Server from Scratch - Part 2

Implementing Server-Side RFC 3501 (IMAP) in F# (Part 2)

Previous: Implementing Server-Side RFC 3501 (IMAP) in F# (Part 1)

Alright, so the previous blog post got us introducded to our RFC 3501 (IMAP) implementation. We looked at the basic TCP/IP stack of the architecture. Today, we're going to actually start building part of the IMAP server itself.

To start building the IMAP server, we need to start building a mock file-system. Now me, personally, I'm about keeping things as simple as possible. As a result, the mock file-server I'm building is not going to do anything fancy. We'll use a DemoFiles folder in the IMAP server for all the data, then within that folder we'll have a Configuration file, then a Message_{Id} file for each message. The Configuration file will have some basic stuff:

  • Password: to verify the user against on authentication;
  • UIDVALIDITY: that value that RFC 3501 says can never change;
  • NextUniqueIdentifier: the value that RFC 3501 dictates must change only when a message is received;

Next, each Message_{Id} file needs to have a few of it's own pieces:

  • Attributes: at least all of the following attributes:
    • \Seen
    • \Answered
    • \Flagged
    • \Deleted
    • \Draft
  • Date/Time Received: the date and time that the message was received by the server (typically via SMTP);
  • Message Size: the number of 8-bit octets in the message;
  • Envelope Structure: the parsed header of the message;
  • Body Structure: the parsed body (MIME-IME) of the message;
  • Message Text: the text(s) in the message;

We'll worry about messages later, for now I just want to concern ourselves with the configuration, as that's the first part of implementing IMAP.

Building a Configuration File

Now we have several methods for building the configuration file: XML, JSON, INI, CSV, etc. Because I'm lazy, and because this is a sample storage system, I'm going to build it as a line-delimited file. I.e.: each line is a value in the config. (Easy to write by hand, easy to ready by hand, easy to parse with code, all-in-all, pretty easy.)

Our config file will look like:

Plaintext Password (For easy troubleshooting)
The UIDVALIDITY value (Constant)
The Next Unique Identifier value (Changes when a message comes in)

My sample configuration:

Password123
307110933
1

Building Storage Events

Using this basic configuration, we can start to modify our server to take note of file read/write. We can start modifying the IMAP server to start using some storage events.

type StorageEvents =
    { Authenticate : string * string -> User option
      GetUidValidity : User -> int
      GetNextUniqueId : User -> int }

So we can start developing some basic events here. We want to be able to tell if a user can be authenticated (take a username and password and output a Some User if it's valid, or None if not). We want to get the UIDVALIDITY value (take a User and return an int), and we want to get the next unique ID (take a User and output an int).

Now we don't really take commands from the server yet, but we can shoehorn a test or two in:

let objectToStr o = o.ToString()
let rec runClient (socket : Socket) =
    let printfn = printfn "Socket %s: %s" (socket.RemoteEndPoint.ToString())
    let user = Some { Id = "ebrown@example.com" }
    async {
        let! buffer = () |> socket.AsyncReceiveAll
        let str = buffer |> Encoding.ASCII.GetString
        sprintf "Received (%i bytes): %s" buffer.Length str |> printfn
        if str.Length = 3 && str = "BYE" then printfn "Disconnected"
        else
            let! bytesSent = [| "Hello, other world!"B |> Some; user |> Option.map (storageEvents.GetUidValidity >> objectToStr >> Seq.map byte >> Seq.toArray) |] |> Array.choose id |> Array.concat |> socket.AsyncSend
            bytesSent |> sprintf "Sent response (%i bytes)" |> printfn
            return! socket |> runClient }

We'll send back the UIDVALIDITY value every time we get a packet from a client, that should be a good proof-of-concept.

Our server itself needs to change (ever-so-slightly): type Server (storageEvents) =, this will allow us to pass the storageEvents in, and the server has no idea what or how the underlying storage mechanism works (nor does it care).

Defining the entire storage system to work is really, really trivial:

type Configuration =
    { Password : string
      UidValididty : int
      NextUniqueId : int }
let getConfig (f : string) =
    let lines = System.IO.Path.Combine(@"DemoFiles", f, @"Configuration.txt") |> System.IO.File.ReadAllLines
    { Password = lines.[0]; UidValididty = lines.[1] |> int; NextUniqueId = lines.[2] |> int }
let serverEvents = 
    { Authenticate = (fun (u, p) -> if (u |> getConfig).Password = p then Some { Id = u } else None)
      GetUidValidity = (fun u -> (u.Id |> getConfig).UidValididty)
      GetNextUniqueId = (fun u -> (u.Id |> getConfig).NextUniqueId) }

We defined a Configuration type, a function to parse it, and then created a serverEvents object to hold all the functions for working with the storage system. Our main function changes little:

use server = Server(serverEvents).Start()
Console.ReadLine() |> ignore
printfn "Closing..."
0

We just put serverEvents in now.

If we put it all together to start testing it, we'll get Received 28 bytes: Hello, other world!307110933 on the client, which proves that our storage works.

We can push this off to a Storage module, to get it out of our work-area:

module EBrown.Imap.Server.Storage
open EBrown.Imap.Core

type Configuration =
    { Password : string
      UidValididty : int
      NextUniqueId : int }
let getConfig (f : string) =
    let lines = System.IO.Path.Combine(@"DemoFiles", f, @"Configuration.txt") |> System.IO.File.ReadAllLines
    { Password = lines.[0]; UidValididty = lines.[1] |> int; NextUniqueId = lines.[2] |> int }
let getStorageEvents () = 
    { Authenticate = (fun (u, p) -> if (u |> getConfig).Password = p then Some { Id = u } else None)
      GetUidValidity = (fun u -> (u.Id |> getConfig).UidValididty)
      GetNextUniqueId = (fun u -> (u.Id |> getConfig).NextUniqueId) }

Implementing IMAP Commands

Alright, so now that we have a storage mechanism, we want to start implementing some IMAP commands and state and such.

IMAP commands are either tagged or untagged, and comprise of a command name, and possibly arguments. Thus a command would look like <TAG> <COMMANDNAME> <ARGUMENTS>, where <TAG> is either a client-generated string, or a * if the command is untagged. There are a few, basic commands, that can be implemented regardless of the state of the IMAP connection:

  • CAPABILITY: the capability command is tagged and has no arguments. The response should be one untagged response with a list of what capabilities are currently supported by the server, and the server should send one tagged OK response (or a tagged BAD response if the command is badly formed).
  • NOOP: the noop command is tagged and has no arguments. There is no specific response, but it can be used as a polling command as any command can return a status update. The server should send one tagged OK (or BAD) response after sending status update data, if appropriate.
  • LOGOUT: the logout command is tagged and has no arguments. The response should be one untagged BYE, and one OK or BAD.

The format of an OK response to a command is <TAG> OK <COMMANDNAME> completed.

Thus, if a client sends a a002 NOOP, the server OK response would be a002 OK NOOP completed.

With all this in mind, we can start building a command parser and generator.

The first part we need is a Tag. This is honestly quite trivial:

type Tag = | Untagged | Tagged of string
let getTag = function | Untagged -> "*" | Tagged s -> s

As commands are either tagged or untagged, this makes life really easy.

Next, we need a general generateLine, which can be used to generate any given command or response:

let joinStrings (sep : string) (sarr : string array) = System.String.Join(sep, sarr)
let generateLine tag largs name rargs =
    [|[|tag |> getTag |> Some|]; largs; [|name |> Some|]; rargs|]
    |> Array.concat
    |> Array.choose id
    |> joinStrings " "

We generalize this enough so that we can use it for any command, which makes life extremely trivial to build a generateCommand:

let generateCommand tag name args = generateLine (Tagged tag) [||] name (args |> Array.map Some)

Now we can generalize command generation, let's build a capability command:

let capability tag = generateCommand tag "CAPABILITY" [||]
let capabilityResponse tag args = [|generateLine Untagged [||] "CAPABILITY" (args |> Array.map Some); generateLine (Tagged tag) [|"OK" |> Some|] "CAPABILITY" [|"completed" |> Some|]|]

Again, we're keeping it mostly simple.

The next step is to parse a command. This will be just as easy as generation, we'll define new types and a function to handle the input.

type Command = | Capability
type InputCommand = { Tag : Tag; Command : Command }
let parseCommand (command : string) =
    let parseTag = function | "*" -> Untagged | s -> Tagged s
    let parseCommandName =
        function
        | [|"CAPABILITY"|] -> Capability |> Some
        | _ -> None
    let parts = command.Split(' ')
    parts.[1..] |> parseCommandName |> Option.map (fun c -> { Tag = parts.[0] |> parseTag; Command = c })

Again, quite simple. We parse the tag out, then we parse the command out, and return the result.

So with this, extending to add a NOOP command is really easy:

type Command = | Capability | Noop
....
let noop tag = generateCommand tag "NOOP" [||]
let noopResponse tag = [|generateLine (Tagged tag) [||] "NOOP" [|"completed" |> Some|]|]
....
let parseCommand (command : string) =
    let parseTag = function | "*" -> Untagged | s -> Tagged s
    let parseCommandName =
        function
        | [|"CAPABILITY"|] -> Capability |> Some
        | [|"NOOP"|] -> Noop |> Some
        | _ -> None
    let parts = command.Split(' ')
    parts.[1..] |> parseCommandName |> Option.map (fun c -> { Tag = parts.[0] |> parseTag; Command = c })

And now we have NOOP support.

Lastly, LOGOUT command support:

let logout tag = generateCommand tag "LOGOUT" [||]
let logoutResponse tag = [|generateLine Untagged [||] "BYE" ([|"IMAP4rev1"; "Server"; "logging"; "out"|] |> Array.map Some); generateLine (Tagged tag) [||] "LOGOUT" [|"completed" |> Some|]|]
let parseCommand (command : string) =
    let parseTag = function | "*" -> Untagged | s -> Tagged s
    let parseCommandName =
        function
        | [|"CAPABILITY"|] -> Capability |> Some
        | [|"NOOP"|] -> Noop |> Some
        | [|"LOGOUT"|] -> Logout |> Some
        | _ -> None
    let parts = command.Split(' ')
    parts.[1..] |> parseCommandName |> Option.map (fun c -> { Tag = parts.[0] |> parseTag; Command = c })

Now as we work with this, we should realize that we can actually remove most of the specific functions, and use the InputCommand for the capability, noop, and logout functions we built:

type ClientCommandName = | Capability | Noop | Logout
type ClientCommand = { Tag : Tag; Command : ClientCommandName }
let generateClientCommandName =
    function
    | Capability -> ("CAPABILITY", [||])
    | Noop -> ("NOOP", [||])
    | Logout -> ("LOGOUT", [||])
let generateClientCommand (command : ClientCommand) = command.Command |> generateClientCommandName ||> generateLine command.Tag [||]
let parseClientCommand (command : string) =
    let parseTag = function | "*" -> Untagged | s -> Tagged s
    let parseCommandName =
        function
        | [|"CAPABILITY"|] -> Capability |> Some
        | [|"NOOP"|] -> Noop |> Some
        | [|"LOGOUT"|] -> Logout |> Some
        | _ -> None
    let parts = command.Split(' ')
    parts.[1..] |> parseCommandName |> Option.map (fun c -> { ClientCommand.Tag = parts.[0] |> parseTag; Command = c })

type OkBadResult = | Ok | Bad
let getOkBad = function | Ok -> ("OK", "completed") | Bad -> ("BAD", "")
type ServerCommandName = | Capability of OkBadResult * string array | Noop of OkBadResult | Logout of OkBadResult
type ServerCommand = { Tag : Tag; Command : ServerCommandName }
let generateServerCommandName (command : ServerCommand) =
    match command.Command with 
    | Capability (res, options) ->
        let lRes, rRes = res |> getOkBad
        [|(Untagged, [||], "CAPABILITY", options); (command.Tag, [|lRes|], "CAPABILITY", [|rRes|])|]
    | Noop res ->
        let lRes, rRes = res |> getOkBad
        [|command.Tag, [|lRes|], "NOOP", [|rRes|]|]
    | Logout res ->
        let lRes, rRes = res |> getOkBad
        [|(Untagged, [||], "BYE", [|"IMAP4rev1 Server logging out"|]); command.Tag, [|lRes|], "LOGOUT", [|rRes|]|]
let generateServerCommand (command : ServerCommand) = command |> generateServerCommandName |> Array.map (fun (t, l, n, r) -> generateLine t (l |> Array.map Some) n (r |> Array.map Some))

So now we have a manner of building commands, somewhat dynamically. We can construct a command from what it needs, which means we can continue on with the next step: authentication / authorization. That will be the subject of the next blog post, as I want to allow a lot of this to sink in, so that we can hit the ground running in the next situation. We have a whole hell-of-a-lot to do yet, so we'll take it in shorter, more manageable bits.


As a reminder, the code for this is on GitHub. The version for this specific blog post is tree 8d7b3f15777d0be7cd78903bb5f0c94d8175230d.


Due to time constraints, I didn't make it as far as I would have liked with this post. But never fear, consistent, slow progress is better than inconsistent progress.

Constructing an IMAP Server from Scratch - Part 1

Implementing Server-Side RFC 3501 (IMAP) in F# (Part 1)

So it's been a while, I've had little free time over the last couple months (December through February are my extrememly busy period), but I want to start a new project today, inspired by a complete failure of my existing mail provider to work properly.

Start with RTFM (or RFC's, in this case)

Now IMAP, as a protocol, is extraordinarily complicated. It is primarily detailed by RFC 3501, but many other RFC's have come in over the years to update it. As a result, we have 23 RFC's that relate to IMAP directly, and several more indirectly. I'm going to list the most important ones (in numeric order) first, then the extraneious ones.

Primarily, we have 18 RFC's to work with: 1730, 2060, 2061, 2088, 2342, 3501, 3502, 3516, 4466, 4469, 4551, 5032, 5182, 5738, 6237, 6855, 7162, and 7377.

We also have 5 additional RFC's for MIME that are extremely important: 2045. 2046, 2047, 2048, and 2049.

Additionally, there are 7 other RFC's that are directly relevant, though we can ignore for now: 2595, 5068, 5550, 6186, 6858, 7817, and 8314.

Finally, we have 6 other RFC's we'll need to think about: 2822, 5322, 5335, 5336, 6531, and 6854.


I don't expect you to read all of these, but you should know where to find them. We'll actually deal with most of the 36 RFC's detailed here tangentially, and I won't go into much detail on what parts of which RFC's we've implemented. (Though I haven't decided on that, yet.) The RFC rabbit-hole on IMAP is deep, and as a result we will need to be aware of issues we might run into that aren't detailed here. (We may discover new RFC's pertaining to what we're doing, for example.)

What is IMAP?

So let's back our complicated story up a little bit. First, let's talk about what is and isn't IMAP.

IMAP is the Internet Message Access Protocol, a protocol designed to allow a client (such as Outlook, Thunderbird, etc.) to retreive and update Internet Messages (Email's) associated with an account. This means you specify the account and credentials, the client uses those to perform IMAP interactions, and then displays those results to you.

This protocol does not specify how or where those messages must be stored (server or client side), nor how one can send messages to another Email box (which is typically SMTP / relays), it only specifies how a client system should access, modify, and remove Emails that it owns.

The short and simple: this means we can build a generic IMAP access protocol, which can then be used to interact with another service (or .NET library) for storage of the physical message.

Getting Started: Understanding Requirements

We'll begin our adventure down this IMAP server path by reading some of the RFC information. Fortunately, we won't read all of it, but we do have quite a bit of information to digest.

One of the more basic requirements we'll want to facilitate with our design is the transport-layer protocol and port, which happens to be TCP Port 143. This is found in RFC 3501 §2.1.

Another interesting note is the definition of a command / line, in RFC 3501 §2.2. By definition, a line ends in CRLF, and data is either a line, or a known length of octets (8-bit bytes) followed by a line.

Further in this same section, we are told that transferring data from Server to Client can happen on request, or be initiated by the server. We also learn that a client should generate an alpha-numeric "tag" which indicates which command the server is responding to. (There is no fixed syntax for a tag, just that it may only be alpha-numeric.)

Further reading tells us that there are a couple other items and states we'll need to keep. Particularly:

  • For each mailbox we must have a UIDVALIDITY value that is unique to that mailbox;
  • For each message, we must have a UID value that is unique to that message;

The combination of the mailbox name, UIDVALIDITY, and UID must describe a single message in the mail server. Each of these values is a 32-bit integer.


While there are many, many more requirements, I have set a goal to write code in every blog-post in this new series. As a result, I want to dive into writing, at the very least, a single-threading server for clients to connect to.

There are many examples out there of doing OpenSendReceiveClose, but that's not what we want. With IMAP, we need to do an OpenLoop (SendReceive) → Close. This obviously complicates things, especially once we get into mutliple concurrent connections.

So, to start this, we're going to build a demo client/server to get our infrastructure together. We're going to build one that allows us to send messages to the server, and pipe other messages back to the client. (Similar to a chat, but without message sharing.) This will allow us to build and demonstrate all the pieces of the IMAP infrastructure. (Many concurrent, connected clients, etc.) We're going to use F# Async-Computations to do so, which are actually really fun.

Let's start building a server!

Raw TCP/IP is a very difficult task to set out to implement without an understanding of how TCP/IP works. I'm not going to go into any of that detail, as I want to get right into writing some code, but the basics are that a TCP/IP connection is a "Socket", the socket is opened, and remains open until closed by the client or server. TCP/IP sockets are also bidirectional, meaning that at any given point the client and the server both reserve the right to unilaterally send data or close the connection.

We'll start with the TCP Server. This is an easy enough build, and can be abstracted to a pretty simple API. But before we do that, I want to make a few extensions to .NET.

Boilerplate

One of the pain-points (for me) with .NET is that there is no way to read an entire response from a raw TCP socket. For some reason (probably a good one), the designers left that part out. As a result, I always write some modifications to System.Net.Sockets.NetworkStream and System.Net.Sockets.Socket to make them more friendly:

module EBrown.Tcp.NetworkStream
open System.Net.Sockets

/// Add definitions to the `System.Net.Sockets.NetworkStream`
type NetworkStream with
    member stream.AsyncReceive (buffer : byte array, ?offset, ?count) =
        Async.FromBeginEnd(
            buffer, 
            defaultArg offset 0, 
            defaultArg count buffer.Length, 
            stream.BeginRead, 
            stream.EndRead)
    member stream.AsyncReceiveAll () =
        let rec receive buffer =
            async {
                let tempBuffer = 1024 |> Array.zeroCreate
                let! bytesReceived = tempBuffer |> stream.AsyncReceive
                let buffer = [|buffer; tempBuffer.[0..bytesReceived - 1]|] |> Array.concat
                if bytesReceived < tempBuffer.Length then return buffer else return! buffer |> receive }
        [||] |> receive
    member stream.ReadAll () =
        let rec receive buffer =
            let tempBuffer = 1024 |> Array.zeroCreate
            let bytes = (tempBuffer, 0, tempBuffer.Length) |> stream.Read
            let buffer = [|buffer; tempBuffer.[0..bytes - 1]|] |> Array.concat
            if bytes < tempBuffer.Length then buffer else buffer |> receive
        [||] |> receive
    member stream.Write (buffer : byte array) = stream.Write(buffer, 0, buffer.Length)

The first thing you see is AsyncRead, which performs an asynchronous call to BeginRead and EndRead, to do the entire thing in one go. This seems to be the most preferred way of doing .NET async operations at this level. The next line starts some interesting syntax: we define an AsyncReadAll function which will do an AsyncRead call until it has no more data. You might not have seen async { ... } before, or let! or return!, but they're F# Asynchronous Computation Expressions. I won't go into detail (because I don't really understand how they work, just how to use them) but we'll use them all over the place here.

The basics you need to know are that in an async block, you can make calls to other async code, and bind that. The let! (let bang) keyword will call to an async function, and bind the result, ONCE the result is ready. If the result isn't ready, then this function execution is paused and the thread is used elsewhere. The return! (return bang) will return the result of the async expression, after it's ready.

This is important to know because the following two lines are not the same:

let bytesReceived = tempBuffer |> stream.AsyncReceive
let! bytesReceived = tempBuffer |> stream.AsyncReceive

The first line binds an Async<int> to bytesReceived, whereas the second binds an int to bytesReceived. The first value is just a function call, that has not yet been started. The second calls the function and then yields the thread until the result is ready.

You then see that the NetworkStream has a ReadAll, which does a Read until no more data, and a Write, which does a stream.Write with a buffer.

Next we want to extend Socket to make life easier:

module EBrown.Tcp.Socket
open System.Net.Sockets

/// Add definitions to the `System.Net.Sockets.Socket`
type Socket with
    member socket.AsyncAccept () = Async.FromBeginEnd(socket.BeginAccept, socket.EndAccept)
    member socket.AsyncReceive (buffer : byte array, ?offset, ?count) =
        Async.FromBeginEnd(
            buffer, 
            defaultArg offset 0, 
            defaultArg count buffer.Length, 
            (fun (buffer, offset, size, callback, state) -> socket.BeginReceive(buffer, offset, size, SocketFlags.None, callback, state)), 
            socket.EndReceive)
    member socket.AsyncSend (buffer : byte array, ?offset, ?count) =
        Async.FromBeginEnd(
            buffer,
            defaultArg offset 0,
            defaultArg count buffer.Length,
            (fun (buffer, offset, size, callback, state) -> socket.BeginSend(buffer, offset, size, SocketFlags.None, callback, state)),
            socket.EndSend)
    member socket.AsyncReceiveAll () =
        let rec receive buffer =
            async {
                let tempBuffer = 1024 |> Array.zeroCreate
                let! bytesReceived = tempBuffer |> socket.AsyncReceive
                let buffer = [|buffer; tempBuffer.[0..bytesReceived - 1]|] |> Array.concat
                if bytesReceived < tempBuffer.Length then return buffer else return! buffer |> receive }
        [||] |> receive

This is the same as the NetworkStream for the most part, so I won't explain it.

Build a TCP Server

We're going to build two servers here, we'll build a TCP/IP server, and then an IMAP server. We'll do this because the raw TCP/IP server is pretty simple, and can be used for other servers as well.

A TCP/IP server needs two data-points to bind to: an IP Address, and a Port. These two values, when concatenated with a colon (:) make up a "socket". The server has a socket, and the client has a socket.

So one of the first things we need is the server endpoint: let endpoint = (ipAddress, port) |> IPEndPoint

So that's easy. The next thing we need is a server. In .NET we can accomplish a low-level TCP/IP server by using the Socket class (which we extended above), and binding. To start, we define our Socket:

let server = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)

This builds a TCP/IP socket (which we named the server) to the InterNetwork address family (IP), the Stream socket type (raw bytes to and from), and the Tcp protocol (the TCP part of TCP/IP). Thus, we can use a stream to send and receive data.

Next, we bind:

(ipAddress, port) |> IPEndPoint |> server.Bind

Because I'm lazy, I bound and created the endpoint in the same line. This tells the server that it will be dealing specifically on the IP Address and Port we provided.

After binding, we listen:

SocketOptionName.MaxConnections |> int |> server.Listen

This builds a server with the MaxConnections configuration (allowing the maximum number of connections in the backlog) and then starts listening on the socket we asked for.

Once we've done that, we need to do the hard part. We need to do something with inbound connections, because by default the server doesn't actually do anything.

Listening for a new connection can be done with the socket.AsyncAccept function we built earlier, and because it's async we can ignore the threading issues (for the most part) and bind directly to it:

let! socket = () |> server.AsyncAccept

Now we won't get anything into socket until someone tries to connect, so we can then start working with that connection:

try Async.Start(socket |> events.Connect, cancellationToken = cts.Token)
with e -> ()

Now, what I did here is attempt to spawn a new asynchronous computation expression to handle the connection, it's not the job of the TCP/IP server to track that. We also passed a cancellation token in case we want to stop that expression later.

We'll then wrap that in an async { } block, and bind it to a function. If we include some basic logging, we have:

let cts = new CancellationTokenSource ()
let server = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
(ipAddress, port) |> IPEndPoint |> server.Bind
SocketOptionName.MaxConnections |> int |> server.Listen
(() |> ipAddress.ToString, port) ||> printfn "Started listening on %s:%d"

let rec waitForConnection () = 
    async {
        printfn "Waiting for connection..."
        let! socket = () |> server.AsyncAccept
        () |> socket.RemoteEndPoint.ToString |> printfn "Socket connected: %s"
        try Async.Start(socket |> events.Connect, cancellationToken = cts.Token)
        with e -> e.ToString() |> printfn "An error occurred: %s"
        return! () |> waitForConnection }

Good good, progress. Next, we want to unconditionally start the waitForConnection loop:

Async.Start(() |> waitForConnection, cancellationToken = cts.Token)

And finally, we'll build an IDisposable to properly handle closing the server:

{ new IDisposable with
    member this.Dispose () =
        () |> events.Close
        () |> cts.Cancel
        () |> server.Close
        () |> cts.Dispose
        () |> server.Dispose }

One of the things not defined here is events. I built a custom record for events the server can perform, which is extremely simple:

type ServerEvents =
    { Connect : Socket -> Async<unit>
      Close : unit -> unit }

Basically, we have a Connect and Close event.

All-in-all, if we put everything together and build a couple functions in a type, we have:

type Server () =
    static member StartI events port (ipAddress : IPAddress) =
        let cts = new CancellationTokenSource ()
        let server = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
        (ipAddress, port) |> IPEndPoint |> server.Bind
        SocketOptionName.MaxConnections |> int |> server.Listen
        (() |> ipAddress.ToString, port) ||> printfn "Started listening on %s:%d"

        let rec waitForConnection () = 
            async {
                printfn "Waiting for connection..."
                let! socket = () |> server.AsyncAccept
                () |> socket.RemoteEndPoint.ToString |> printfn "Socket connected: %s"
                try Async.Start(socket |> events.Connect, cancellationToken = cts.Token)
                with e -> e.ToString() |> printfn "An error occurred: %s"
                return! () |> waitForConnection }
        Async.Start(() |> waitForConnection, cancellationToken = cts.Token)

        { new IDisposable with
            member this.Dispose () =
                () |> events.Close
                () |> cts.Cancel
                () |> server.Close
                () |> cts.Dispose
                () |> server.Dispose }
    static member Start events port = (events, port, IPAddress.Any) |||> Server.StartI 

Which is a fully-functional TCP/IP server that does nothing, as of yet.

Building the basic IMAP Server

Our basic IMAP server won't comply with any parts of the protocol, but it will function as an end-to-end test of TCP/IP and the server we created. The IMAP server will use a TCP/IP server instance from the previous class to handle the raw TCP/IP portions, then will use a mutable list of connected sockets (including a basic User ID) to manage the connections. It will also send a BYE on close of the server to all connected sockets, thus allowing us to tell clients we're not there anymore, cleanly.

To start this process, we'll actually define the Start function of the server:

member this.Start () = EBrown.Tcp.Server.Start { Connect = this.OnConnect; Close = this.OnClose } 143

This helps push us to the right direction of where to go next. We need to define OnConnect and OnClose, which will be called by the TCP/IP server.

The OnConnect function really only has one job: accept a connected client, spawn the async computation for it, and add it to the list of all sockets. So for that, we'll define a pretty brief function:

member this.OnConnect (socket : Socket) =
    async {
        try
            lock sockets (fun () -> sockets <- (socket, None)::sockets)
            sockets |> List.length |> printfn "Sockets open: %i"
            return! socket |> runClient
        finally
            lock sockets (fun () -> sockets <- sockets |> List.filter (fst >> (<>) socket))
            SocketShutdown.Both |> socket.Shutdown |> socket.Close
            sockets |> List.length |> printfn "Sockets open: %i" }

This function just takes a socket, indicates that it's open, and then runs the async computation for it. When it closes, it removes the socket from the open pool. Pretty simple and basic. What we see here, that we haven't yet written, is runClient.

The runClient function is a little more complex, as it will actually do some things in our IMAP server. For now, all we want it to do is echo back what the user sent over the socket, when the user sends data. For this, we'll use AsyncReceiveAll, and send back with AsyncSend:

let rec runClient (socket : Socket) =
    let printfn = printfn "Socket %s: %s" (socket.RemoteEndPoint.ToString())
    async {
        let! buffer = () |> socket.AsyncReceiveAll
        let str = buffer |> Encoding.ASCII.GetString
        sprintf "Received (%i bytes): %s" buffer.Length str |> printfn
        if str.Length = 3 && str = "BYE" then printfn "Disconnected"
        else
            let! bytesSent = [| "Hello, other world!"B |] |> Array.concat |> socket.AsyncSend
            bytesSent |> sprintf "Sent response (%i bytes)" |> printfn
            return! socket |> runClient }

The goal here, is to loop until we have to kill the socket (for any reason). Currently, the only valid reason is a BYE, but there may be others at some point. (Such as errors, authentication failures, etc.)

Finally, we need the OnClose function, which should kill the server and tell all clients it's gone. This is actually really simple:

member this.OnClose () =
    lock sockets (fun () ->
        sockets
        |> List.toArray
        |> Array.filter (fun (sock, _) -> sock.Connected)
        |> Array.map (fun (sock, user) -> [| "BYE"B |] |> Array.concat |> sock.AsyncSend)
        |> Async.Parallel
        |> Async.RunSynchronously
        |> ignore)

If we put it all together, and add our mutable sockets, we get:

type User = { Id : string }

type Server () =
    let mutable sockets : (Socket * User option) list = []
    let rec runClient (socket : Socket) =
        let printfn = printfn "Socket %s: %s" (socket.RemoteEndPoint.ToString())
        async {
            let! buffer = () |> socket.AsyncReceiveAll
            let str = buffer |> Encoding.ASCII.GetString
            sprintf "Received (%i bytes): %s" buffer.Length str |> printfn
            if str.Length = 3 && str = "BYE" then printfn "Disconnected"
            else
                let! bytesSent = [| "Hello, other world!"B |] |> Array.concat |> socket.AsyncSend
                bytesSent |> sprintf "Sent response (%i bytes)" |> printfn
                return! socket |> runClient }
    member this.OnConnect (socket : Socket) =
        async {
            try
                lock sockets (fun () -> sockets <- (socket, None)::sockets)
                sockets |> List.length |> printfn "Sockets open: %i"
                return! socket |> runClient
            finally
                lock sockets (fun () -> sockets <- sockets |> List.filter (fst >> (<>) socket))
                SocketShutdown.Both |> socket.Shutdown |> socket.Close
                sockets |> List.length |> printfn "Sockets open: %i" }
    member this.OnClose () =
        lock sockets (fun () ->
            sockets
            |> List.toArray
            |> Array.filter (fun (sock, _) -> sock.Connected)
            |> Array.map (fun (sock, user) -> [| "BYE"B |] |> Array.concat |> sock.AsyncSend)
            |> Async.Parallel
            |> Async.RunSynchronously
            |> ignore)
    member this.Start () = EBrown.Tcp.Server.Start { Connect = this.OnConnect; Close = this.OnClose } 143

So now we want to test it.

Testing our Server

To test our server we need to start it, and build a client to connect to it. Both are trivial:

use server = Server().Start()
Console.ReadLine() |> ignore
printfn "Closing..."

This runs the server, forever. It will stay online until the user presses "Enter".

The client is much simpler than the server, and is just a few lines of code:

printfn "Press [Enter] / [Return] to quit, any other character to send data."
let cts = new CancellationTokenSource ()
use client = new TcpClient("127.0.0.1", 143)
printfn "Connected to %s" (client.Client.RemoteEndPoint.ToString())
use stream = client.GetStream()

let sendData = 
    async {
        while (Console.ReadKey().Key <> ConsoleKey.Enter) do
            if not cts.IsCancellationRequested then
                printfn ""
                [| "Hello world!"B |] |> Array.concat |> stream.Write
        [| "BYE"B |] |> Array.concat |> stream.Write
        printfn "Disconnected" }
let receiveData = 
    async {
        let rec loop () =
            async {
                let! bytes = () |> stream.AsyncReadAll
                let str = bytes |> Encoding.ASCII.GetString
                printfn "Received %i bytes: %s" bytes.Length str
                if str.Length = 3 && str = "BYE" then printfn "Disconnected, press any key to exit."; cts.Cancel() else return! () |> loop }
        return! () |> loop }
Async.Start(receiveData, cancellationToken = cts.Token)
try Async.RunSynchronously(sendData, cancellationToken = cts.Token)
with | :? OperationCanceledException -> () | e -> printfn "%s" (e.ToString()); printfn "Press enter to exit..."; Console.ReadLine() |> ignore

We're not going to do client-side work at all, so I won't detail anything about it, but it's really quite trivial.

If we build out two console applications, throw this code in and make it work, we can test it and see the result:

Client:

Press [Enter] / [Return] to quit, any other character to send data.
Connected to 127.0.0.1:143
a
Received 19 bytes: Hello, other world!
Disconnected

Server:

Started listening on 0.0.0.0:143
Waiting for connection...
Socket connected: 127.0.0.1:57309
Waiting for connection...
Sockets open: 1
Socket 127.0.0.1:57309: Received (12 bytes): Hello world!
Socket 127.0.0.1:57309: Sent response (19 bytes)
Socket 127.0.0.1:57309: Received (3 bytes): BYE
Socket 127.0.0.1:57309: Disconnected
Sockets open: 0

And there we have it. We've built bi-directional TCP/IP in a small amount of F# code, and can now begin the adventure of building the actual IMAP server.


If you are lazy, like me, and want this project for free, the entire thing will be available on GitHub. The version for this specific blog post is tree 8d269692ab169806e87b59700d69853b1d2eb1ff.


The blog post today is brought to you buy a significant amount of Coca-Cola and Fritos. I do not recommend either of these products unless you hate yourself (like me) and are trying to gain a lot of unnecessary weight. What I do recommend, however, is that you try to enjoy yourself as much as possible, and ignore what anyone else says about that. (You do you, basically.)

It's been a short while, as I took a longer-than-expected hiatus after the F# Advent of Code, but I'm back and we're going to work on a fun little project that involves a lot of detail and such.

F# Advent Day of Code: Modeling types to prevent bugs

F# Advent Day of Code: Taking advantage of the F# Type-System

Today is my date for F# Advent, and I want to discuss as pretty large topic, which happens to be domain modeling with F# types, and how to model the types to prevent bugs.

One of the "going jokes" in the F# community is that "if it compiles, it works and bug free." This is obviously not entirely true, there's always a possibility that it won't work properly, but it is actually proven fairly often. Part of this is because of F#'s type system, the strong-typing requisite, and the fact that it does not implicitly convert them. The problem, and often the source of bugs that do manifest, is that without a little thinking, it's hard to build a type-structure that creates this "bug free" scenario. It can be done, but you have to forget about how you think about types.

In this lesson we're going to take a library I'm preparing to open-source, and discuss how to build a type-system that is mostly flawless, and doesn't allow us to model incorrect structure or state.

Step 1: same as usual, identify the problem

The first step of any program is to identify the problem. In our situation, the problem I'm proposing to solve is the enormous problem of generating HTML code from a non-HTML language, and not writing it by hand. More often than not I'm forced to look up intricacies of HTML, and subtleties that really ought to be a lot easier. We're also going to see how we can generate HTML that is 100% conformant to the HTML specification, and how we can even embed compiler warnings into our system, such that if it compiles, it's valid HTML.

Remember that F# is a functional language, and uses an algebraic type-system. The language itself is designed to support these types of ideas natively and out of the box, so we're going to go over the basics of building this system, and we'll cover what the type-system gives us that enables compile-time safety of a generated system.

Step 2: solve the problem

This is the last heading, and it's going to be long, so buckle-up and enjoy the ride.

HTML is a language that is used predominantly to generate markup for websites and web-based applications. It's used to define a structure which a browser or device can use to render and organize content. A basic, perfectly valid, HTML page might look as follows:

A Page Title

To a normal developer like myself, there's nothing to this. We define the "DOCTYPE" first, then define the HTML content. This is a basic HTML5 compliant page, that only has a title and no content. HTML does not require content, but it does, interestingly, require the <title> element.

Now from this we can begin extracting some types. We can start to define how we want to model the type-system to make sure that we always generate a perfectly compliant page. We see that HTML has a few characteristics:

  • A DOCTYPE defining the document;
  • A <head> including the meta-data of the document;
  • A <title> in the head defining the title of the document;
  • A <body> including the content of the document;

With this, we'll actually build a quick type:

type DocType_V1 = | Html5
type Head_V1 = { Title : string }
type Document_V1 = { Doctype : DocType_V1; Head : Head_V1; Body : string }

let html5Document_v1 = { Document_V1.Doctype = Html5; Head = { Head_V1.Title = "A Test Document" }; Body = "" }

While this is a perfectly valid type, this doesn't solve our problem yet. All we have accomplished here is defining the "basics" of HTML, we can actually have a completely invalid <body> at this point. We also have none of the additional features available in the <head> element, such as <meta> tags, style-sheets, JavaScript, etc. Because I'm pragmatic, we're going to work from top-to-bottom, so we'll start with defining a better document.

The biggest issue here is that there are 7 major document types in modern HTML:

  1. HTML 5 (the newest);
  2. HTML 4.01 Frameset (very loose definition);
  3. HTML 4.01 Transitional (less loose, but still not very strict);
  4. HTML 4.01 Strict (very strict, compliant definition);
  5. XHTML 1.0 Frameset (a loose XHTML definition);
  6. XHTML 1.0 Transitional (less loose, but still not very strict XHTML definition);
  7. XHTML 1.0 Strict (very strict, compliant XHTML definition);

Knowing this, there's actually very different aspects to each document type. Therefore, it doesn't make sense to use an enum for it, it instead makes sense to use a outer union for it:

type Head_V2 = { Title : string }
type StrictDocument_V2 = { Head : Head_V2; Body : string }
type TransitionalDocument_V2 = { Head : Head_V2; Body : string }
type FramesetDocument_V2 = { Head : Head_V2; Body : string }
type DocumentForm_V2 = | Strict of StrictDocument_V2 | Transitional of TransitionalDocument_V2 | Frameset of FramesetDocument_V2
type Document_V2 = | Html5Document of StrictDocument_V2 | Html4Document of DocumentForm_V2 | XhtmlDocument of DocumentForm_V2

let html5Document_v2 = Document_V2.Html5Document { StrictDocument_V2.Head = { Head_V2.Title = "A Test Document" }; Body = "" }

What just became quite interesting is that it's actually impossible to model an unsupported document type now. Previously, we would actually hit an issue where it would become possible to model a document that was not a StrictDocument, which would create an invalid HTML document.

The next step is to model some of the <meta> tags that are often placed inside a <head> element. These are things like the document character-set, linked-resources, language, description, keywords, etc. The <meta> tags can also be free-form, meaning they can be a tag to support things like Facebook descriptions and identifiers which help with creating pages that are more friendly to the social-network, etc. To support this we'll actually build mutliple meta-tag models.

To model this, we'll start with the "standard" meta-data's to be supported. This is easily modeled following the requirements in the HTML5 specification which is freely (and easily) accessible:

type StandardMeta_V3 = | ApplicationName of Lang : string option | Author | Description | Generator | Keywords | Referrer

The interesting tag here is ApplicationName, which allows you to specify between zero and one, inclusive, tags with the same "language". These would be included in the document as follows:

The kicker is that lang is optional, and there may be only one of each lang value in a document.

Another important aspect of a <head> element is the charset, or character-set. Every HTML page is allowed to declare a <meta> tag with a name="charset" attribute exactly once, and this charset should be the encoding used to send the page from server to browser. We'll actually define an enum type for this:

type Charset_V3 = | Utf8

And we'll include this and the standard <meta> tags in the new Head element:

type Head_V3 = { Title : string; Charset : Charset_V3 option; StandardMetas : Map<StandardMeta_V3, string> }

Still quite simple, and we'll use our previous model with a few minor modifications:

type StrictDocument_V3 = { Head : Head_V3; Body : string option }
type TransitionalDocument_V3 = { Head : Head_V3; Body : string option }
type FramesetDocument_V3 = { Head : Head_V3; Body : string option }
type DocumentForm_V3 = | Strict of StrictDocument_V3 | Transitional of TransitionalDocument_V3 | Frameset of FramesetDocument_V3
type Document_V3 = | Html5Document of StrictDocument_V3 | Html4Document of DocumentForm_V3 | XhtmlDocument of DocumentForm_V3

let html5Document_v3 =
    Document_V3.Html5Document
      { StrictDocument_V3.Head =
          { Head_V3.Title = "A Test Document"
            Charset = Some Charset_V3.Utf8
            StandardMetas = [(StandardMeta_V3.ApplicationName (Some "en"), "Test Application"); (StandardMeta_V3.Description, "A test application.")] |> Map.ofList }
        Body = None }

Side note: we used Map, but as there's currently no syntax to construct a map natively, it's possible to create a situation where the compiler will not stop us from creating an invalid list, this is something the F# team has on the list, and hopefully it's resolved sooner rather than later.

So now that we have a document created and modeled, it's time to generate. Fortunately, generating HTML is actually really easy if you follow the rules / guidelines. For XHTML that means always lower-case the attribute and tag names, for regular HTML it's not as strict, but still has rules.

The first rule of HTML and XHTML alike is the DOCTYPE, which is (fortunately) a static list. For us, we care about the following:

let getDoctype_v3 document =
    match document with
    | Document_V3.XhtmlDocument (Strict _) -> "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"
    | XhtmlDocument (Transitional _) -> "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
    | XhtmlDocument (Frameset _) -> "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Frameset//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\">"
    | Html4Document (Strict _) -> "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">"
    | Html4Document (Transitional _) -> "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">"
    | Html4Document (Frameset _) -> "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Frameset//EN\" \"http://www.w3.org/TR/html4/frameset.dtd\">"
    | Html5Document _ -> "<!DOCTYPE HTML>"

With this single function, we can get every DOCTYPE we need for all our supported models.

let html5Document_v3_doctype = html5Document_v3 |> getDoctype_v3

This ought to return <!DOCTYPE HTML>. You'll notice that for all three of the XHTML DOCTYPE results, html is lower-case. That's part of the spec I mentioned.

As usual, I want to declare a couple functions we'll use for the whole project:

let joinStrings sep (strs : string array) = System.String.Join(sep, strs)
let prependIfText sep str = if str |> Seq.isEmpty |> not then sprintf "%s%s" sep str else ""

They just make life easier.

Next, we need to think about our document. Our document is made up of "elements" which are represented as "tags", and tags have a couple rules about them:

  • Some tags are allowed to self-close, or <tag /> vs <tag></tag> (the <script> tag for example cannot self-close);
  • Some tags are "inline" tags, that is, we don't want to break lines inside the tag (like <title>, we want to do <title>...</title>, without a line-break);
  • Some tags have attributes, and some do not;
  • Some tags will have content, and some will not;
  • All tags have a name;

With this, we can begin to define a function that will fulfill these requirements. We'll always know ahead-of-time whether the tag will allow self-closing, and whether or not we want to include line-breaks, so we want to start defining an API:

let renderTag_v3_1 (selfClose : bool) (includeBreaks : bool) (tagName : string) = ()

Obviously we need to add a body to the function, but this starts us off. We can then define functions which will alias to this function. The next item should probably be attributes, because sometimes we'll have those and sometimes we won't.

We probably want to include content as well, so our final API might look like:

let renderTag_v3_2 (selfClose : bool) (includeBreaks : bool) (tagName : string) (attrs : Map<string, string>) (content : string) = ()

Finally, we want to define a body. The first part of any tag is starting it, or the <tagName bit. (Notice I excluded the closing bracket: this is going to be dealt with momentarily.) The <tagName bit should also have attributes if there are any, so we'll want <tagName attr="value". To do this, we can actually map our attributes to an array, then map them to key=\"value\", then join them on a space, then add space if there are any, then finally we can print the starting tag:

let renderTag_v3_3 (selfClose : bool) (includeBreaks : bool) (tagName : string) (attrs : Map<string, string>) (content : string) =
    let tagStart =
        attrs
        |> Map.toArray
        |> Array.map (fun ((k : string), (v : string)) -> sprintf "%s=\"%s\"" (k.ToLower()) (v.Replace("\"", "\\\"")))
        |> joinStrings " "
        |> prependIfText " "
        |> sprintf "<%s%s" tagName
    tagStart

Once again, we're making decent progress. We've followed the XHTML convention, of lower-casing the key, which is also entirely valid for HTML. (Conveniently.)

Next, if the content is empty, we want to either self-close or explictly close the tag. This is accomplished by a simple if.

let renderTag_v3_4 (selfClose : bool) (includeBreaks : bool) (tagName : string) (attrs : Map<string, string>) (content : string) =
    let tagStart =
        attrs
        |> Map.toArray
        |> Array.map (fun ((k : string), (v : string)) -> sprintf "%s=\"%s\"" (k.ToLower()) (v.Replace("\"", "\\\"")))
        |> joinStrings " "
        |> prependIfText " "
        |> sprintf "<%s%s" tagName
    if content |> System.String.IsNullOrEmpty then
        if selfClose then sprintf "%s />" tagStart
        else sprintf "%s></%s>" tagStart tagName
    else 
        tagStart

In our else branch, when there is content, we want to test it for any line-breaks, and if we should include them do so:

let renderTag_v3_5 selfClose includeBreaks tagName attrs content =
    let tagStart =
        attrs
        |> Map.toArray
        |> Array.map (fun ((k : string), (v : string)) -> sprintf "%s=\"%s\"" (k.ToLower()) (v.Replace("\"", "\\\"")))
        |> joinStrings " "
        |> prependIfText " "
        |> sprintf "<%s%s" tagName
    if content |> System.String.IsNullOrEmpty then
        if selfClose then sprintf "%s />" tagStart
        else sprintf "%s></%s>" tagStart tagName
    else
        let tagStart = sprintf "%s>" tagStart
        let breakV = if includeBreaks then System.Environment.NewLine else ""
        let tagEnd = sprintf "</%s>" tagName
        sprintf "%s%s%s%s%s%s" tagStart breakV content breakV tagEnd breakV

So now that gets our main tag-rendering started. The next bit is to actually build prototypes for each main tag:

let renderTitle_v3 = renderTag_v3_5 false false "title" Map.empty<string, string>
let renderHead_v3 = renderTag_v3_5 false true "head" Map.empty<string, string>
let renderBody_v3 = renderTag_v3_5 false true "body"

As you can see, each tag has specific expectations. Our <title> tag has no line-breaks, and does not allow attributes, whereas our <head> tag has line-breaks, and does not allow attributes. Our <body> tag has both.

We have another utility method to define:

let stringOrBlank = function | Some s -> s | None -> ""

Which will make sense in a moment.

The next step is to render the actual document. This is relatively easy, render the <head>, render the <body>, and render the <!DOCTYPE>. Because we have 3 major HTML document-types, and each has different rules, we'll define a function for each:

let getStrictBody_v3 (document : StrictDocument_V3) =
    let sb = System.Text.StringBuilder()
    document.Head.Title |> renderTitle_v3 |> renderHead_v3 |> sb.Append |> ignore
    document.Body |> stringOrBlank |> renderBody_v3 ([] |> Map.ofList) |> sb.Append |> ignore
    sb.ToString()
let getTransitionalBody_v3 (document : TransitionalDocument_V3) =
    let sb = System.Text.StringBuilder()
    document.Head.Title |> renderTitle_v3 |> renderHead_v3 |> sb.Append |> ignore
    document.Body |> stringOrBlank |> renderBody_v3 ([] |> Map.ofList) |> sb.Append |> ignore
    sb.ToString()
let getFramesetBody_v3 (document : FramesetDocument_V3) =
    let sb = System.Text.StringBuilder()
    document.Head.Title |> renderTitle_v3 |> renderHead_v3 |> sb.Append |> ignore
    document.Body |> stringOrBlank |> renderBody_v3 ([] |> Map.ofList) |> sb.Append |> ignore
    sb.ToString()

Now we see how things start to fit together, and we still have to return one more larger document for the full <html> bit:

let getBody_v3 document =
    match document with
    | XhtmlDocument (Strict d) | Html4Document (Strict d) | Html5Document d -> d |> getStrictBody_v3
    | XhtmlDocument (Transitional d) | Html4Document (Transitional d) -> d |> getTransitionalBody_v3
    | XhtmlDocument (Frameset d) | Html4Document (Frameset d) -> d |> getFramesetBody_v3

Of course, it's obvious this isn't handling the <html> bit, this just calls the next renderer.

let printDocument_v3 document =
    let doctype = document |> getDoctype_v3
    let sb = System.Text.StringBuilder()
    doctype |> sb.Append |> ignore
    System.Environment.NewLine |> sb.Append |> ignore
    let attrs =
        match document with
        | Html4Document _ | Html5Document _ -> [] |> Map.ofList
        | XhtmlDocument _ -> [("xmlns", "http://www.w3.org/1999/xhtml")] |> Map.ofList
    document |> getBody_v3 |> renderTag_v3_5 false true "html" attrs |> sb.Append |> ignore
    sb.ToString()

While this is a great start, we haven't fully supported our model. We don't render the <meta> tags I promised. To do that, we have to add a little more code (though not much). For these we'll abstract up our <head> rendering another level, because we need to render sequences in the <head> tag.

let renderMetaTag_v4 = renderTag_v3_5 true false "meta"
let getCharset_v4 c =
    match c with
    | Charset_V3.Utf8 -> "utf-8"

let getStandardMetaAttrs_v4 m =
    let result s = [("name", s)]
    match m with
    | StandardMeta_V3.ApplicationName lang -> lang |> (function | Some l -> [("lang", l)] | _ -> []) |> List.append [("name", "application-name")]
    | Author -> "author" |> result
    | Description -> "description" |> result
    | Generator -> "generator" |> result
    | Keywords -> "keywords" |> result
    | Referrer -> "referrer" |> result

let getHead_v4 head =
    let sb = System.Text.StringBuilder()
    head.Title |> renderTitle_v3 |> sb.Append |> ignore
    match head.Charset with
    | Some c ->
        let charset = c |> getCharset_v4
        renderMetaTag_v4 ([("content", (sprintf "text/html; charset=%s" charset)); ("http-equiv", "Content-Type")] |> Map.ofList) "" |> sb.Append |> ignore
    | None -> ()
    head.StandardMetas |> Map.toArray |> Array.map (fun (k, v) -> renderMetaTag_v4 ([("content", v)] |> List.append (k |> getStandardMetaAttrs_v4) |> Map.ofList) "") |> Array.iter (sb.Append >> ignore)
    sb.ToString() |> renderHead_v3

let getStrictBody_v4 (document : StrictDocument_V3) =
    let sb = System.Text.StringBuilder()
    document.Head |> getHead_v4 |> sb.Append |> ignore
    document.Body |> stringOrBlank |> renderBody_v3 ([] |> Map.ofList) |> sb.Append |> ignore
    sb.ToString()
let getTransitionalBody_v4 (document : TransitionalDocument_V3) =
    let sb = System.Text.StringBuilder()
    document.Head |> getHead_v4 |> sb.Append |> ignore
    document.Body |> stringOrBlank |> renderBody_v3 ([] |> Map.ofList) |> sb.Append |> ignore
    sb.ToString()
let getFramesetBody_v4 (document : FramesetDocument_V3) =
    let sb = System.Text.StringBuilder()
    document.Head |> getHead_v4 |> sb.Append |> ignore
    document.Body |> stringOrBlank |> renderBody_v3 ([] |> Map.ofList) |> sb.Append |> ignore
    sb.ToString()

With all that done, we should get the apporpriate result with our "standard" meta-tags returned in the result output. This demonstrates how truly easy it is to modify this system to add features.

Now we haven't even come close to finishing with the <head> section, nor even close to fulfilling even the most common elements of it. For that, we'll actually add a couple more properties to our record type.

A <head> section in an (X)HTML page often consists of stylesheets and JavaScript as well as the items we've already included. Fortunately, including support for those two sections is bewilderingly easy. We'll just create two new properties on our Head type, and we'll also add a new Union type to indicate if they're a file, or the actual script.

There are two ways to include stylesheets and JavaScript: you can include a reference to a file that holds the information, or you can include the raw information. Supporting both here is extremely easy:

type Resource_V5 = | File of Name : string | Data of RawData : string

So now, we could actually create a JavaScript section to our Head:

type Head_V5 = { Title : string; Charset : Charset_V3 option; StandardMetas : Map<StandardMeta_V3, string>; Scripts : Resource_V5 list; Stylesheets : Resource_V5 list }

That's easy enough. I also included the stylesheets, because both will use the same union type, with different values. Now we can create a new document with these specified:

type StrictDocument_V5 = { Head : Head_V5; Body : string option }
type TransitionalDocument_V5 = { Head : Head_V5; Body : string option }
type FramesetDocument_V5 = { Head : Head_V5; Body : string option }
type DocumentForm_V5 = | Strict of StrictDocument_V5 | Transitional of TransitionalDocument_V5 | Frameset of FramesetDocument_V5
type Document_V5 = | Html5Document of StrictDocument_V5 | Html4Document of DocumentForm_V5 | XhtmlDocument of DocumentForm_V5
let html5Document_v5 =
    Document_V5.Html5Document
      { StrictDocument_V5.Head =
          { Head_V5.Title = "A Test Document"
            Charset = Some Charset_V3.Utf8
            StandardMetas = [(StandardMeta_V3.ApplicationName (Some "en"), "Test Application"); (StandardMeta_V3.Description, "A test application.")] |> Map.ofList
            Scripts = [Resource_V5.File "somepath/file.js"; Resource_V5.Data "alert(\"Test script!\");"]
            Stylesheets = [] }
        Body = None }

Now we just have to render these, which is only a slight change to our getHead function:

let renderScript_v5 s =
    match s with
    | Resource_V5.File f -> renderTag_v3_5 false false "script" ([("type", "text/javascript"); ("src", f)] |> Map.ofList) ""
    | Data d -> renderTag_v3_5 false true "script" ([("type", "text/javascript")] |> Map.ofList) d
let renderStyle_v5 s =
    match s with
    | Resource_V5.File f -> renderTag_v3_5 true false "link" ([("rel", "stylesheet"); ("type", "text/css"); ("href", f)] |> Map.ofList) ""
    | Data d -> renderTag_v3_5 true true "style" ([("type", "text/css")] |> Map.ofList) d
let getHead_v5 head =
    let sb = System.Text.StringBuilder()
    let render (s : string) =
        s |> sb.Append |> ignore
        System.Environment.NewLine |> sb.Append |> ignore
    head.Title |> renderTitle_v3 |> render
    match head.Charset with
    | Some c ->
        let charset = c |> getCharset_v4
        "" |> render
        renderMetaTag_v4 ([("content", (sprintf "text/html; charset=%s" charset)); ("http-equiv", "Content-Type")] |> Map.ofList) "" |> sb.Append |> ignore
    | None -> ()
    match head.StandardMetas with
    | m when m.IsEmpty -> ()
    | m ->
        "" |> render
        m |> Map.toArray |> Array.map (fun (k, v) -> renderMetaTag_v4 ([("content", v)] |> List.append (k |> getStandardMetaAttrs_v4) |> Map.ofList) "") |> Array.iter render
    match head.Stylesheets with
    | [] -> ()
    | s ->
        "" |> render
        s |> List.map renderStyle_v5 |> List.iter render
    match head.Scripts with
    | [] -> ()
    | s ->
        "" |> render
        s |> List.map renderScript_v5 |> List.iter render
    sb.ToString() |> renderHead_v3

let getStrictBody_v5 (document : StrictDocument_V5) =
    let sb = System.Text.StringBuilder()
    document.Head |> getHead_v5 |> sb.Append |> ignore
    document.Body |> stringOrBlank |> renderBody_v3 ([] |> Map.ofList) |> sb.Append |> ignore
    sb.ToString()
let getTransitionalBody_v5 (document : TransitionalDocument_V5) =
    let sb = System.Text.StringBuilder()
    document.Head |> getHead_v5 |> sb.Append |> ignore
    document.Body |> stringOrBlank |> renderBody_v3 ([] |> Map.ofList) |> sb.Append |> ignore
    sb.ToString()
let getFramesetBody_v5 (document : FramesetDocument_V5) =
    let sb = System.Text.StringBuilder()
    document.Head |> getHead_v5 |> sb.Append |> ignore
    document.Body |> stringOrBlank |> renderBody_v3 ([] |> Map.ofList) |> sb.Append |> ignore
    sb.ToString()

let getBody_v5 document =
    match document with
    | XhtmlDocument (Strict d) | Html4Document (Strict d) | Html5Document d -> d |> getStrictBody_v5
    | XhtmlDocument (Transitional d) | Html4Document (Transitional d) -> d |> getTransitionalBody_v5
    | XhtmlDocument (Frameset d) | Html4Document (Frameset d) -> d |> getFramesetBody_v5

let getDoctype_v5 document =
    match document with
    | Document_V5.XhtmlDocument (Strict _) -> "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"
    | XhtmlDocument (Transitional _) -> "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
    | XhtmlDocument (Frameset _) -> "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Frameset//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\">"
    | Html4Document (Strict _) -> "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">"
    | Html4Document (Transitional _) -> "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">"
    | Html4Document (Frameset _) -> "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Frameset//EN\" \"http://www.w3.org/TR/html4/frameset.dtd\">"
    | Html5Document _ -> "<!DOCTYPE HTML>"

let printDocument_v5 (document : Document_V5) =
    let doctype = document |> getDoctype_v5
    let sb = System.Text.StringBuilder()
    doctype |> sb.Append |> ignore
    System.Environment.NewLine |> sb.Append |> ignore
    let attrs =
        match document with
        | Html4Document _ | Html5Document _ -> [] |> Map.ofList
        | XhtmlDocument _ -> [("xmlns", "http://www.w3.org/1999/xhtml")] |> Map.ofList
    document |> getBody_v5 |> renderTag_v3_5 false true "html" attrs |> sb.Append |> ignore
    sb.ToString()

let writeFile f c = System.IO.File.WriteAllText(System.IO.Path.Combine(__SOURCE_DIRECTORY__, f), c)
html5Document_v5 |> printDocument_v5 |> writeFile "TestFile.html"

With all of this, our work starts to come together nicely. We want to test it with an XHTML document as well, which is just as easy:

let xhtml1Document_v5 =
    Document_V5.XhtmlDocument
      (DocumentForm_V5.Strict
          { StrictDocument_V5.Head =
              { Head_V5.Title = "A Test Document"
                Charset = Some Charset_V3.Utf8
                StandardMetas = [(StandardMeta_V3.ApplicationName (Some "en"), "Test Application"); (StandardMeta_V3.Description, "A test application.")] |> Map.ofList
                Scripts = [Resource_V5.File "somepath/file.js"; Resource_V5.Data "alert(\"Test script!\");"]
                Stylesheets = [] }
            Body = None } )
xhtml1Document_v5 |> printDocument_v5 |> writeFile "TestFile.html"

Next, we want to add support for non-standard <meta> tags, which is extremely easy:

type Head_V6 = { Title : string; Charset : Charset_V3 option; StandardMetas : Map<StandardMeta_V3, string>; AdditionalMetas : Map<string, string>; Scripts : Resource_V5 list; Stylesheets : Resource_V5 list }

let getHead_v6 head =
    let sb = System.Text.StringBuilder()
    let render (s : string) =
        s |> sb.Append |> ignore
        System.Environment.NewLine |> sb.Append |> ignore
    head.Title |> renderTitle_v3 |> render
    match head.Charset with
    | Some c ->
        let charset = c |> getCharset_v4
        "" |> render
        renderMetaTag_v4 ([("content", (sprintf "text/html; charset=%s" charset)); ("http-equiv", "Content-Type")] |> Map.ofList) "" |> sb.Append |> ignore
    | None -> ()
    match head.StandardMetas with
    | m when m.IsEmpty -> ()
    | m ->
        "" |> render
        m |> Map.toArray |> Array.map (fun (k, v) -> renderMetaTag_v4 ([("content", v)] |> List.append (head.AdditionalMetas |> Map.toList) |> List.append (k |> getStandardMetaAttrs_v4) |> Map.ofList) "") |> Array.iter render
    match head.Stylesheets with
    | [] -> ()
    | s ->
        "" |> render
        s |> List.map renderStyle_v5 |> List.iter render
    match head.Scripts with
    | [] -> ()
    | s ->
        "" |> render
        s |> List.map renderScript_v5 |> List.iter render
    sb.ToString() |> renderHead_v3

You'll notice the only thing I changed is to add a List.append (head.AdditionalMetas |> Map.toList) in our standard meta printing, because the standard metas are, at that point, string * string. This means we can guarantee it works because we used the exact same code to do both. By taking the combined list back to a Map, we guarantee that each key is specified exactly once even among the combined lists.

We have one more part of the <head> tag I want to go over, and that's the presence of a <base> tag. This is where we'll see a divide between the XHTML and HTML specifications, as they have different allowable options.

For XHTML 1.0, the <base> tag is allowed to have an href attribute, that specifies what the base URL is for relative navigation. (I.e. if you provide a link to somepage.html, the <base href="http://www.example.com/"> tag will make that a link to http://www.example.com/somepage.html.)

In HTML, the following unions would satisfy our <base> tag requirements:

type HrefTarget_V7 = | SameContextWindow | NewCleanContext | ParentContext | TopMostContext
type HtmlBaseElement_V7 = | Href of string | Target of HrefTarget_V7 | HrefTarget of string * HrefTarget_V7

In XHTML, it's only allowed to be a Href or not specified. For that, we'll define some new <head> models:

type HtmlHead_V7 = {
    Title : string
    Base : HtmlBaseElement_V7 option
    Charset : Charset_V3 option
    StandardMetas : Map<StandardMeta_V3, string>
    AdditionalMetas : Map<string, string>
    Stylesheets : Resource_V5 list
    Scripts : Resource_V5 list }
type XhtmlHead_V7 = {
    Title : string
    Base : string option
    Charset : Charset_V3 option
    StandardMetas : Map<StandardMeta_V3, string>
    AdditionalMetas : Map<string, string>
    Stylesheets : Resource_V5 list
    Scripts : Resource_V5 list }

Obviously this means we will now have two different head models, which means we will need two different renderers, and two different sets of document types, etc. While we're at it, we may-as-well start defining the Body type, which will hold our main content.

type Body_V7 = { Attributes : Map<string, string>; Children : string option }
type HtmlFramesetDocument_V7 = { Head : HtmlHead_V7; Body : Body_V7 }
type HtmlTransitionalDocument_V7 = { Head : HtmlHead_V7; Body : Body_V7 }
type HtmlStrictDocument_V7 = { Head : HtmlHead_V7; Body : Body_V7 }
type XhtmlFramesetDocument_V7 = { Head : XhtmlHead_V7; Body : Body_V7 }
type XhtmlTransitionalDocument_V7 = { Head : XhtmlHead_V7; Body : Body_V7 }
type XhtmlStrictDocument_V7 = { Head : XhtmlHead_V7; Body : Body_V7 }
type HtmlDocumentForm_V7 = | Frameset of HtmlFramesetDocument_V7 | Transitional of HtmlTransitionalDocument_V7 | Strict of HtmlStrictDocument_V7
type XhtmlDocumentForm_V7 = | Frameset of XhtmlFramesetDocument_V7 | Transitional of XhtmlTransitionalDocument_V7 | Strict of XhtmlStrictDocument_V7
type WebDocument_V7 = | Html5Document of HtmlStrictDocument_V7 | Html4Document of HtmlDocumentForm_V7 | XhtmlDocument of XhtmlDocumentForm_V7
let getDoctype_v7 document =
    match document with
    | XhtmlDocument (XhtmlDocumentForm_V7.Strict _) -> "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"
    | XhtmlDocument (XhtmlDocumentForm_V7.Transitional _) -> "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
    | XhtmlDocument (XhtmlDocumentForm_V7.Frameset _) -> "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Frameset//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\">"
    | Html4Document (HtmlDocumentForm_V7.Strict _) -> "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">"
    | Html4Document (HtmlDocumentForm_V7.Transitional _) -> "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">"
    | Html4Document (HtmlDocumentForm_V7.Frameset _) -> "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Frameset//EN\" \"http://www.w3.org/TR/html4/frameset.dtd\">"
    | Html5Document _ -> "<!DOCTYPE HTML>"

As is obvious here, we're forcing the different between the document types to be more obvious. This is mostly due to our <head> tag issue, but we would have needed to do this anyway (and will do so for the <body> eventually). Now, fortunately, most of our boiler-plate will stay the same, except for the first couple steps of rendering:

let getTarget_v7 = function | SameContextWindow -> "_self" | NewCleanContext -> "_blank" | ParentContext -> "_parent" | TopMostContext -> "_top"

let getHtmlHead (head : HtmlHead_V7) =
    let sb = System.Text.StringBuilder()
    let render (s : string) =
        s |> sb.Append |> ignore
        System.Environment.NewLine |> sb.Append |> ignore
    head.Title |> renderTitle_v3 |> render
    match head.Charset with
    | Some c ->
        "" |> render
        renderMetaTag_v4 ([("content", c |> getCharset_v4 |> sprintf "text/html; charset=%s"); ("http-equiv", "Content-Type")] |> Map.ofList) "" |> render
    | None -> ()
    match head.Base with
    | Some b ->
        let attrs = 
            let href =
                match b with
                | Href h | HrefTarget (h, _) -> [("href", h)]
                | _ -> []
            let target = 
                match b with
                | Target t | HrefTarget (_, t) -> [("target", t |> getTarget_v7)]
                | _ -> []
            target |> List.append href |> Map.ofList
        "" |> render
        renderTag_v3_5 true false "base" attrs "" |> render
    | None -> ()
    match head.StandardMetas with
    | m when m.IsEmpty -> ()
    | m ->
        "" |> render
        m |> Map.toArray |> Array.map (fun (k, v) -> renderMetaTag_v4 ([("content", v)] |> List.append (head.AdditionalMetas |> Map.toList) |> List.append (k |> getStandardMetaAttrs_v4) |> Map.ofList) "") |> Array.iter render
    match head.Stylesheets with
    | [] -> ()
    | s ->
        "" |> render
        s |> List.map renderStyle_v5 |> List.iter render
    match head.Scripts with
    | [] -> ()
    | s ->
        "" |> render
        s |> List.map renderScript_v5 |> List.iter render
    sb.ToString() |> renderHead_v3

Now, interestingly, we can make XHTML head rendering really easy by converting it to an HTML head, at least for now.

let getXhtmlHead (head : XhtmlHead_V7) =
    { HtmlHead_V7.Title = head.Title
      Charset = head.Charset
      AdditionalMetas = head.AdditionalMetas
      Base = head.Base |> Option.map Href
      StandardMetas = head.StandardMetas
      Stylesheets = head.Stylesheets
      Scripts = head.Scripts }
    |> getHtmlHead

Then we need to define some new get___Body and such functions, which are going to be just as trivial as before.

let getHtmlStrictBody_v7 (document : HtmlStrictDocument_V7) =
    let sb = System.Text.StringBuilder()
    document.Head |> getHtmlHead |> sb.Append |> ignore
    document.Body.Children |> stringOrBlank |> sprintf "<div>%s</div>" |> renderBody_v3 document.Body.Attributes |> sb.Append |> ignore
    sb.ToString()
let getHtmlTransitionalBody_v7 (document : HtmlTransitionalDocument_V7) =
    let sb = System.Text.StringBuilder()
    document.Head |> getHtmlHead |> sb.Append |> ignore
    document.Body.Children |> stringOrBlank |> sprintf "<div>%s</div>" |> renderBody_v3 document.Body.Attributes |> sb.Append |> ignore
    sb.ToString()
let getHtmlFramesetBody_v7 (document : HtmlFramesetDocument_V7) =
    let sb = System.Text.StringBuilder()
    document.Head |> getHtmlHead |> sb.Append |> ignore
    document.Body.Children |> stringOrBlank |> sprintf "<div>%s</div>" |> renderBody_v3 document.Body.Attributes |> sb.Append |> ignore
    sb.ToString()
let getXhtmlStrictBody_v7 (document : XhtmlStrictDocument_V7) =
    let sb = System.Text.StringBuilder()
    document.Head |> getXhtmlHead |> sb.Append |> ignore
    document.Body.Children |> stringOrBlank |> sprintf "<div>%s</div>" |> renderBody_v3 document.Body.Attributes |> sb.Append |> ignore
    sb.ToString()
let getXhtmlTransitionalBody_v7 (document : XhtmlTransitionalDocument_V7) =
    let sb = System.Text.StringBuilder()
    document.Head |> getXhtmlHead |> sb.Append |> ignore
    document.Body.Children |> stringOrBlank |> sprintf "<div>%s</div>" |> renderBody_v3 document.Body.Attributes |> sb.Append |> ignore
    sb.ToString()
let getXhtmlFramesetBody_v7 (document : XhtmlFramesetDocument_V7) =
    let sb = System.Text.StringBuilder()
    document.Head |> getXhtmlHead |> sb.Append |> ignore
    document.Body.Children |> stringOrBlank |> sprintf "<div>%s</div>" |> renderBody_v3 document.Body.Attributes |> sb.Append |> ignore
    sb.ToString()

We also added a sprintf "<div>%s</div>" to maintain (X)HTML compliance in the <body> section while we develop. Then getBody becomes pretty simple:

let getBody_v7 document =
    match document with
    | Html4Document (HtmlDocumentForm_V7.Strict d) | Html5Document d -> d |> getHtmlStrictBody_v7
    | XhtmlDocument (XhtmlDocumentForm_V7.Strict d) -> d |> getXhtmlStrictBody_v7
    | Html4Document (HtmlDocumentForm_V7.Transitional d) -> d |> getHtmlTransitionalBody_v7
    | XhtmlDocument (XhtmlDocumentForm_V7.Transitional d) -> d |> getXhtmlTransitionalBody_v7
    | Html4Document (HtmlDocumentForm_V7.Frameset d) -> d |> getHtmlFramesetBody_v7
    | XhtmlDocument (XhtmlDocumentForm_V7.Frameset d) -> d |> getXhtmlFramesetBody_v7

But our printDocument doesn't change:

let printDocument_v7 document =
    let doctype = document |> getDoctype_v7
    let sb = System.Text.StringBuilder()
    doctype |> sb.Append |> ignore
    System.Environment.NewLine |> sb.Append |> ignore
    let attrs =
        match document with
        | Html4Document _ | Html5Document _ -> [] |> Map.ofList
        | XhtmlDocument _ -> [("xmlns", "http://www.w3.org/1999/xhtml")] |> Map.ofList
    document |> getBody_v7 |> renderTag_v3_5 false true "html" attrs |> sb.Append |> ignore
    sb.ToString()

let xhtmlDocument_v7 =
    XhtmlDocument (
        Strict {
            XhtmlStrictDocument_V7.Head =
              { Title = "Test"
                Base = Some "http://example.com/"
                Charset = Some Utf8
                StandardMetas = [(ApplicationName None, "Test Application"); (Description, "A test application."); (Keywords, "test, application")] |> Map.ofList
                AdditionalMetas = [] |> Map.ofList
                Scripts = [File "test.js"]
                Stylesheets = [File "style.css"] }
            Body = { Attributes = [] |> Map.ofList; Children = Some "Test" }
        })

let html5Document_v7 =
    Html5Document {
        HtmlStrictDocument_V7.Head =
          { Title = "Test"
            Base = NewCleanContext |> Target |> Some
            Charset = Some Utf8
            StandardMetas = [(ApplicationName None, "Test Application"); (Description, "A test application."); (Keywords, "test, application")] |> Map.ofList
            AdditionalMetas = [] |> Map.ofList
            Scripts = [File "test.js"]
            Stylesheets = [File "style.css"] }
        Body = { Attributes = [] |> Map.ofList; Children = None }
    }

[xhtmlDocument_v7; html5Document_v7] |> List.map printDocument_v7 |> List.iter (writeFile "TestFile.html")

Ok, ok, so I've gone through a whole bunch of stuff here, but haven't really described what it all means. Here's the thing: the purpose of this post is to show you exactly what is possible with the F# type system. It's designed to show you that we can actually define and use types which create a safe environment to generate a completely different language. We described the basic types and processes which are used to generate HTML (at least in my solution) from F#. I'll be making this whole library (which is far better written than my poor examples here) open source in the coming weeks, but the basic idea is something I've been meaning to describe for some time. With the definitions we have, the next step would be to define parts that can be assembled, so that we could take an html5Document and add the appropriate master style-sheets or templating to it. We could take a document and convert it to a different type, with minimal issue. The goal of this library was to generate HTML, and offer compile-time safety of that generated HTML, and as you can see we are already accomplishing that for our <head>.

You also might be asking yourself what the advantage to something like this really is. "Elliott, it's not all that hard to write HTML, really." And you're absolutely correct, HTML is a pretty easy markup language to write, and browsers are quite forgiving, which means that even when you mess up it still works out mostly-OK, but why don't we try to do better? I use this system to generate my HTML because it gives me three major benefits:

  1. Generate partial templates to be combined in later stages with ease;
  2. Ensure compliance with the HTML specification and that we don't forget to close the appropriate tags;
  3. Allow layout swaps and transitions with a much higher ease;

And I'm not sure about you, but these are the same reasons I use F# when I can, it gives me these same benefits.

Now I could continue to go on about how we would design this templating system, but I won't. It's overly excessive explanation for not-a-lot of benefit. Instead, I'll conclude our discussion here and when this is released, then I'll go through more of how-to-use it and why you might want to.

Until then, farewell, and I hope everyone enjoys the holiday season! ☃☃☃

Getting started with programming and getting absolutely nowhere (Part 21)

Building a Twitter Bot: Part 2 (Finish it all up)

Lesson 20: Building a Twitter Bot

Today I want to finish up our Twitter Bot. There's really not a lot left to do, we just have to tie a bunch of loose-ends up, parameterize some stuff and make it reusable. (Remember our previous discussion: write good, readable, usable code.)

Identify Dependencies

There are always dependencies in code, you'll never get around this. I want to identify the ones in our code today, and use them to push us to the next stage of development.

  • Dependency 1: the Consumer Key and Secret (hell, include the Access Token and Secret);
  • Dependency 2: the text file (our lyric file);
  • Dependency 3: the directory for said text file;
  • Dependency 4: what to split on (\r\n\r\n in our case, but it could be anything);
  • Dependency 5: the account ID;

This actually sums our dependencies up quite nicely. Now I want to define them in some sort of "configuration" file, in our case I'm going to use JSON because it's just so damn convenient. Let's define the aforementioned JSON config file:

{
      "ConsumerKey": "abcd1234",
      "ConsumerSecret": "abcd1234",
      "AccessToken": "1234-abcd1234",
      "AccessTokenSecret": "abcd1234",
      "AccountId": 9223372036854775807,
      "TextFile": "File.txt",
      "Split": "\r\n",
      "BaseDir": "."
}

Too easy. We can define the entire thing as 8 lines, that describe the type of data we expect. These are all the dependencies we have. All of them. Wrapped up with a nice little bow.

The next step is to bring those dependencies into our code. This is bewilderingly easy with F#: we're going to Install-Package FSharp.Data if you're using NuGet (just type that in the Package Manager Console), if you use packet then you're smarter than me and know exactly how to install it. (I know nothing of packet, but I hear it's nice.)

Now I save the config JSON above in a file called Config.sample.json, we'll need it to pull into F# so that we can define a type for the configuration. This is literally one line (well two, with the open):

open FSharp.Data
type Parameters = JsonProvider<"Config.sample.json">

And done. The JsonProvider will read our Config.sample.json file, and define an entire type for us to use. We can read something in as follows:

let parameters = configFile |> File.ReadAllText |> Parameters.Parse

Easy enough. The configFile will be a filename, I default to Config.json but you can use whatever you like.

Fix our Percent Encoding

If you follow me on Twitter, you'll notice that I built a bot at the request of a good friend / colleague of mine, and I recently tweeted that I broke it. (Which I did, but I fixed it.)

This issue was entirely in our percentEncode, and as I mentioned way back in the last lesson, we didn't account for Unicode characters. This was bad, at least for this new bot (for the Don McLean bot it was a non-issue). This is fixed easily enough, and I want to give you the code to do so:

open System.Text
let encoding = Encoding.UTF8
let percentEncode : string -> string =
    encoding.GetBytes
    >> Array.collect (fun x -> 
        match x with
        | cint when cint = 0x2Duy
                 || cint = 0x2Euy
                 || cint = 0x5Fuy
                 || cint = 0x7Euy
                 || (cint >= 0x30uy && cint <= 0x39uy)
                 || (cint >= 0x41uy && cint <= 0x5Auy)
                 || (cint >= 0x61uy && cint <= 0x7Auy) -> [|cint|]
        | cint -> sprintf "%%%s" (cint.ToString("X")) |> Seq.toArray |> Array.map byte)
    >> Array.map char
    >> String

So our previous code relied on analyzing the char, which is completely inappropriate for Twitter when dealing with non-ASCII text. The char in .NET is a UTF-16 character, whereas Twitter works with UTF-8. So, we have to make an adjustment for that. Instead of using the char, we just use the System.Text.Encoding.UTF8.GetBytes(str) function to get a byte-array for the text we need to encode, then we iterate through it, test each char for the necessity of encoding (because UTF-8 is full ASCII-128 compatible, we don't need to do anything special, our same character codes will work), and return the same resultant String. So nothing comsuming percentEncode has to change (awesome!)._Activator

Analyze a Timeline

So now we get to the hard part. For the bot to be successful I wanted it to be capable of analyzing it's timeline and determining what position it was in to tweet next. This would mean no state has to be retained anywhere, you could in fact run the .exe from any computer and it would still continue the correct sequence.

To do this, we have to read a timeline. To do that, we have to abstract our OAuth / API handling code. UGH

Alright, so this isn't actually that hard. We want to build a composition chain that goes through the full API request.

If you lost our sign function, I have it right here:

let sign httpMethod endpoint oauthParams queryParams postParams =
    Array.concat [|oauthParams; queryParams; postParams|]
    |> Array.sortBy fst
    |> Array.map keyValueToStr
    |> stringJoin "&"
    |> Array.singleton
    |> Array.append [|httpMethod |> httpMethodToStr; endpoint|]
    |> Array.map percentEncode
    |> stringJoin "&"
    |> hasher

You'll see it's slightly different. I have this httpMethodToStr function which, by the looks of things, takes something called httpMethod and converts it to a string. Ah right, I should share that!

type HttpMethod = | Get | Post
let httpMethodToStr = function | Get -> "GET" | Post -> "POST"

Because I like having the help of the compiler, I built a quick HttpMethod type which is just a Get or Post, and the httpMethodToStr converts that to the capital-case value.

The other helper there:

let keyValueToStr (key, value) = sprintf "%s=%s" key (value |> percentEncode)

Again, pretty simple. So now we can see that our sign function is really just some chains. You'll also see I have a hasher, which we didn't have last time.

let hmacSha1Hash (encoding : Encoding) (key : string) (str : string) : string =
    use hmacSha1 = new HMACSHA1(key |> encoding.GetBytes)
    str |> encoding.GetBytes |> hmacSha1.ComputeHash |> Convert.ToBase64String
let baseOauthParams = [|("oauth_signature_method", "HMAC-SHA1"); ("oauth_version", "1.0"); ("oauth_consumer_key", parameters.ConsumerKey); ("oauth_token", parameters.AccessToken)|]
let hasher = hmacSha1Hash encoding ([|parameters.ConsumerSecret; parameters.AccessTokenSecret|] |> stringJoin "&")

Here we see our parameters from the config come into play. We pull the appropriate static OAuth parameters (signature method, version, etc.) into a standard array, then we build a hasher which is a partially-applied function which will only require us to add the final string to hash. The key and encoding are embedded.

For ease of use, I built a sendRequest, which looks as follows:

let sendRequest (submitMethod, oauthString, url : string, postParams) =
    try
        use wc = new WebClient()
        wc.Headers.Add(HttpRequestHeader.Authorization, oauthString)
        match submitMethod with
        | Post -> wc.UploadString(url, postParams)
        | Get -> wc.DownloadString(url)
        |> Some
    with
    | :? WebException as ex ->
        use sr = new StreamReader(ex.Response.GetResponseStream())
        printfn "Failure (%s): %A" ex.Message (sr.ReadToEnd())
        None
    | ex ->
        printfn "Failure: %A" ex
        None

This was really quite simple. Add headers, determine submit method, send the request, return. If it errors, print the error and return None.

Now we previously had a buildTweetRequest which would do a lot of work to create a signed OAuth request. Today we took a lot of that away and instead created a buildBaseRequest, which can be a more abstract version:

let baseBuildRequest submitMethod path postParams queryParams =
    let url = [baseUrl; path] |> stringJoin ""
    let timestamp = DateTime.Now |> timeToEpoch
    let nonce = Guid.NewGuid().ToString("N")
    let oauthParams =
        [|("oauth_timestamp", timestamp.ToString()); ("oauth_nonce", nonce)|]
        |> Array.append baseOauthParams
    let oauthString =
        [|("oauth_signature", sign submitMethod url oauthParams queryParams postParams)|]
        |> Array.append oauthParams
        |> formOAuthString
    let queryString = queryParams |> Array.map keyValueToStr |> stringJoin "&"
    let queryString = if queryString.Length > 0 then sprintf "?%s" queryString else queryString
    let postString = postParams |> Array.map keyValueToStr |> stringJoin "&"
    (submitMethod, oauthString, (sprintf "%s%s" url queryString), postString)

You'll notice that it does a lot of the boilerplate, but it requires quite a few parameters to function properly. This will create the base request, which can be directly piped to sendRequest. This means our new buildTweetRequest looks as follows:

let buildTweetRequest tweet =
    baseBuildRequest Post "statuses/update.json" [||] [|("status", tweet)|]

Again, nothing complex, we just build a base request with the appropriate parameters, and then return it.

Consumption of sending a tweet at this point is literally tweet |> buildTweetRequest |> sendRequest. You can even check if it succeeeded or failed.

Of course, we aren't worried about that yet. We want to get a timeline first:

let buildGetTimeline (userid : int64) =
    baseBuildRequest Get "statuses/user_timeline.json" [||] [|("user_id", userid.ToString()); ("count", "25"); ("tweet_mode", "extended")|]

Oh yeah, getting a timeline is super smooth. Adding new API support, in fact, is very simple and easy. We might be in the home stretch.

let getTimelineTweets = buildGetTimeline >> sendRequest

OK, maybe not. We have the timeline now, but we have to build out the ability to read it.

I have a Twitter Timeline.sample.json which I'll upload as an attachment to this post, which I use to create a type-provider type:

type Timeline = JsonProvider<"Twitter Timeline.sample.json">

I also created a Tweet type where I only care about the important things:

type Tweet = { TId : int64; Text : string; Truncated : bool; CreatedAt : DateTime }

Because I don't give a hoot about the rest of the data, I made it a slimmed down version of an actual tweet.

We're also going to need to consume statuses/show.json, which I'm providing below:

let buildShowRequest (id : int64) =
    baseBuildRequest Get "statuses/show.json" [||] [|("id", id.ToString()); ("tweet_mode", "extended")|]

This is another pretty basic function, it takes a tweet ID and gives us the request to send to get that tweet.

Finally, to get the timeline into memory, we need a timelineToTweets function:

let timelineToTweets (timeline : string) : Tweet array option =
    let mapFn (x : Timeline.Root) = { TId = x.Id; Text = x.FullText; Truncated = x.Truncated; CreatedAt = DateTime.ParseExact(x.CreatedAt.Substring(0, 19), "ddd MMM dd HH:mm:ss", null) }
    let idsEqual t1 t2 = t1.TId = t2.TId
    let tweets = timeline |> Timeline.Parse |> Array.map mapFn
    let idsLookup = tweets |> Array.filter (fun x -> x.Truncated) |> Array.map (fun x -> x.TId)
    idsLookup
    |> Array.choose (buildShowRequest >> sendRequest >> Option.map (Timeline.Parse >> Array.map mapFn))
    |> Array.concat
    |> Some
    |> Option.map (fun x ->
        tweets |> Array.map (fun t1 ->
            x |> Array.filter (idsEqual t1) |> function | [|t|] -> t | _ -> t1))

It should be easy to see that we're just loading a timeline JSON into memory, checking what tweets are "truncated", then looking those up, and mapping them to a new tweet array. (If we had to get a new tweet, we'll use it, otherwise we keep the original.)

Alright, so we have an array of tweets (timeline) in memory, what do we do next? Well, we need to calculate what index we were most recently at, so we can find the next one in the list.

Because our lyric program has several tweets that will be identical in the same sequence, we need to look at more than one tweet in a row. I pulled 25 tweets from the timeline (look at the count parameter in buildGetTimeline), then make sure that we get as many matches as possible.

let getIndex (tweets : Tweet array) (comparisons : string array) =
    let comparisons = comparisons |> Array.rev |> Array.map newLineReplace
    let tweets =
        tweets
        |> Array.sortByDescending (fun x -> x.CreatedAt)
        |> Array.map (fun x -> x.Text)
        |> Array.filter (fun x -> comparisons |> Array.contains x)
    let rec alg skip =
        if skip >= comparisons.Length then 0
        else
            let si = comparisons |> Array.skip skip |> Array.findIndex ((=) tweets.[0]) |> (+) skip
            let matches = tweets |> Array.fold (fun (r, i) t -> (r && (comparisons.[i % comparisons.Length] = t), i + 1)) (true, si) |> fst
            if matches then si else alg (si + 1)
    if tweets.Length = 0 then 0 else alg 0

This is a somewhat convoluted function, but it basically starts at a tweet, then determines if it matched from there on in the comparisons (reference) array. If it does, then that's our current index.

Consume it all

Consuming this whole thing is done by the following function:

member this.run send =
    let path = [rootDir; parameters.TextFile] |> stringJoin ""
    let file = File.ReadAllText(path)
    let parts = file.Split([|parameters.Split|], StringSplitOptions.RemoveEmptyEntries) |> Array.filter (Seq.length >> (>=) 280) |> Array.map (fun x -> x.Trim())

    let existingTweets =
        parameters.AccountId
        |> getTimelineTweets
        |> Option.map timelineToTweets
        |> Option.flatten
        |> function | None -> [||] | Some v -> v
    let newStatus = parts.[(parts.Length - getIndex existingTweets parts) % parts.Length] |> newLineReplace
    let req = buildTweetRequest newStatus

    if send then (req, newStatus, req |> sendRequest)
    else (req, newStatus, None)

Wait, member? That's new.

So F# allows us to define types with members, and in fact, I defined almost all the functions above as let definitions in the type, and then the only necessary member is the single function above. This can be run by a client to either send the next tweet, or just load and return it. I built that in for the case when we want to debug, and see what it's going to send next without actually doing so.

All in all, the entire TwitterBot is 150 lines of F#:

module TwitterBot
open System
open System.IO
open System.Net
open System.Security.Cryptography
open System.Text
open FSharp.Data

type HttpMethod = | Get | Post
type Tweet = { TId : int64; Text : string; Truncated : bool; CreatedAt : DateTime }
type Parameters = JsonProvider<"Config.sample.json">
type Timeline = JsonProvider<"Twitter Timeline.sample.json">

let encoding = Encoding.UTF8
let percentEncode : string -> string =
    encoding.GetBytes
    >> Array.collect (fun x -> 
        match x with
        | cint when cint = 0x2Duy
                 || cint = 0x2Euy
                 || cint = 0x5Fuy
                 || cint = 0x7Euy
                 || (cint >= 0x30uy && cint <= 0x39uy)
                 || (cint >= 0x41uy && cint <= 0x5Auy)
                 || (cint >= 0x61uy && cint <= 0x7Auy) -> [|cint|]
        | cint -> sprintf "%%%s" (cint.ToString("X")) |> Seq.toArray |> Array.map byte)
    >> Array.map char
    >> String

let httpMethodToStr = function | Get -> "GET" | Post -> "POST"
let newLineReplace (str : string) = str.Replace("\r\n", " / ")
let stringJoin sep (strs : string seq) = String.Join(sep, strs)
let keyValueToStr (key, value) = sprintf "%s=%s" key (value |> percentEncode)
let keyValueToQuotedStr (key, value) = sprintf "%s=\"%s\"" key (value |> percentEncode)
let timeToEpoch (time : DateTime) =
    let epoch = DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)
    (time.ToUniversalTime() - epoch).TotalSeconds |> bigint
let hmacSha1Hash (encoding : Encoding) (key : string) (str : string) : string =
    use hmacSha1 = new HMACSHA1(key |> encoding.GetBytes)
    str |> encoding.GetBytes |> hmacSha1.ComputeHash |> Convert.ToBase64String

type Bot(configFile) =
    let parameters = configFile |> File.ReadAllText |> Parameters.Parse
    let baseUrl = "https://api.twitter.com/1.1/"
    let rootDir = sprintf "%s%s" parameters.BaseDir "\\"
    let baseOauthParams = [|("oauth_signature_method", "HMAC-SHA1"); ("oauth_version", "1.0"); ("oauth_consumer_key", parameters.ConsumerKey); ("oauth_token", parameters.AccessToken)|]
    let hasher = hmacSha1Hash encoding ([|parameters.ConsumerSecret; parameters.AccessTokenSecret|] |> stringJoin "&")

    let formOAuthString : (string * string) array -> string =
        Array.sortBy fst
        >> Array.map keyValueToQuotedStr
        >> stringJoin ", "
        >> sprintf "OAuth %s"

    let sign httpMethod endpoint oauthParams queryParams postParams =
        Array.concat [|oauthParams; queryParams; postParams|]
        |> Array.sortBy fst
        |> Array.map keyValueToStr
        |> stringJoin "&"
        |> Array.singleton
        |> Array.append [|httpMethod |> httpMethodToStr; endpoint|]
        |> Array.map percentEncode
        |> stringJoin "&"
        |> hasher

    let sendRequest (submitMethod, oauthString, url : string, postParams) =
        try
            use wc = new WebClient()
            wc.Headers.Add(HttpRequestHeader.Authorization, oauthString)
            match submitMethod with
            | Post -> wc.UploadString(url, postParams)
            | Get -> wc.DownloadString(url)
            |> Some
        with
        | :? WebException as ex ->
            use sr = new StreamReader(ex.Response.GetResponseStream())
            printfn "Failure (%s): %A" ex.Message (sr.ReadToEnd())
            None
        | ex ->
            printfn "Failure: %A" ex
            None

    let baseBuildRequest submitMethod path postParams queryParams =
        let url = [baseUrl; path] |> stringJoin ""
        let timestamp = DateTime.Now |> timeToEpoch
        let nonce = Guid.NewGuid().ToString("N")
        let oauthParams =
            [|("oauth_timestamp", timestamp.ToString()); ("oauth_nonce", nonce)|]
            |> Array.append baseOauthParams
        let oauthString =
            [|("oauth_signature", sign submitMethod url oauthParams queryParams postParams)|]
            |> Array.append oauthParams
            |> formOAuthString
        let queryString = queryParams |> Array.map keyValueToStr |> stringJoin "&"
        let queryString = if queryString.Length > 0 then sprintf "?%s" queryString else queryString
        let postString = postParams |> Array.map keyValueToStr |> stringJoin "&"
        (submitMethod, oauthString, (sprintf "%s%s" url queryString), postString)

    let buildShowRequest (id : int64) =
        baseBuildRequest Get "statuses/show.json" [||] [|("id", id.ToString()); ("tweet_mode", "extended")|]
    let buildGetTimeline (userid : int64) =
        baseBuildRequest Get "statuses/user_timeline.json" [||] [|("user_id", userid.ToString()); ("count", "25"); ("tweet_mode", "extended")|]
    let buildTweetRequest tweet =
        baseBuildRequest Post "statuses/update.json" [||] [|("status", tweet)|]
    let getTimelineTweets = buildGetTimeline >> sendRequest

    let timelineToTweets (timeline : string) : Tweet array option =
        let mapFn (x : Timeline.Root) = { TId = x.Id; Text = x.FullText; Truncated = x.Truncated; CreatedAt = DateTime.ParseExact(x.CreatedAt.Substring(0, 19), "ddd MMM dd HH:mm:ss", null) }
        let idsEqual t1 t2 = t1.TId = t2.TId
        let tweets = timeline |> Timeline.Parse |> Array.map mapFn
        let idsLookup = tweets |> Array.filter (fun x -> x.Truncated) |> Array.map (fun x -> x.TId)
        idsLookup
        |> Array.choose (buildShowRequest >> sendRequest >> Option.map (Timeline.Parse >> Array.map mapFn))
        |> Array.concat
        |> Some
        |> Option.map (fun x ->
            tweets |> Array.map (fun t1 ->
                x |> Array.filter (idsEqual t1) |> function | [|t|] -> t | _ -> t1))

    let getIndex (tweets : Tweet array) (comparisons : string array) =
        let comparisons = comparisons |> Array.rev |> Array.map newLineReplace
        let tweets =
            tweets
            |> Array.sortByDescending (fun x -> x.CreatedAt)
            |> Array.map (fun x -> x.Text)
            |> Array.filter (fun x -> comparisons |> Array.contains x)
        let rec alg skip =
            if skip >= comparisons.Length then 0
            else
                let si = comparisons |> Array.skip skip |> Array.findIndex ((=) tweets.[0]) |> (+) skip
                let matches = tweets |> Array.fold (fun (r, i) t -> (r && (comparisons.[i % comparisons.Length] = t), i + 1)) (true, si) |> fst
                if matches then si else alg (si + 1)
        if tweets.Length = 0 then 0 else alg 0

    member this.run send =
        let path = [rootDir; parameters.TextFile] |> stringJoin ""
        let file = File.ReadAllText(path)
        let parts = file.Split([|parameters.Split|], StringSplitOptions.RemoveEmptyEntries) |> Array.filter (Seq.length >> (>=) 280) |> Array.map (fun x -> x.Trim())

        let existingTweets =
            parameters.AccountId
            |> getTimelineTweets
            |> Option.map timelineToTweets
            |> Option.flatten
            |> function | None -> [||] | Some v -> v
        let newStatus = parts.[(parts.Length - getIndex existingTweets parts) % parts.Length] |> newLineReplace
        let req = buildTweetRequest newStatus

        if send then (req, newStatus, req |> sendRequest)
        else (req, newStatus, None)

This is almost everything necessary, the only thing left to do is run it.

Build a robust client

Because I want robust bot, and I want to be able to hot-swap config files, I set our bot up so that it takes two command line arguments (if you want):

  • a boolean to indicate whether or not to send;
  • a string to indicate the configuration file to load;

Basically, I want to specify the following:

type Parms = { Send : bool; Config : string }

Well that's easy. I've demonstrated this before, but my default pattern for this type of situation is as follows:

let stripQuotes (s : string) = if s.StartsWith "\"" && s.EndsWith "\"" then s.Substring(1, s.Length - 2) else s
let defParms = { Send = false; Config = "Config.json" }
let parms = 
    match argv with
    | [|s; c|] -> { defParms with Send = (s = "true"); Config = c |> stripQuotes }
    | [|s|] -> { defParms with Send = (s = "true") }
    | _ -> defParms
printfn "%A" parms

Essentially, I specify reasonable defaults, then when a command-line interface calls it, you can send arguments to override.

Then, the basic setup:

let bot = parms.Config |> TwitterBot.Bot
let request, tweet, response = bot.run parms.Send
let encodedTweet = tweet |> percentEncode
printfn "Request: %A" request
printfn "Tweet: (%i chars) %A" tweet.Length tweet
printfn "Encoded: (%i chars) %A" encodedTweet.Length encodedTweet
printfn "Response: %A" response

If the Parms.Send was true, the tweet sent, so in the case it's not then I do some human prompting:

if parms.Send |> not then
    let rec getResponse () =
        printfn "Send tweet? (Y for yes, N for no)"
        let key = Console.ReadKey().Key
        printfn ""
        match key with
        | ConsoleKey.Y -> true
        | ConsoleKey.N -> false
        | k ->
            printfn "Invalid key: %A" k
            () |> getResponse

    match () |> getResponse with
    | true -> 
        let request, tweet, response = bot.run true
        let encodedTweet = tweet |> percentEncode
        printfn "Request: %A" request
        printfn "Tweet: (%i chars) %A" tweet.Length tweet
        printfn "Encoded: (%i chars) %A" encodedTweet.Length encodedTweet
        printfn "Response: %A" response
        printfn "Press enter to exit..."
        Console.ReadLine() |> ignore
    | false -> ()

This means we get the best of both worlds: human-interface compatibility, and argument compatibility. Overall, the whole file is 47 lines:

open System
open TwitterBot

type Parms = { Send : bool; Config : string }

[<EntryPoint>]
let main argv =
    let stripQuotes (s : string) = if s.StartsWith "\"" && s.EndsWith "\"" then s.Substring(1, s.Length - 2) else s
    let defParms = { Send = false; Config = "Config.json" }
    let parms = 
        match argv with
        | [|s; c|] -> { defParms with Send = (s = "true"); Config = c |> stripQuotes }
        | [|s|] -> { defParms with Send = (s = "true") }
        | _ -> defParms
    printfn "%A" parms

    let bot = parms.Config |> TwitterBot.Bot
    let request, tweet, response = bot.run parms.Send
    let encodedTweet = tweet |> percentEncode
    printfn "Request: %A" request
    printfn "Tweet: (%i chars) %A" tweet.Length tweet
    printfn "Encoded: (%i chars) %A" encodedTweet.Length encodedTweet
    printfn "Response: %A" response

    if parms.Send |> not then
        let rec getResponse () =
            printfn "Send tweet? (Y for yes, N for no)"
            let key = Console.ReadKey().Key
            printfn ""
            match key with
            | ConsoleKey.Y -> true
            | ConsoleKey.N -> false
            | k ->
                printfn "Invalid key: %A" k
                () |> getResponse

        match () |> getResponse with
        | true -> 
            let request, tweet, response = bot.run true
            let encodedTweet = tweet |> percentEncode
            printfn "Request: %A" request
            printfn "Tweet: (%i chars) %A" tweet.Length tweet
            printfn "Encoded: (%i chars) %A" encodedTweet.Length encodedTweet
            printfn "Response: %A" response
            printfn "Press enter to exit..."
            Console.ReadLine() |> ignore
        | false -> ()
    0

I include the stripQuotes as when we send a path from argument -> program we may need to quote it, which will be included in the argv element. So, we just need to strip them when we expect them.

And that's it, we've built the whole bot, and we can now automate it entirely in one tight, self-contained program. There's only one external dependency, and even that is only for easy of development. We built all the functionality we wanted, and then some, and did it without any major pain points.


Shoutout to Janelle Shane, who's work inspired the second bot written with this script, and who's work also encouraged me to finish it up and tie this two-section blog post series up. I'll be continuing on with the next installment of "Getting Started with Programming and Getting Absolutely Nowhere" in the next post.

Files: Twitter Timeline.sample.json (70.11 kb)Config.sample.json (245.00 bytes)TwitterBot.fs (7.23 kb)Program.fs (1.76 kb)