Tuesday, August 3, 2010

Figment: a web DSL for F#

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.

7 comments:

Frank de Groot said...

Cool!

Recently I read about node.js (see nodejs.org): a web server from Google that is entirely non-blocking. Maybe you could combine that with F#'s async to make a more scalable web server?

Frank de Groot said...

Also, maybe you could use the ? operator to turn req.["firstname"] into req?firstname.

Mauricio Scheffer said...

@Frank: right now my plan is to rely on IIS and ASP.NET MVC for a large part of the infrastructure. However, I'm thinking about adding support for async actions (they would map to AsyncControllers or something like that. I'm also thinking about writing a small server for REPL development (i.e. used from fsi).

Frank de Groot said...

Sounds good! A REPL version combined with IIS Express would make a very sweet combination.

George Mauer said...

Neat! Color me interested.

Anonymous said...

Hi Mausch, I've downloaded you web DSL to learn as what it does but how can i see it working?. When i debug the console Application all i get is a ASP.NET page which shows me contents of the debug folder?.

Mauricio Scheffer said...

@Anonymous: either set up an IIS website with the Sample directory as root, or from Visual Studio set the Sample project as StartUp project and make sure the external program points to your WebStarter.exe