As I read through the code-base there are a couple of abstraction points that I would implement in the long run, to ensure easy testability and extensibility of the project. This issue is more a discussion issue; I want more feedback, and I would also like to know how these topics would affect performance and if there would be enough gain by implementing it.
Basically anything that is non-functional/non-referentially transparent is this issue.
File IO
It's often nice to be able to stub or use in-memory file systems when writing integration tests of web sites and there are libraries in .Net that allow such abstractions across operating systems, with even better semantics than what .Net gives. One example is DotNetIO. Using it would allow you to stub file systems, serve with in-memory file systems and would allow you to work cross-platform.
Example of existing code, Http.fs:
if Directory.Exists dirname then
let di = new DirectoryInfo(dirname)
(di.GetFileSystemInfos()) |> Array.sortBy (fun x -> x.Name) |> Array.iter buildLine
ok (bytes (result.ToString())) req
else fail
DateTime handling
E.g. when adding code that traces an execution, is can be helpful in tests to 'own' the time. This allows you to test edge cases and NTP daemons moving time backwards.
I normally introduce a Clock interface that can be mocked/stubbed and expected on.
Example from existing code, Log.fs (related to #17):
/// Log a line with the given format, printing the current time in UTC ISO-8601 format
/// and then the string, like such:
/// '2013-10-13T13:03:50.2950037Z: today is the day'
let log format =
Printf.kprintf (lock sync_root <| fun () -> printfn "%s: %s" (DateTime.UtcNow.ToString("o"))) format
Session support, idempotency handling
By moving away from globals we gain testability. While we can use function composition/currying to compose the application, such points of inversion (composing functions) need to be stated from the 'start' of the composition. In other words, it's easy to use a global variable 'far down' into the application and then have to do a lot of refactoring to expose that variable as an interface...
Example from code:
/// Static dictionary of sessions
let session_map = new ConcurrentDictionary<string, ConcurrentDictionary<string, obj>>()
/// Get the session from the HttpRequest
let session (request : HttpRequest) =
let sessionId = request.SessionId
// snip
(which by the way has a race condition)
While this doesn't mean that we necessarily use this global variable, it's still easy for a dev to use what's in the example:
url "/session"
>>= session_support
// snip
and be hit by the fact that suddenly the web app is no longer possible to load-balance without bugs. Instead session_support could take a parameter with a session store implementation:
url "/session" >>= session_support Globals.AppDomainSessionStore
if identical semantics is wanted.
Similarly, idempotency handling based on request ids or hashes could be made composable based on a single-purpose protocol.
Similarly for uploads (ties into the abstraction of a file system).