Samouczek F#: Jak zbudować aplikację F# z pełnym stosem
Opublikowany: 2022-03-11W ostatnich latach programowanie funkcjonalne zyskało reputację szczególnie rygorystycznego i produktywnego paradygmatu. Nie tylko funkcjonalne języki programowania zyskują uwagę społeczności programistów, ale wiele dużych firm zaczyna również używać funkcjonalnych języków programowania do rozwiązywania problemów komercyjnych.
Na przykład Walmart zaczął używać Clojure, opartego na JVM funkcjonalnego dialektu Lisp, do infrastruktury kasowej; Jet.com, duża platforma e-commerce (obecnie należąca do Walmart), wykorzystuje F# do tworzenia większości swoich mikrousług; oraz Jane Street, firma zajmująca się handlem własnym, głównie używają OCamla do tworzenia swoich algorytmów.
Dzisiaj zajmiemy się programowaniem w języku F#. F# to jeden z funkcjonalnych języków programowania, który zyskuje coraz większą popularność dzięki swojej elastyczności, silnej integracji z platformą .NET i wysokiej jakości dostępnych narzędzi. Na potrzeby tego samouczka języka F# utworzymy prosty serwer sieci Web i powiązaną aplikację mobilną, używając tylko języka F# zarówno dla frontonu, jak i zaplecza.
Dlaczego warto wybrać F# i do czego służy F#?
W dzisiejszym projekcie będziemy używać wyłącznie języka F#. Istnieje kilka powodów, aby preferować F# jako nasz wybrany język:
- Integracja .NET: F# ma bardzo ścisłą integrację z resztą świata .NET, dzięki czemu zapewnia łatwy dostęp do dużego ekosystemu dobrze obsługiwanych i dokładnie udokumentowanych bibliotek do rozwiązywania szerokiego zakresu zadań programistycznych.
- Zwięzłość: F# jest niezwykle zwięzły ze względu na potężny system wnioskowania o typach i zwięzłą składnię. Zadania programistyczne często można rozwiązywać znacznie bardziej elegancko przy użyciu języka F# niż C# lub Java. Kod F# może wydawać się bardzo uproszczony w porównaniu.
- Narzędzia programistyczne: F# cieszy się silną integracją z Visual Studio, który jest jednym z najlepszych IDE dla ekosystemu .NET. Dla osób pracujących na platformach innych niż Windows istnieje mnóstwo wtyczek w kodzie Visual Studio. Te narzędzia sprawiają, że programowanie w języku F# jest niezwykle wydajne.
Mógłbym mówić o korzyściach płynących z używania F #, ale bez zbędnych ceregieli, zanurzmy się!
Idea stojąca za naszym samouczkiem F#
W Stanach Zjednoczonych popularne jest powiedzenie: „Gdzieś jest piąta” .
W niektórych częściach świata godzina 17:00 to najwcześniejszy czas, kiedy społecznie akceptowalne jest wypicie drinka lub tradycyjnej filiżanki herbaty.
Dziś zbudujemy aplikację opartą na tej koncepcji. Zbudujemy aplikację, która w dowolnym momencie przeszukuje różne strefy czasowe, dowiaduje się, gdzie jest godzina piąta i przekazuje te informacje użytkownikowi.
Zaplecze
Konfiguracja serwera internetowego
Zaczniemy od stworzenia usługi zaplecza, która wykonuje funkcję wyszukiwania strefy czasowej. Wykorzystamy Suave.IO do zbudowania JSON API.
Suave.IO to intuicyjna platforma internetowa z lekkim serwerem internetowym, która umożliwia bardzo szybkie kodowanie prostych aplikacji internetowych.
Aby rozpocząć, przejdź do programu Visual Studio i uruchom nowy projekt aplikacji konsoli F#. Jeśli ta opcja nie jest dostępna, może być konieczne zainstalowanie funkcji języka F# za pomocą Instalatora programu Visual Studio. Nazwij projekt „FivePM”. Po utworzeniu aplikacji powinieneś zobaczyć coś takiego:
[<EntryPoint>] let main argv = printfn "%A" argv 0 // return an integer exit code
Jest to bardzo prosty fragment kodu startowego, który wyświetla argument i kończy działanie z kodem stanu 0. Możesz swobodnie zmienić instrukcję print i poeksperymentować z różnymi funkcjami kodu. Program formatujący „%A” to specjalny program formatujący, który drukuje ciąg znaków dowolnego typu, który przekazujesz, więc możesz swobodnie drukować liczby całkowite, zmiennoprzecinkowe, a nawet typy złożone. Gdy już opanujesz podstawową składnię, czas zainstalować Suave.
Najłatwiejszym sposobem zainstalowania Suave jest użycie menedżera pakietów NuGet. Przejdź do Projekt -> Zarządzaj pakietami NuGet i kliknij kartę Przeglądaj. Wyszukaj Suave i kliknij zainstaluj. Po zaakceptowaniu pakietów do zainstalowania wszystko powinno być gotowe! Teraz wróć do ekranu program.fs i jesteśmy gotowi do rozpoczęcia budowy serwera.
Aby zacząć korzystać z Suave, najpierw musimy zaimportować pakiet. W górnej części programu wpisz następujące instrukcje:
open Suave open Suave.Operators open Suave.Filters open Suave.Successful
Spowoduje to zaimportowanie podstawowych pakietów wymaganych do zbudowania podstawowego serwera WWW. Teraz zastąp kod w main następującym, który definiuje prostą aplikację i obsługuje ją na porcie 8080:
[<EntryPoint>] let main argv = // Define the port where you want to serve. We'll hardcode this for now. let port = 8080 // create an app config with the port let cfg = { defaultConfig with bindings = [ HttpBinding.createSimple HTTP "0.0.0.0" port]} // We'll define a single GET route at the / endpoint that returns "Hello World" let app = choose [ GET >=> choose [ path "/" >=> request (fun _ -> OK "Hello World!")] ] // Now we start the server startWebServer cfg app 0
Kod powinien wyglądać bardzo prosto, nawet jeśli nie znasz składni języka F# lub sposobu definiowania procedur obsługi tras przez Suave, kod powinien być dość czytelny. Zasadniczo aplikacja internetowa powraca ze statusem 200 i „Hello World!” ciąg, gdy zostanie trafiony żądaniem GET na trasie „/”. Śmiało i uruchom aplikację (F5 w Visual Studio) i przejdź do localhost: 8080, a powinieneś zobaczyć „Hello World!” w oknie przeglądarki.
Refaktoryzacja kodu serwera
Teraz mamy serwer WWW! Niestety nie robi zbyt wiele - więc dodajmy mu trochę funkcjonalności! Po pierwsze, przenieśmy funkcjonalność serwera WWW gdzie indziej, abyśmy zbudowali jakąś funkcjonalność bez martwienia się o serwer WWW (później połączymy go z serwerem WWW). Zdefiniuj osobną funkcję w ten sposób:
// We'll use argv later :) let runWebServer argv = // Define the port where you want to serve. We'll hardcode this for now. let port = 8080 // create an app config with the port let cfg = { defaultConfig with bindings = [ HttpBinding.createSimple HTTP "0.0.0.0" port]} // We'll define a single GET route at the / endpoint that returns "Hello World" let app = choose [ GET >=> choose [ path "/" >=> request (fun _ -> OK "Hello World!")] ] // Now we start the server startWebServer cfg app
Teraz zmień główną funkcję na następującą i upewnij się, że zrobiliśmy to dobrze.
[<EntryPoint>] let main argv = runWebServer argv 0
Naciśnij F5 i nasze „Witaj świecie!” serwer powinien działać jak poprzednio.
Uzyskiwanie stref czasowych
Teraz zbudujmy funkcjonalność, która określa strefę czasową, w której jest godzina piąta. Chcemy napisać kod, aby przejść przez wszystkie strefy czasowe i określić strefę czasową najbliższą 17:00.
Co więcej, tak naprawdę nie chcemy zwracać strefy czasowej, która jest bardzo bliska 17:00, ale nieco wcześniej (np. 16:58), ponieważ na potrzeby tej demonstracji założenie jest takie, że nie może być przed 17:00. 1:00, jednak blisko.
Zacznijmy od uzyskania listy stref czasowych. W F# jest to bardzo proste, ponieważ tak dobrze integruje się z C#. Dodaj „otwarty system” u góry i zmień swoją aplikację F# na następującą:
[<EntryPoint>] let main argv = // This gets all the time zones into a List-like object let tzs = TimeZoneInfo.GetSystemTimeZones() // Now we iterate through the list and print out the names of the timezones for tz in tzs do printfn "%s" tz.DisplayName 0
Uruchom aplikację i powinieneś zobaczyć w konsoli listę wszystkich stref czasowych, ich przesunięcia i nazwę wyświetlaną.
Tworzenie i używanie typu niestandardowego
Teraz, gdy mamy listę stref czasowych, możemy przekonwertować je na niestandardowy typ danych, który jest dla nas bardziej przydatny, coś, co zawiera informacje, takie jak przesunięcie UTC, czas lokalny, jak daleko jest od 17:00 czasu lokalnego itd. W tym celu zdefiniujmy typ niestandardowy, tuż nad główną funkcją:
type TZInfo = {tzName: string; minDiff: float; localTime: string; utcOffset: float}
Teraz możemy przekształcić i informacje o strefie czasowej, które otrzymaliśmy z ostatniego kroku, w listę obiektów TZInfo. Zmień swoją główną funkcję w ten sposób:
[<EntryPoint>] let main argv = // This gets all the time zones into a List-like object let tzs = TimeZoneInfo.GetSystemTimeZones() // List comprehension + type inference allows us to easily perform conversions let tzList = [ for tz in tzs do // convert the current time to the local time zone let localTz = TimeZoneInfo.ConvertTime(DateTime.Now, tz) // Get the datetime object if it was 5:00pm let fivePM = DateTime(localTz.Year, localTz.Month, localTz.Day, 17, 0, 0) // Get the difference between now local time and 5:00pm local time. let minDifference = (localTz - fivePM).TotalMinutes yield { tzName=tz.StandardName; minDiff=minDifference; localTime=localTz.ToString("hh:mm tt"); utcOffset=tz.BaseUtcOffset.TotalHours; } ] printfn "%A" tzList.Head 0
Powinieneś zobaczyć obiekt tzInfo dla czasu Dateline Standard wydrukowany na ekranie.
Sortowanie, filtrowanie i orurowanie, o mój!
Teraz, gdy mamy listę tych obiektów tzInfo, możemy filtrować i sortować te obiekty, aby znaleźć strefę czasową, w której jest 1) po 17:00 i 2) najbliższa 17:00 strefy czasowej w 1). Zmień swoją główną funkcję w ten sposób:
[<EntryPoint>] let main argv = // This gets all the time zones into a List-like object let tzs = TimeZoneInfo.GetSystemTimeZones() // List comprehension + type inference allows us to easily perform conversions let tzList = [ for tz in tzs do // convert the current time to the local time zone let localTz = TimeZoneInfo.ConvertTime(DateTime.Now, tz) // Get the datetime object if it was 5:00pm let fivePM = DateTime(localTz.Year, localTz.Month, localTz.Day, 17, 0, 0) // Get the difference between now local time and 5:00pm local time. let minDifference = (localTz - fivePM).TotalMinutes yield { tzName=tz.StandardName; minDiff=minDifference; localTime=localTz.ToString("hh:mm tt"); utcOffset=tz.BaseUtcOffset.TotalHours; } ] // We use the pipe operator to chain functiona calls together let closest = tzList // filter so that we only get tz after 5pm |> List.filter (fun (i:TZInfo) -> i.minDiff >= 0.0) // sort by minDiff |> List.sortBy (fun (i:TZInfo) -> i.minDiff) // Get the first item |> List.head printfn "%A" closest
A teraz powinniśmy mieć strefę czasową, której szukamy.
Refaktoryzacja gettera strefy czasowej do jego własnej funkcji
Teraz zrefaktoryzujmy kod do jego własnej funkcji, abyśmy mogli go później użyć. Zdefiniuj funkcję w ten sposób:
// the function takes uint as input, and we represent that as "()" let getClosest () = // This gets all the time zones into a List-like object let tzs = TimeZoneInfo.GetSystemTimeZones() // List comprehension + type inference allows us to easily perform conversions let tzList = [ for tz in tzs do // convert the current time to the local time zone let localTz = TimeZoneInfo.ConvertTime(DateTime.Now, tz) // Get the datetime object if it was 5:00pm let fivePM = DateTime(localTz.Year, localTz.Month, localTz.Day, 17, 0, 0) // Get the difference between now local time and 5:00pm local time. let minDifference = (localTz - fivePM).TotalMinutes yield { tzName=tz.StandardName; minDiff=minDifference; localTime=localTz.ToString("hh:mm tt"); utcOffset=tz.BaseUtcOffset.TotalHours; } ] // We use the pipe operator to chain function calls together tzList // filter so that we only get tz after 5pm |> List.filter (fun (i:TZInfo) -> i.minDiff >= 0.0) // sort by minDiff |> List.sortBy (fun (i:TZInfo) -> i.minDiff) // Get the first item |> List.head And our main function can just be: [<EntryPoint>] let main argv = printfn "%A" <| getClosest() 0
Uruchom kod i powinieneś zobaczyć te same dane wyjściowe, co poprzednio.

