Search code examples
f#suave

not understanding the Suave API, always the same result is returned


Here is a test:

open System
open System.Threading
open Newtonsoft.Json
open Suave
open Suave.Logging
open Suave.Operators
open Suave.Filters
open Suave.Writers


let private configuration = {
    defaultConfig with
        bindings   = [ HttpBinding.createSimple HTTP "0.0.0.0" 80 ]
}


let private getServerTime () : WebPart =
    DateTime.UtcNow |> JsonConvert.SerializeObject |> Successful.OK >=> setMimeType "application/json"


let private webApplication =
    choose
        [
            GET >=> choose
                [
                    path "/servertime"   >=> getServerTime ()
                ]
        ]

let start () =
    let listening, server = startWebServerAsync configuration webApplication
    server    |> Async.Start
    listening |> Async.RunSynchronously |> ignore


[<EntryPoint>]
let main _ =

    start()

    Thread.Sleep(Timeout.Infinite)
    0

with this example, the getServerTime function is called once and this is it, every subsequent call to the endpoint will return the original result.

I don't understand why? When I use pathScan with parameters, then the function is called each time, as expected, but in this case, with a simple get, only the first call is done while this is defined as a function.

But I don't understand the doc at all either (from the flow of the doc, its contents and the overall document structure...), so the answer is probably simple :)


Solution

  • First of all, I would highly recommend that you study monadic composition. This is a necessary foundation for understanding these things. It will give you an idea of what >=> and >>= are and how to deal with them.

    As for the problem at hand: yes, you defined getServerTime as a function, but that kind of doesn't matter, because that function is only called once, during construction of the webApplication value.

    The structure of the server is such that it's literally a function HttpContext -> Async<HttpContext option>. It gets a request context and returns a modified version of it. And all of those combinators - choose and >=> and so on - they work with such functions.

    The expression path "/servertime" is also such function. Literally. You can call it like this:

    let httpCtx = ...
    let newCtxAsync = path "/servertime" httpCtx
    

    Moreover, the expression getServerTime() is ALSO such function. So you can do:

    let httpCtx = ...
    let newCtxAsync = getServerTime () httpCtx
    

    That's what the WebPart type is. It's an async function from context to new context.

    Now, what the >=> operator does is combine these functions. Makes them pipe the context from one webpart to the next. That's all.


    When you wrote your getServerTime function, you created a WebPart that always returns the same thing. It's kind of like this:

    let f x y = printf "x = %d" x
    let g = f 42
    

    Here, g is a function (just like a WebPart is a function), but whenever it's called, it will always return "x = 42". Why? Because I partially applied that parameter, it's sort of "baked in" in the definition of g now. The same way the current time is "baked in" in the WebPart that you have created inside getServerTime.

    If you want a different time to be returned every time, you need to recreate the WebPart every time. Construct a new WebPart on every call, one with that call's time baked in. The smallest change to do that is probably this:

    let private getServerTime () : WebPart =
        let time : WebPart = fun ctx -> (DateTime.UtcNow |> string |> Successful.OK) ctx
        time >=> setMimeType "text/plain"
    

    On the surface it may look like the definition of time is silly: after all, let f x = g x can always be replaced by let f = g, right? Well, not always. Only as long as g is pure. But your webpart here is not: it depends on the current time.

    This way, every time the time webpart is "run" (which means it gets a context as a parameter), it will run DateTime.UtcNow, then pass it to Successful.OK, and then pass the context to the resulting function.