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 ActionResult
s, 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 ActionResult
s, 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 ActionResult
s 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.
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@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.
ReplyDeleteI 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.
Great post, thank you
ReplyDelete