Kodowanie danych zwrotu JSON
Teraz, gdy możemy uzyskać dane o strefie czasowej, możemy przekształcić informacje w JSON i obsłużyć je za pośrednictwem naszej aplikacji. To całkiem proste, dzięki pakietowi JSON.NET firmy NewtonSoft. Wróć do menedżera pakietów NuGet i znajdź Newtonsoft.Json i zainstaluj pakiet. Teraz wróć do Program.fs i dokonaj małej zmiany w naszej głównej funkcji:
[<EntryPoint>] let main argv = printfn "%s" <| JsonConvert.SerializeObject(getClosest()) 0
Uruchom kod teraz i zamiast obiektu TZInfo powinieneś zobaczyć JSON wydrukowany na twojej konsoli.
Łączenie informacji o strefie czasowej z JSON API
Bardzo łatwo jest połączyć to z naszym API JSON. Po prostu wprowadź następujące zmiany w funkcji runWebServer:
// We'll use argv later :) let runWebServer argv = // Define the port where you want to serve. We'll hardcode this for now. let port = 8080 // create an app config with the port let cfg = { defaultConfig with bindings = [ HttpBinding.createSimple HTTP "0.0.0.0" port]} // We'll define a single GET route at the / endpoint that returns "Hello World" let app = choose [ GET >=> choose [ // We are getting the closest time zone, converting it to JSON, then setting the MimeType path "/" >=> request (fun _ -> OK <| JsonConvert.SerializeObject(getClosest())) >=> setMimeType "application/json; charset=utf-8" ] ] // Now we start the server startWebServer cfg app
Uruchom aplikację i przejdź do localhost:8080. Powinieneś zobaczyć JSON w oknie przeglądarki.
Wdrażanie serwera
Teraz, gdy mamy serwer JSON API, możemy go wdrożyć, aby był dostępny w Internecie. Jednym z najłatwiejszych sposobów wdrożenia tej aplikacji jest usługa App Service Microsoft Azure, którą można rozumieć jako zarządzaną usługę IIS. Aby wdrożyć w usłudze Azure App Service, przejdź do https://portal.azure.com i przejdź do usługi App Service. Utwórz nową aplikację i przejdź do centrum wdrażania w swoim portalu. Portal może być nieco przytłaczający, jeśli jest to Twój pierwszy raz, więc jeśli masz problemy, zapoznaj się z jednym z wielu samouczków dotyczących korzystania z usługi App Service.
Powinieneś zobaczyć różne opcje wdrażania. Możesz użyć dowolnego, ale dla uproszczenia możemy użyć opcji FTP.
Usługa aplikacji szuka pliku web.config w katalogu głównym aplikacji, aby dowiedzieć się, jak uruchomić aplikację. Ponieważ nasz serwer WWW jest niezbędną aplikacją konsolową, możemy opublikować aplikację i zintegrować się z serwerem IIS za pomocą HttpPlatformHandler. W programie Visual Studio dodaj plik XML do projektu i nadaj mu nazwę web.config. Wypełnij go następującym kodem XML:
<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <handlers> <remove name="httpplatformhandler" /> <add name="httpplatformhandler" path="*" verb="*" modules="httpPlatformHandler" resourceType="Unspecified"/> </handlers> <httpPlatform stdoutLogEnabled="true" stdoutLogFile="suave.log" startupTimeLimit="20" processPath=".\publish\FivePM.exe" arguments="%HTTP_PLATFORM_PORT%"/> </system.webServer> </configuration>
Połącz się z serwerem FTP, używając danych uwierzytelniających uzyskanych z centrum wdrażania (musisz kliknąć opcję FTP). Przenieś plik web.config do folderu wwwroot witryny FTP usługi aplikacji.
Teraz chcemy zbudować i opublikować naszą aplikację, ale zanim to zrobimy, musimy wprowadzić niewielką zmianę w kodzie serwera. Przejdź do funkcji runServer i zmień pierwsze 3 wiersze na następujące:
let runWebServer (argv:string[]) = // Define the port where you want to serve. We'll hardcode this for now. let port = if argv.Length = 0 then 8080 else (int argv.[0])
Dzięki temu aplikacja może spojrzeć na przekazany argument i użyć pierwszego argumentu jako numeru portu, zamiast zakodować go na stałe w 8080. W konfiguracji sieciowej jako pierwszy argument przekazujemy %HTTP_PLATFORM_PORT%, więc należy ustawić.
Skompiluj aplikację w trybie wydania, opublikuj aplikację i skopiuj opublikowany folder do wwwroot. Uruchom ponownie aplikację i powinieneś zobaczyć wynik interfejsu API JSON w witrynie *.azurewebsites.net
.
Teraz nasza aplikacja jest wdrożona!
Frontend
Teraz, gdy mamy już wdrożony serwer, możemy zbudować front-end. Na potrzeby frontendu zbudujemy aplikację na Androida przy użyciu Xamarin i F#. Ten stos, podobnie jak nasze środowisko zaplecza, jest głęboko zintegrowany z programem Visual Studio. Oczywiście ekosystem F# obsługuje kilka opcji programistycznych front-end (WebSharper, Fable/Elmish, Xamarin.iOS, DotLiquid itp.), ale w trosce o zwięzłość, w tym poście będziemy programować tylko przy użyciu Xamarin.Android i wyjść je do przyszłych postów.
Konfiguracja
Aby skonfigurować aplikację dla systemu Android, rozpocznij nowy projekt i wybierz opcję Xamarin Android. Upewnij się, że masz zainstalowane narzędzia programistyczne dla Androida. Po skonfigurowaniu projektu powinieneś zobaczyć coś takiego w głównym pliku kodu.
[<Activity (Label = "FivePMFinder", MainLauncher = true, Icon = "@mipmap/icon")>] type MainActivity () = inherit Activity () let mutable count:int = 1 override this.OnCreate (bundle) = base.OnCreate (bundle) // Set our view from the "main" layout resource this.SetContentView (Resources.Layout.Main) // Get our button from the layout resource, and attach an event to it let button = this.FindViewById<Button>(Resources.Id.myButton) button.Click.Add (fun args -> button.Text <- sprintf "%d clicks!" count count <- count + 1 )
To jest kod startowy dla platformy F# Android Xamarin. Kod obecnie tylko śledzi, ile razy przycisk został kliknięty i wyświetla bieżącą wartość licznika. Możesz zobaczyć, jak działa, naciskając F5, aby uruchomić emulator i uruchomić aplikację w trybie debugowania.
Dodawanie komponentów interfejsu użytkownika
Dodajmy kilka składników interfejsu użytkownika i sprawmy, aby był bardziej użyteczny. Otwórz zasób/układy i przejdź do Main.axml.
Powinieneś zobaczyć wizualną reprezentację głównego układu działania. Możesz edytować różne elementy interfejsu użytkownika, klikając elementy. Możesz dodać elementy, przechodząc do przybornika i wybierając element, który chcesz dodać. Zmień nazwę przycisku i dodaj textView pod przyciskiem. Reprezentacja XML twojego AXML powinna wyglądać podobnie do tego:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android: android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/fivePM" /> <TextView android:text="" android:textAppearance="?android:attr/textAppearanceMedium" android:layout_width="match_parent" android:layout_height="wrap_content" android: /> </LinearLayout>
AXML odwołuje się do pliku zasobów ciągów, więc otwórz plik resources/values/strings.xml i wprowadź następujące zmiany:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="fivePM">It\'s 5PM Somewhere!</string> <string name="app_name">5PM Finder</string> </resources>
Teraz zbudowaliśmy front-endowy AXML. Teraz połączmy to z jakimś kodem. Przejdź do MainActivity.fs i wprowadź następujące zmiany w funkcji onCreate:
base.OnCreate (bundle) // Set our view from the "main" layout resource this.SetContentView (Resources.Layout.Main) // Get our button from the layout resource, and attach an event to it let button = this.FindViewById<Button>(Resources.Id.myButton) let txtView = this.FindViewById<TextView>(Resources.Id.textView1); button.Click.Add (fun args -> let webClient = new WebClient() txtView.Text <- webClient.DownloadString("https://fivepm.azurewebsites.net/") )
Zastąp fivepm.azurewebsites.net adresem URL własnego wdrożenia interfejsu API JSON. Uruchom aplikację i kliknij przycisk w emulatorze. Za chwilę powinieneś zobaczyć zwrot JSON API z wynikiem API.
Parsowanie JSON
Prawie jesteśmy na miejscu! W tej chwili nasza aplikacja wyświetla surowy JSON i jest on raczej nieczytelny. Następnym krokiem jest zatem przeanalizowanie JSON i wygenerowanie ciągu bardziej czytelnego dla człowieka. Aby przeanalizować JSON, możemy użyć biblioteki Newtonsoft.JSON z serwera.
Przejdź do menedżera pakietów NuGet i wyszukaj plik Newtonsoft.JSON. Zainstaluj i wróć do pliku MainActivity.fs. Zaimportuj go, dodając „open Newtonsoft.Json”.
Teraz dodaj do projektu typ TZInfo. Moglibyśmy ponownie użyć TZInfo z serwera, ale ponieważ w rzeczywistości potrzebujemy tylko dwóch pól, możemy zdefiniować tutaj typ niestandardowy:
type TZInfo = { tzName:string localTime: string }
Dodaj definicję typu nad główną funkcją, a teraz zmień główną funkcję w ten sposób:
button.Click.Add (fun args -> let webClient = new WebClient() let tzi = JsonConvert.DeserializeObject<TZInfo>(webClient.DownloadString("https://fivepm.azurewebsites.net/")) txtView.Text <- sprintf "It's (about) 5PM in the\n\n%s Timezone! \n\nSpecifically, it is %s there" tzi.tzName tzi.localTime )
Teraz wynik interfejsu API JSON jest deserializowany do obiektu TZInfo i używany do konstruowania ciągu. Uruchom aplikację i kliknij przycisk. Powinieneś zobaczyć sformatowany ciąg wyskakujący na ekranie.
Chociaż ta aplikacja jest bardzo prosta i być może nierafinowana, zbudowaliśmy aplikację mobilną, która wykorzystuje F# JSON API, przekształca dane i wyświetla je użytkownikowi. A wszystko to zrobiliśmy w F#. Pobaw się różnymi kontrolkami i sprawdź, czy możesz ulepszyć projekt.
Mamy to! Prosta aplikacja mobilna F# i F# JSON API i informuje użytkownika, gdzie jest godzina piąta.
Zawijanie
Dzisiaj przeszliśmy przez tworzenie prostego interfejsu API sieci Web i prostej aplikacji dla systemu Android przy użyciu tylko języka F#, demonstrując zarówno wyrazistość języka F#, jak i siłę ekosystemu F#. Jednak ledwie zarysowaliśmy powierzchnię rozwoju F#, więc napiszę jeszcze kilka postów, aby oprzeć się na tym, co omówiliśmy dzisiaj. Dodatkowo mam nadzieję, że ten post zainspirował Cię do zbudowania własnych aplikacji F#!
Kod, którego użyliśmy w tym samouczku, można znaleźć w serwisie GitHub.