giraffe-fsharp / Giraffe

A native functional ASP.NET Core web framework for F# developers.
https://giraffe.wiki
Apache License 2.0
2.11k stars 267 forks source link

razorHtmlView caches mutable state #144

Closed Stuey2 closed 6 years ago

Stuey2 commented 6 years ago

Not sure what causes this, but if you combine a route with a razorHtmlView directly it caches the state of the razor view, whereas if you use a wrapper HttpHandler that returns exactly the same thing it works.

Below /messagesWorks works if you mutate testMessages, but /messagesCaches always returns just "Hello world!"

let mutable testMessages = ["Hello world!"]
let getRazorView =
  fun (next : HttpFunc) (ctx : HttpContext) ->
    razorHtmlView "Index" { Text = testMessages |> String.concat ", " } next ctx

let webApp =
  choose [
    GET >=>
      choose [
        route "/messagesWorks" >=> getRazorView
        route "/messagesCaches" >=> razorHtmlView "Index" { Text = testMessages |> String.concat ", " }
      ]
    setStatusCode 404 >=> text "Not Found" ]
gerardtoconnor commented 6 years ago

Hi Stuey2,

This behavior is expected to allow you to cache when appropriate.

The pipelines are pre-complied in giraffe to allow you to set state at startup if needed such that any variables provided in a pipeline are fixed/cached as the tail-end of the handler function calls are all that are remaining to be applied through currying, eg is:

let mutable value  = "Hello"
let cachehandler v = fun next ctx -> ctx.WriteJson v  // anything before args 'next ctx ->' cached copy when immutable data like string/list etc
let freshhandler = fun next ctx -> ctx.WriteJson value // gets direct ref to mutable obj 
let webApp =
  choose [
    GET >=>
      choose [
        route "/messagesFresh" >=> freshhandler 
        route "/messagesCache" >=> cachehandler value  
     ]

strings and lists etc are immutable datastructures such that although you set value to be mutable, once the value is passed by ref to a function or anything, it is no longer linked to "value" ref cell as such. if you create and pass a mutable ref cell or pass a record/class with mutable fields within, these updates would be captured as the underlying reference passed remains the same.

Stuey2 commented 6 years ago

Thanks for the explanation @gerardtoconnor, I'm still a little lost as to when something will cache vs won't. Following on from your explanation, the rule seems to be: if the last value to be serialised is immutable, e.g. string, list, etc. then the result is cached. Or in fact is it that the route is itself cached, so that any immutable data specified as per route is cached?

If that is the case I guess there is no way to pass freshhandler an immutable value from the route and have it not cache the result? For example the following still caches:

let writeJsonMutable data =
  fun (next : HttpFunc) (ctx : HttpContext) -> json data next ctx

This doesn't cache, but is a bit verbose as you have to wrap everything in AnyWrapper:

type AnyWrapper<'T> = { mutable data : 'T}
let writeJsonMutable data =
  fun (next : HttpFunc) (ctx : HttpContext) -> json data.data next ctx

I had hoped this might work, but no go, that's what makes me think the route itself is cached.

type AnyWrapper<'T> = { mutable data : 'T}
let writeJsonMutable data =
  let wrapped = {data = data}
  fun (next : HttpFunc) (ctx : HttpContext) -> json wrapped.data next ctx

Having seen ASP.NET Core Kestrel: Adventures in building a fast web server I realise getting performance is a tricky business. I'm interested, is the caching strategy baked in, or is there a way to disable for certain routes?

I suppose in general you're not going to be returning directly from a route, but some other function so it probably doesn't matter, just interested.

gerardtoconnor commented 6 years ago

The route pipelines are designed to compile/collapse at startup such that the whole continuation pipeline is built & nested (by partially applying the next continuation fn), and it is just awaiting the ctx to execute pipeline tree so thinking of the routes as cached is a way to think of it.

To create a mutable cell ref, to allow swapping of immutable/any references, you can use ref function

