As part of my master's thesis project, I'm writing Figment, an embedded DSL for web development for F#. In the spirit of similar web DSLs like Sinatra and Compojure, it aims to be simple, flexible, idiomatic.
It's still very experimental and likely to change but I'd like to show you what I have so far. So here's "Hello World" in Figment:
First a very basic Global.asax to set the entry point:
<%@ Application Inherits="BasicSampleApp.App" %>
and now the code itself:
namespace BasicSampleApp
open System.Web
open Figment.Routing
open Figment.Actions
type App() =
inherit HttpApplication()
member this.Application_Start() =
get "hi" (content "<h1>Hello World!</h1>")
Run it, visit /hi and you get a big Hello World.
Of course, everything but the last line is boring boilerplate, so let's focus on that last line.
This is basically how it works: first, we have the action type:
type FAction = ControllerContext -> ActionResult
Yes, those are ASP.NET MVC2 classes. Figment is built on top of ASP.NET MVC2. Now, the get function takes a route and an action, and maps GET requests.
get : string -> FAction -> unit
and content is an action generator (or parameterized action), it creates an action that outputs a string as response.
content : string -> FAction
Similarly, there's a redirect action generator, so we can redirect /hello to /hi by saying:
get "hello" (redirect "hi")
Actions and Results
So far we've only seen action generators, now let's see proper actions with a variant of Hello World. We start with a simple function:
let greet firstName lastName age =
sprintf "Hello %s %s, you're %d years old" firstName lastName age
and now we bind it to the request and map it to a route:
let greet' (ctx: ControllerContext) =
let req = ctx.HttpContext.Request
greet req.["firstname"] req.["lastname"] (int req.["age"])
|> sprintf "<p>%s</p>" |> Result.content
get "greetme" greet'
Visit /greetme?firstname=John&lastname=Doe&age=50 to see this in action.
Did you notice Result.content? It maps directly to ContentResult. Normally you don't have both Figment.Actions and Figment.Result open in the same file so usually you can skip writing "Result.".
We could have used Result.view (ViewResult) to send the information to a regular ASP.NET MVC view:
let greet2 (p: NameValueCollection) =
greet p.["firstname"] p.["lastname"] (int p.["age"])
get "greetme2" (bindQuerystring greet2 >> Result.view "someview")
Note also how function composition make it easy to work at any level of abstraction (bindQuerystring is in Figment.Binding)
Filters
Filters are just functions with this signature:
type Filter = FAction -> FAction
With this simple abstraction we can implement authorization, caching, etc. For example, here's how to apply the equivalent of a RequireHttpsAttribute:
get "securegreet" (requireHttps greet')
requireHttps and others live in Figment.Filters.
Routing DSL
Sometimes you need flexibility when defining a route. For example, use a regular expression, or check for a mobile browser. Enter Figment.RoutingConstraints. A routing constraint is a function like this:
type RouteConstraint = HttpContextBase * RouteData -> bool
It returns true if it's a match, false if it's not. It's applied with the action router:
action : RouteConstraint -> FAction -> unit
A trivial route constraint:
let unconstrained (ctx: HttpContextBase, route: RouteData) = true
action unconstrained (content "Hello World")
would map that content to every URL/method. You might think that taking a single constraint is useless, but they can be combined with a few operators to create a small DSL:
let ifGetDsl = ifUrlMatches "^/dsl" &&. ifMethodIsGet
action
(ifGetDsl &&. !. (ifUserAgentMatches "MSIE"))
(content "You're NOT using Internet Explorer")
action ifGetDsl (content "You're using Internet Explorer")
Hopefully this last sample was self-explanatory!
Strongly-typed routing
A couple of blog posts ago I briefly mentioned using PrintfFormat manipulation to define strongly-typed routes. This is what I meant:
let nameAndAge firstname lastname age =
sprintf "Hello %s %s, %d years old" firstname lastname age
|> Result.content
getS "route/{firstname:%s}/{lastname:%s}/{age:%d}" nameAndAge
This actually routes and binds at the same time.
Conclusions
As I said, this is very much work in progress, and there's still a lot to do. I intend to make it fully open source when I finish writing my thesis.
I'll have to analyze tons of web frameworks, in particular functional web frameworks, so hopefully I'll pick up some interesting stuff from Happstack, Snap, Haskell on a Horse, etc. In particular, I'm interested in implementing formlets, IMHO one of the coolest features of WebSharper.
Source code is here.