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.
Cool!
ReplyDeleteRecently 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?
Also, maybe you could use the ? operator to turn req.["firstname"] into req?firstname.
ReplyDelete@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).
ReplyDeleteSounds good! A REPL version combined with IIS Express would make a very sweet combination.
ReplyDeleteNeat! Color me interested.
ReplyDeleteHi 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?.
ReplyDelete@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
ReplyDelete