F# web z Suave

Najpopularniejszym frameworkiem webowym dla aplikacji .NET jest ASP.NET MVC. Pracując w C# sprawdza się on się świetnie, ale bazuje na obiektowym paradygmacie programowania, co nie do końca współgra z funkcyjnym podejściem F#.

Na szczęście jest Suave, który pozwala na proste i funkcyjne pisanie aplikacji w F#.

Wprowadzenie

Pierwszym naszym krokiem będzie pobranie paczki nuget Suave. Następnie, musimy otworzyć moduł w naszym kodzie:

open Suave

I gdy chcemy uruchomić nasz serwer to użyjemy funkcji:

startWebServer defaultConfig app

Gdzie defaultConfig jest domyślną konfiguracją HTTP o zasięgu 127.0.0.1 i porcie 8080. A app jest dowolną aplikacją typu WebPart.

Czym jest WebPart?

type WebPart = HttpContext -> Async<HttpContext option>

Jest to funkcja, która przyjmuje HttpContext i zwraca asynchroniczny workflow, który zwraca obiekt typu HttpContext option, czyli albo Some x, gdzie x to HttpContext, albo None.

Asynchroniczny workflow to coś á la metoda async Task w C#, która może działać synchronicznie i asynchronicznie.

Zmiana konfiguracji

Zanim zobaczymy jak się taką aplikację definiuje, to zobaczmy jak zmienić konfigurację. Ponieważ jest ona rekordem to wystarczy użyć tej domyślnej jako wzorca i zmienić to co chcemy. Np:

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

Gdzie ustalamy, że nadal będziemy używać HTTP, ale nasłuchujemy na 0.0.0.0 na porcie 80.

Wszystkie możliwe opcje znajdziecie tu: Server configuration.

Więc teraz będziemy uruchamiali nasz serwer przez

startWebServer config app

Tworzenie aplikacji

Więc potrzebujemy coś typu WebPart co będzie określało naszą aplikację. Suave został tak zaprojektowany, że aplikacja może być jedną czynnością, zbiorem czynności, lub zbiorem zbiorów, itd.

Najprostsza czynność to stała czynność, np:

let app = OK "Hello World!"

Funkcja OK przyjmuje tekst i zwraca WebPart, który oznacza, że po wejściu na serwer dostaniemy odpowiedź o numerze HTTP 200 OK oraz o zawartości równiej podanemu tekstowi.

Aby mieć dostęp do OK musimy otworzyć moduł Successful.

Powiedzmy, że mamy jakiś obiekt, którego stan będzie się zmieniał. Niech będzie to obecny czas serwera. Jeśli zrobilibyśmy tak jak poprzednio

let app = OK (sprintf "Obecnie jest %A" System.DateTime.Now)

to w momencie przejścia aplikacji przez tą linijkę zostałaby wczytana data i zapisana do wartości zwróconej przez OK jako stała. Natomiast my chcielibyśmy wykonywać to polecenie za każdym requestem.

let app = request (fun httpRequest -> OK sprintf "Obecnie jest %A" System.DateTime.Now)

Funkcja request przyjmuje funkcję HttpRequest -> WebPart i ta funkcja jest wykonywana za każdym razem kiedy wyślemy zapytanie do serwera.

Wybieranie zachowania

Do tego momentu jakikolwiek url byśmy do naszego serwera nie dopisali, to otrzymamy zawsze to samo. Aby wybrać inną akcję dla danej ścieżki, skorzystamy z funkcji choose.

let app =
    choose [
        path "/" >=> OK "To jest strona główna"
        path "/blog" >=> OK "To jest strona blog."
        path "/date" >=> request (fun httpRequest -> OK sprintf "Obecnie jest %A" System.DateTime.Now)
        NOT_FOUND "Nie znaleziono strony."
    ]

Aby powyższy kawałek kodu się skompilował musi otworzyć więcej modułów

open Suave
open Suave.Filters
open Suave.Operators
open Suave.Successful
open Suave.RequestErrors

Jak możecie się domyślać operator >=> jest zdefiniowany w module Operators, a NOT_FOUND w module RequestErrors. Natomiast funkcja path jest w module Filters.

Więc jak widzimy kiedy przychodzi zapytanie do serwera, odpytujemy nasz app i widzimy że jest to wybór. Idziemy po kolei i sprawdzamy czy pasujemy do danego filtra. Jeśli tak to wykonujemy WebPart, na który on wskazuje. Warto na koniec dać odpowiedź NOT_FOUND aby wiadomo było, że ktoś wchodzi w zły url.

W module Filters są również takie funkcje jak:

  • pathStarts, która przyjmuje string i działa tak jak path, ale sprawdza tylko początkową część.
  • pathRegex, która dopasowywuje ścieżkę na podstawie podanego wyrażenia regularnego.
  • hasFlag, która przyjmuje string oznaczający flagę w url (po ?).
  • GET, POST, PUT, DELETE, HEAD, CONNECT, PATCH, TRACE, OPTIONS - czyli sprawdzające czasownik HTTP.
  • log*, zestaw funkcji logujących zapytanie
  • pathScan, która przyjmuje string w formacie Printf oraz funkcję z argumentów w stringu w WebPart
  • timeoutWebPart, która przyjmuje TimeSpan i WebPart i określa timeout na wykonanie akcji w WebPart, po którym zwraca HTTP 408 z wiadomością "Request Timeout".

Podsumowanie

Jak widać jest to dość proste i dość niskie API, które bardzo ładnie wpasowywuje się w funkcyjną naturę F#. Naprawdę łatwo można w nim pisać aplikacje dostarczające dane (mamy moduł Json), ale do tworzenia aplikacji zwracających HTML potrzeba nam jeszcze silnika w stylu Razora/DotLiquid.

Polecam także przeczytać ten artykuł, który pokazuje jeszcze kilka funkcji Suave’a. Oraz projekt F# Snippets - Web site gdzie możecie zobaczyć praktyczne użycie Suave’a do stworzenia strony internetowej.