Monday, June 27, 2011

Refactoring to functional ActionResults in Figment

This is a refactoring tale about Figment, the web framework I've been writing. As with most refactoring tales, I'll be extra verbose and explicit and maybe a little dramatic about each step and its rationale.

Figment is, as I explained when I first wrote about it, based on ASP.NET MVC. As such, it uses many ASP.NET MVC types, like ControllerContext and ActionResult.

Let's say we wanted to create a new ActionResult to model a HTTP "method not allowed" response. The RFC says that this response has a status code 405 and "the response MUST include an Allow header containing a list of valid methods for the requested resource".

Such a response is appropriate when the client issues a request with a HTTP method that is not supported by the server, i.e. not every application can handle a DELETE method. But pretty much every application can handle GET and POST, so when receiving a DELETE request the server could respond with 405 and an Allow: GET, POST header (supporting HEAD is pretty common too, but for this post let's assume only GET and POST are supported).

If we were working with objects and classes we would write a class inheriting ActionResult to encapsulate this, something like (using F#):

type MethodNotAllowed(validMethods: string seq) = 
    inherit ActionResult() with 
        override x.ExecuteResult ctx = 
            ctx.Response.StatusCode <- 405 
            ctx.Response.AppendHeader("Allow", String.Join(", ", validMethods))

and we would use this in Figment like this:

action (ifMethodIs "DELETE") (fun _ -> MethodNotAllowed ["GET"; "POST"])

Thanks to object expressions in F#, we can get away without writing an explicit class:

let methodNotAllowed(validMethods: #seq<string>) = 
    { new ActionResult() with 
        override x.ExecuteResult ctx = 
            ctx.Response.StatusCode <- 405 
            ctx.Response.AppendHeader("Allow", String.Join(", ", validMethods)) }

Now in Figment there's not much difference:

action (ifMethodIs "DELETE") (fun _ -> methodNotAllowed ["GET"; "POST"])

But we can be more concise and functional in the definition of this response.

The first thing to realize is that ActionResult has a single method ExecuteResult with signature ControllerContext -> unit. So we could easily represent it as a regular function ControllerContext -> unit and then build the actual ActionResult whenever we need it:

let inline result r = 
    {new ActionResult() with 
        override x.ExecuteResult ctx = 
            r ctx }

result here is (ControllerContext -> unit) -> ActionResult

This is a pretty common pattern in F# to "functionalize" a single-method interface or class.

Let's also write a little function to execute an ActionResult:

let inline exec ctx (r: ActionResult) = 
    r.ExecuteResult ctx

Setting a status code and header are pretty common things to do. We should encapsulate them into their own ActionResults, and then we can compose them:

let status code = 
    result (fun ctx -> ctx.Response.StatusCode <- code) 

let header name value = 
    result (fun ctx -> ctx.Response.AppendHeader(name, value))

Now we'd like to define methodNotAllowed by composing these two ActionResults, for example:

let allow (methods: #seq<string>) = header "Allow" (String.Join(", ", methods))

let methodNotAllowed methods = status 405 >>. allow methods

Notice how the ControllerContext and Response are implicit now. We can define the >>. operator  like this:

let concat a b = 
    let c ctx = 
        exec ctx a 
        exec ctx b 
    result c

let (>>.) = concat 

That is, concat executes two ActionResults sequentially.

But wait a minute... "sequencing actions"... where have we seen this before? Yup, monads. We have just reinvented the Reader monad. Let's not reinvent it and instead make it explicit, but first we have to refactor Figment to use ControllerContext -> unit instead of ActionResult (it will be still used, but under the covers). This is a simple, mechanical, uninteresting refactor, so I won't show it. Just consider it done. Now we can define:

type ReaderBuilder() =
    member x.Bind(m, f) = fun c -> f (m c) c
    member x.Return a = fun _ -> a
    member x.ReturnFrom a = a

let result = ReaderBuilder()
let (>>.) m f = r.Bind(m, fun _ -> f)
let (>>=) m f = r.Bind(m,f)

>>. is just like Haskell's >> operator, described like this:

Sequentially compose two actions, discarding any value produced by the first, like sequencing operators (such as the semicolon) in imperative languages.

We can still define methodNotAllowed as above, or using computation expression syntax:

let methodNotAllowed methods = 
    result { 
        do! status 405 
        do! allow methods 
    }

Or, if you don't want to use monads, you can just pass the context explicitly:

let methodNotAllowed allowedMethods = 
    fun ctx -> 
        status 405 ctx 
        allow allowedMethods ctx

They're all equivalent definitions.

Now, after the last refactor ("unpacking" ActionResult) we changed the Figment action type from

ControllerContext -> ActionResult

to

ControllerContext -> (ControllerContext -> unit)

which is a pretty silly type if you think about it... but that's a refactoring tale for another day ;-)

I hope this post served to contrast object-oriented and functional code in a familiar environment (ASP.NET MVC), and to show how monads can arise "naturally", it's just a matter of recognizing them.

I should say that this is of course not the only way to do it, and I don't claim this is the best way to do it.

In the next post I'll show more uses and conveniences of having the action as a Reader monad.

3 comments:

  1. Beautiful. I started down this path originally with Frank. The problem I ran into was trying to tie the reader and writer monads together, so I eventually opted for the state monad, which is now also gone. I think this works well for you, though, as you have picked a specific platform. By the way, do you plan to do the same on the write side? I'm curious to see how you handle either a monad transform or executing the reader in your app directly. Cheers!

    ReplyDelete
  2. @Ryan: yeah, I saw that somewhat similar was happening in Frank. However I don't need a writer or a state in Figment, since everything (Request AND Response) is in the ControllerContxt which itself is readonly. The reader is simply executed by passing the ControllerContext instance.
    I still think a pure request -> response type is better, but I don't think it's possible with ASP.NET types, because ActionResult = ControllerContext -> unit , which means nothing but side-effects on ControllerContext, which in turn means having to mock Request, Response, etc, as all ASP.NET apps have to do when testing at that level. I may write a post about testing in Figment, comparing it to Frank.

    ReplyDelete