want brevity in .net mvc code? switch to f#.
Cats: Uncategorized
Tags: bistromvc, f#, mvc
It's been a while (seems like my writing is slightly manic - either all or nothing), but i actually have something to write about. I finally got a chance to clean up the FSharpExtensions project for bistro (you can find the new release here). While the underlying concepts are the same, a recent stint with Django (which is a strong influence in the Bistro framework) reminded me of how little work you have to do in python to get going with a controller, and that bistro and the fs extensions still have some catching up to do on the brevity front.
So here's what's new (from the download page)
Return values can now contain expressions, using the 'named' aliasing function: given function foo(), "foo() |> named 'bar'" will place the result of calling 'foo' onto the request context under the name 'bar'
Translation: before you had to
1 2 3 4 | [<Bind("get /hello/world/{parm}"); ReflectedDefinition>] let reallySimpleController (ctx: ictx) parm = let foo = parm + "2" foo |
but now, you can
1 2 3 | [<Bind("get /hello/world/{parm}"); ReflectedDefinition>] let reallySimpleController (ctx: ictx) parm = parm + "2" |> named "foo" |
Session values do not need aliasing - the statement 'SessionValue foo' will place 'foo' onto the session context directly
Which means this will work without the 'named' function shown above
1 2 3 | [<Bind("get /hello/world/{parm}"); ReflectedDefinition>] let reallySimpleController (ctx: ictx) parm = SessionValue parm |
The session and request context namespaces have now been merged. This means that it is no longer necessary to qualify function input parameters with the session discriminator, as data will be pulled from wherever it is available. Return values should still use the SessionValue discriminator to specify that something should go onto the session
This one is a bit trickier and has some ramifications (good and bad).
1 2 3 4 5 6 7 8 | [<Bind("get /hello/world/{parm}"); ReflectedDefinition>] let reallySimpleController (ctx: ictx) parm = let something = some_complicated_function parm SessionValue something [<Bind("get /hello/world/{parm}"); ReflectedDefinition>] let slightlyMoreComplicatedController (ctx: ictx) something = do_something_else something |> named "something_else" |
You can see that I didn't need to use the old-style syntax of having to specify the type of something in my second controller (it would have been (something: string session) before). The drawback of this is that you now can have a collision between the two namespaces, which is bad. There are two considerations here - one, the order of processing is deterministic. Request context always wins, as bistro apps tend to be light on the session. Two, my work with NDjango (which has a similar limitation) has shown me how little I run into that, so it's a limitation i'm willing to live with, seeing as how it drops the need for funky types and wrapping/unwrapping.
A new syntax for form fields is now available. If a controller function takes a record type as a parameter, and that record type is marked with the 'FormData' attribute, the fields of that record are populated from the form data.
This one is pretty cool. Say I have a form (with ndjango syntax):
1 2 3 4 5 6 |
and a corresponding controller. The old way of handling this form would have been
1 2 3 4 5 | [<Bind("post /do/something"); ReflectedDefinition>] let reallySimpleController (ctx: ictx) (fName: string form) (mName: string form) (lName: string form) (weight: weight form) = if String.IsNullOrEmpty fName.Value then report_error "first name is required" ... fName.Value |> named 'fName', mName.Value |> named 'mName', lName.Value |> named 'lName', weight.Value |> named 'weight' |
the new way, however, is a bit neater:
1 2 3 4 5 6 7 8 9 10 | [<FormData>] type somethingForm = { fName: string; lName: string; mName: string; weight: int; } [<Bind("post /do/something"); ReflectedDefinition>] let reallySimpleController (ctx: ictx) (data: somethingForm) = if String.IsNullOrEmpty somethingForm.fName then report_error "first name is required" ... () |
All you do is declare a record that defines the fields and data types you care about, and work with that record. What's more, is that by default (this is an overridable parameter to the FormData attribute) the fields are automatically passed through to the request context without your intervention, so you don't have to repeat the field list twice for no good reason.
Neat, huh? All of these changes has taken it to the point where you almost never need type declarations, and the compiler can typically infer the necessary data types.
On the über-geeky side of this, the most complex part of all of this was actually recognizing and parsing out calls to the 'named' function. It took the inference code from this:
1 2 3 4 5 6 7 8 9 10 11 12 | /// attempts to locate the return type of the given expression tree let rec try_get_return_sig = function | Lambda (_,ex) -> try_get_return_sig ex | Let (_,_,cont) -> try_get_return_sig cont | IfThenElse (_,t,e) -> choose try_get_return_sig t e | NewTuple e -> Some <| List.map (fun (ex: Expr) -> ex.ToString(), ex.Type) e | Sequential (f,s) -> choose try_get_return_sig f s | TryFinally (tr,fn) -> choose try_get_return_sig tr fn | TryWith (tr,_,wt,_,fn) -> choose (choose try_get_return_sig fn) wt tr | Var var as ex -> Some [(ex.ToString(), ex.Type)] | Value (o, tp) -> if tp = typeof<Unit> then Some [] else None | _ -> None |
to this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | let rec get_raw_value = function | Value (o, tp) -> o.ToString() | Var (e) -> e.Name | NewUnionCase (info, lst) when List.exists ((=) info.Name) valid_unions -> get_raw_value <| List.head lst | _ -> "" let rec extract_named cont = match cont with | Lambda (p,ex) -> match ex with | Call (_, mi, exlist) -> match mi.Name with | "named" -> Some mi.ReturnType | _ -> None | _ -> extract_named ex | ShapeLambda (var2, expr) -> extract_named expr | ShapeCombination (obj, expr_lst) -> List.tryPick extract_named expr_lst | _ -> None let (|CallNamed|_|) = function | Lambda (var,ex) -> match var.Name, ex with | "name", Lambda (var2, ex2) -> match var2.Name, ex2 with | "expr", Call (_, mi2, exlist) -> match mi2.Name, exlist with | "named", h::t when h.Type = typeof<String> -> Some (unwrap_type mi2.ReturnType) | _ -> None | _ -> None | _ -> None | _ -> None let (|LambdaNamed|_|) = function | "name", Lambda (var2, ex2) -> match var2.Name, ex2 with | "expr", Call (_, mi2, exlist) -> match mi2.Name, exlist with | "named", h::t when h.Type = typeof<String> -> Some (get_raw_value h, unwrap_type mi2.ReturnType) | _ -> None | _ -> None | _ -> None /// attempts to locate the return type of the given expression tree let rec try_get_return_sig = function | Call (ex,mi,exlist) -> let run_list = List.tryPick (fun e -> match e with | Var (_) | NewUnionCase (_,_) -> None | _ -> try_get_return_sig e) match mi.Name with | "named" -> match exlist with | h::t when h.Type = typeof<String> -> Some [get_raw_value h, unwrap_type mi.ReturnType] | _ -> run_list exlist | "op_PipeLeft" -> match exlist with | f::s::t -> match f with | CallNamed t -> Some [get_raw_value s, t] | _ -> run_list exlist | _ -> run_list exlist | "op_PipeRight" -> match exlist with | f::s::t -> match s with | CallNamed t -> Some [get_raw_value f, t] | _ -> run_list exlist | _ -> run_list exlist | _ -> run_list exlist | Lambda (var,ex) -> match var.Name, ex with | LambdaNamed (name,tp) -> Some [name, tp] | _ -> try_get_return_sig ex | Let (var,value,cont) -> match var.Name with | "name" -> match extract_named cont with | Some func_type -> Some [get_raw_value value, unwrap_type func_type] | None -> try_get_return_sig cont | _ -> try_get_return_sig cont | IfThenElse (_,t,e) -> choose try_get_return_sig t e | NewTuple e -> Some ( List.fold (fun s (ex: Expr) -> match try_get_return_sig ex with | Some l -> s @ l | None -> s) [] e) | Sequential (f,s) -> choose try_get_return_sig f s | TryFinally (tr,fn) -> choose try_get_return_sig tr fn | TryWith (tr,_,wt,_,fn) -> choose (choose try_get_return_sig fn) wt tr | Var var as ex -> Some [(get_raw_value ex, unwrap_type ex.Type)] | Value (o, tp) -> if tp = typeof<Unit> then Some [] else None | NewUnionCase (info, lst) as v when List.exists ((=) info.Name) valid_unions -> Some [get_raw_value v, v.Type] | _ -> None |
I'm sure there's room for improvement, but a lot of work comes in recognizing all the different ways the quotation tree can be built. The unit tests show all the different invocations. Let's just say that part took some digging...