Closed Stuey2 closed 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.
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.
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
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.
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.
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.
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!"