let dataCell = ref "" // this is a heap ref pointer to fixed location that can have its contents changed
let writeJsonMutable (data:string ref) : HttpHandler =
  fun next ctx -> json !dataCell next ctx     // '!' operator jumps into the cell and pulls out the underlying value, alternatively, `ref` has property `.Value`

ref cells being equiv to your AnyWrapper<'T> type

To clean things up and make handlers tidier use nested response writing with context extension methods:

let dataCell = ref "" 
dataCell := "Hello World!"      // ':=' operator is used to update value in a ref cell
let writeJsonMutable (data:string ref) : HttpHandler =
  fun next ctx -> ctx.WriteJson dataCell.Value        // .Value prop same as '!' operator

On your last failed example, you can get working by :

type AnyWrapper<'T> = { mutable data : 'T}
let dataWrap = { data = "Hello World" }
dataWrap.data <- "Foo Bar" // updating data, then calling handler will reflect new value "Foo Bar"
let writeJsonMutable (dataWrapper:AnyWrapper<'T>) : HttpHandler =
  fun next ctx -> ctx.WriteJson wrapped.data

as you were binding and caching the wrapped value inside the handler for no reason

Stuey2 commented 6 years ago

Ok, so that explanation I think I get, compile rather than cache, tick - makes sense... and partially applying everything and just awaiting ctx makes sense.

Still some holes though ... example on the freshhandler and cachehandler why does the cachehandler compile the mutable value OR RATHER why doesn't the freshHandler?

I clearly have a bit to learn yet on language side. But is it a language construct or a giraffe construct. To me it seems that they're roughly equivalent, similar I suppose to why doesn't getRazorView compile the mutable testMessages, is there something special about the top level route, i.e. are the internals of the HttpHandler functions not partially applied, but only top level HttpHandler's expressed in the route?

Thanks for the time explaining. I don't know if I'm meant to close this.

gerardtoconnor commented 6 years ago

its result of both the language and the framework i guess, what might help is if I provide equiv OO example of what's going on to help explain closures, ie partially/fully applied functions.

Our HttpHandlers of fun ... next ctx -> look to always partially apply the next and other args at start-up so that all that is being awaited on is the ctx so we're returning a fun ctx -> ..., in OO this is equiv to (and what f# compiles to in IL) :

class HttpHandler {
     HttpFunc _next;
     MyHandler (HttpFunc next) {
           _next = next;   // set next in constructor 
     }
     HttpFuncResult Invoke( HttpContext : ctx ) {
          // your function code here
          this._next ctx
     }
} 

do if we are pre-applying parameters like a string etc, as string is passed by ref and immutable

string mystringvar = "hello" // string obj is allocated to eg: 0x5345346 (random), var mystringvar =  0x5345346
class HttpHandler {
     HttpFunc _next;
     string _string;
     MyHandler (string myString ,HttpFunc next) { // in constructor 0x5345346 is passed for string
           _next = next;   // set next in constructor 
           _string = myString;     // my _string internal references 0x5345346
     }
     HttpFuncResult Invoke( HttpContext : ctx ) {
          // your function code here
          Console.WriteLine(  this._string )  // points to 0x5345346 and will always be "hello"
          this._next ctx
     }
} 
mystringvar = "foobar" // I now create a new string obj at 0x7949741 and set mystringvar to 0x7949741

This is in essence how closures & function currying works in most languages including F#, partially applied parameters are added to internal fields through constructor leaving a class that is just awaiting the next parameter to feed into its Invoke method.

Luckily F# elegantly abstracts all this away in a manner that is terse and clean but sometimes it can be helpful to know internals if things are not making sense.

in your above example, having the "testMessages" mutable makes little to no difference to the value that will be used in function/class if it has already taken a snapshot of the reference on construction/partial-application, in order to get the fresh live value, in your handler, after the context argument (fun next ctx ->) you have to call the variable to ensure it has not been cached/snapshot from earlier at startup when handler was constructed.

dustinmoris commented 6 years ago

I'm closing this as I believe the question has been answered, but if there's any more confusion please feel free to re-open this issue again.