Tutorial F#: come creare un'app F# full-stack
Pubblicato: 2022-03-11Negli ultimi anni, la programmazione funzionale si è guadagnata la reputazione di paradigma particolarmente rigoroso e produttivo. Non solo i linguaggi di programmazione funzionale stanno guadagnando attenzione all'interno delle comunità di programmatori, ma anche molte grandi aziende stanno iniziando a utilizzare linguaggi di programmazione funzionale per risolvere problemi commerciali.
Ad esempio, Walmart ha iniziato a utilizzare Clojure, un dialetto Lisp funzionale basato su JVM, per la sua infrastruttura di pagamento; Jet.com, una grande piattaforma di e-commerce (ora di proprietà di Walmart), utilizza F# per creare la maggior parte dei suoi microservizi; e Jane Street, una società di trading proprietario, utilizzano principalmente OCaml per costruire i loro algoritmi.
Oggi esploreremo la programmazione in F#. F# è uno dei linguaggi di programmazione funzionale che sta vedendo una crescente adozione grazie alla sua flessibilità, forte integrazione .NET e alta qualità degli strumenti disponibili. Ai fini di questo tutorial su F#, creeremo un semplice server Web e un'app mobile correlata utilizzando solo F# sia per il front-end che per il back-end.
Perché scegliere F# e a cosa serve F#?
Per il progetto di oggi, utilizzeremo nient'altro che F#. Ci sono diversi motivi per preferire F# come lingua preferita:
- Integrazione .NET: F# ha un'integrazione molto stretta con il resto del mondo .NET e quindi un accesso immediato a un ampio ecosistema di librerie ben supportate e accuratamente documentate per risolvere un'ampia gamma di attività di programmazione.
- Concisione: F# è estremamente conciso grazie al suo potente sistema di inferenza del tipo e alla sintassi concisa. Le attività di programmazione possono spesso essere risolte in modo molto più elegante utilizzando F# rispetto a C# o Java. Il codice F# può apparire molto snello rispetto al confronto.
- Strumenti per sviluppatori: F# gode di una forte integrazione con Visual Studio, che è uno dei migliori IDE per l'ecosistema .NET. Per coloro che lavorano su piattaforme non Windows, c'è un'abbondanza di plugin nel codice di Visual Studio. Questi strumenti rendono la programmazione in F# estremamente produttiva.
Potrei continuare sui vantaggi dell'utilizzo di F#, ma senza ulteriori indugi, tuffiamoci!
L'idea dietro il nostro tutorial su F#
Negli Stati Uniti c'è un detto popolare: "Sono le cinque da qualche parte" .
In alcune parti del mondo, le 17:00 sono il primo momento in cui è socialmente accettabile bere un drink o una tradizionale tazza di tè.
Oggi creeremo un'applicazione basata su questo concetto. Costruiremo un'applicazione che, in qualsiasi momento, ricercherà i vari fusi orari, scoprirà dove sono le cinque e fornirà tali informazioni all'utente.
Il back-end
Configurazione del server web
Inizieremo creando il servizio di back-end che esegue la funzione di ricerca del fuso orario. Useremo Suave.IO per creare un'API JSON.
Suave.IO è un framework Web intuitivo con un server Web leggero che consente di codificare semplici app Web molto rapidamente.
Per iniziare, vai a Visual Studio e avvia un nuovo progetto di applicazione console F#. Se questa opzione non è disponibile, potrebbe essere necessario installare la funzionalità F# con Visual Studio Installer. Assegna un nome al progetto "FivePM". Una volta creata l'applicazione, dovresti vedere qualcosa del genere:
[<EntryPoint>] let main argv = printfn "%A" argv 0 // return an integer exit code
Questo è un codice di partenza molto semplice che stampa l'argomento ed esce con il codice di stato 0. Sentiti libero di cambiare l'istruzione print e sperimentare varie funzioni del codice. Il formattatore "%A" è un formattatore speciale che stampa la rappresentazione della stringa di qualsiasi tipo venga passato, quindi sentiti libero di stampare numeri interi, float o anche tipi complessi. Una volta che hai dimestichezza con la sintassi di base, è il momento di installare Suave.
Il modo più semplice per installare Suave è tramite il gestore di pacchetti NuGet. Vai a Progetto -> Gestisci pacchetti NuGet e fai clic sulla scheda Sfoglia. Cerca Suave e fai clic su Installa. Una volta accettati i pacchetti da installare, dovresti essere tutto pronto! Ora torna alla schermata del tuo programma.fs e siamo pronti per iniziare a costruire il server.
Per iniziare a utilizzare Suave, dovremo prima importare il pacchetto. Nella parte superiore del tuo programma, digita le seguenti istruzioni:
open Suave open Suave.Operators open Suave.Filters open Suave.Successful
Ciò importerà i pacchetti di base richiesti per creare un server Web di base. Ora sostituisci il codice nel main con il seguente, che definisce una semplice app e la serve sulla porta 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
Il codice dovrebbe apparire molto semplice, anche se non si ha familiarità con la sintassi di F# o il modo in cui Suave definisce i gestori di route, il codice dovrebbe essere abbastanza leggibile. In sostanza, l'app Web ritorna con uno stato 200 e "Hello World!" stringa quando viene colpito con una richiesta GET sulla rotta "/". Vai avanti ed esegui l'applicazione (F5 in Visual Studio) e vai a localhost: 8080 e dovresti vedere "Hello World!" nella finestra del tuo browser.
Refactoring del codice del server
Ora abbiamo un server web! Sfortunatamente, non fa molto, quindi diamogli alcune funzionalità! Innanzitutto, spostiamo la funzionalità del server Web altrove in modo da creare alcune funzionalità senza preoccuparci del server Web (lo collegheremo al server Web in seguito). Definire una funzione separata così:
// 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
Ora cambia la funzione principale come segue e assicurati di averlo fatto bene.
[<EntryPoint>] let main argv = runWebServer argv 0
Premi F5 e il nostro "Hello World!" il server dovrebbe funzionare come prima.
Ottenere fusi orari
Ora costruiamo la funzionalità che determina il fuso orario in cui sono le cinque. Vogliamo scrivere del codice per scorrere tutti i fusi orari e determinare il fuso orario più vicino alle 17:00.
Inoltre, non vogliamo proprio restituire un fuso orario molto vicino alle 17:00 ma leggermente precedente (es. 16:58) perché, ai fini di questa dimostrazione, la premessa è che non può essere prima delle 5 :00 pm, comunque vicino.
Iniziamo ottenendo un elenco di fusi orari. In F# questo è molto semplice poiché si integra così bene con C#. Aggiungi "sistema aperto" in alto e cambia la tua applicazione F # in questo:
[<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
Esegui l'applicazione e dovresti vedere nella tua console un elenco di tutti i fusi orari, i loro offset e il loro nome visualizzato.
Creazione e utilizzo di un tipo personalizzato
Ora che abbiamo l'elenco dei fusi orari, possiamo convertirli in un tipo di dati personalizzato che è più utile per noi, qualcosa che contiene informazioni come l'offset UTC, l'ora locale, la distanza dalle 17:00 nell'ora locale , ecc. A tal fine, definiamo un tipo personalizzato, proprio sopra la tua funzione principale:
type TZInfo = {tzName: string; minDiff: float; localTime: string; utcOffset: float}
Ora possiamo trasformare e le informazioni sul fuso orario che abbiamo ottenuto dall'ultimo passaggio in un elenco di questi oggetti TZInfo. Modifica la tua funzione principale in questo modo:
[<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
E dovresti vedere l'oggetto tzInfo per Dateline Standard time stampato sullo schermo.
Ordinamento, filtraggio e tubazioni, Oh mio!
Ora che abbiamo un elenco di questi oggetti tzInfo, possiamo filtrare e ordinare questi oggetti per trovare il fuso orario in cui si trova 1) dopo le 17:00 e 2) più vicino alle 17:00 dei fusi orari in 1). Cambia la tua funzione principale in questo modo:
[<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
E ora dovremmo avere il fuso orario che stiamo cercando.
Refactoring del getter del fuso orario alla sua stessa funzione
Ora eseguiamo il refactoring del codice sulla sua funzione in modo da poterlo utilizzare in seguito. Definire una funzione così:
// 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
Esegui il codice e dovresti vedere lo stesso output di prima.

Codifica JSON dei dati di ritorno
Ora che possiamo ottenere i dati del fuso orario, possiamo trasformare le informazioni in JSON e servirle tramite la nostra applicazione. Questo è abbastanza semplice, grazie al pacchetto JSON.NET di NewtonSoft. Torna al tuo gestore di pacchetti NuGet e trova Newtonsoft.Json e installa il pacchetto. Ora torna a Program.fs e apporta una piccola modifica alla nostra funzione principale:
[<EntryPoint>] let main argv = printfn "%s" <| JsonConvert.SerializeObject(getClosest()) 0
Esegui ora il codice e invece dell'oggetto TZInfo, dovresti vedere il JSON stampato sulla tua console.
Collegamento delle informazioni sul fuso orario all'API JSON
È molto semplice collegarlo alla nostra API JSON. Basta apportare le seguenti modifiche alla funzione 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
Eseguire l'applicazione e passare a localhost:8080. Dovresti vedere il JSON nella finestra del tuo browser.
Distribuzione del server
Ora che abbiamo il server API JSON, possiamo distribuirlo in modo che sia accessibile su Internet. Uno dei modi più semplici per distribuire questa applicazione è tramite il servizio app di Microsoft Azure, che può essere inteso come un servizio IIS gestito. Per eseguire la distribuzione nel servizio app di Azure, vai a https://portal.azure.com e vai al servizio app. Crea una nuova app e accedi al centro di distribuzione nel tuo portale. Il portale può essere un po' opprimente se è la tua prima volta, quindi se hai problemi assicurati di consultare uno dei tanti tutorial disponibili per l'utilizzo del servizio app.
Dovresti vedere una varietà di opzioni per la distribuzione. Puoi usare quello che ti piace, ma per semplicità, possiamo usare l'opzione FTP.
Il servizio app cerca un file web.config nella radice dell'applicazione per sapere come eseguire l'applicazione. Poiché il nostro server Web è un'applicazione console essenziale, possiamo pubblicare l'applicazione e integrarla con il server IIS utilizzando HttpPlatformHandler. In Visual Studio, aggiungi un file XML al tuo progetto e denominalo web.config. Riempilo con il seguente 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>
Collegarsi al server FTP utilizzando le credenziali ottenute dal centro di distribuzione (sarà necessario fare clic sull'opzione FTP). Sposta il file web.config nella cartella wwwroot del sito FTP del servizio app.
Ora vogliamo creare e pubblicare la nostra applicazione, ma prima di farlo, dobbiamo apportare una piccola modifica al codice del server. Vai alla tua funzione runServer e cambia le prime 3 righe come segue:
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])
Ciò consente all'applicazione di guardare l'argomento passato e utilizzare il primo argomento come numero di porta anziché avere la porta hardcoded su 8080. Nella configurazione web, passiamo %HTTP_PLATFORM_PORT% come primo argomento, quindi dovrebbe essere impostato.
Crea l'applicazione in modalità di rilascio, pubblica l'applicazione e copia la cartella pubblicata in wwwroot. Riavvia l'applicazione e dovresti vedere il risultato dell'API JSON nel sito *.azurewebsites.net
.
Ora la nostra applicazione è distribuita!
Il Front End
Ora che abbiamo distribuito il server, possiamo creare un front-end. Per il front-end, creeremo un'applicazione Android usando Xamarin e F#. Questo stack, come il nostro ambiente back-end, gode di una profonda integrazione con Visual Studio. Ovviamente, l'ecosistema F# supporta alcune opzioni di sviluppo front-end (WebSharper, Fable/Elmish, Xamarin.iOS, DotLiquid ecc.), ma per brevità, svilupperemo solo Xamarin.Android per questo post e lasceremo loro per post futuri.
Impostare
Per configurare l'app Android, avvia un nuovo progetto e seleziona l'opzione Xamarin Android. Assicurati di avere installato gli strumenti di sviluppo Android. Una volta impostato il progetto, dovresti vedere qualcosa di simile nel tuo file di codice principale.
[<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 )
Questo è il codice di avviamento per F# Android Xamarin. Il codice attualmente tiene traccia di quante volte è stato cliccato un pulsante e mostra il valore di conteggio corrente. Puoi vederlo funzionare premendo F5 per avviare l'emulatore e avviare l'applicazione in modalità debug.
Aggiunta di componenti dell'interfaccia utente
Aggiungiamo alcuni componenti dell'interfaccia utente e rendiamolo più utile. Apri risorsa/layout e vai a Main.axml.
Dovresti vedere una rappresentazione visiva del layout dell'attività principale. Puoi modificare i vari elementi dell'interfaccia utente facendo clic sugli elementi. Puoi aggiungere elementi andando alla casella degli strumenti e selezionando l'elemento che desideri aggiungere. Rinomina il pulsante e aggiungi una visualizzazione di testo sotto il pulsante. La rappresentazione XML del tuo AXML dovrebbe essere simile a questa:
<?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>
L'AXML fa riferimento al file di risorse delle stringhe, quindi apri il tuo resources/values/strings.xml e apporta le seguenti modifiche:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="fivePM">It\'s 5PM Somewhere!</string> <string name="app_name">5PM Finder</string> </resources>
Ora abbiamo costruito il front-end AXML. Ora colleghiamolo a del codice. Passare a MainActivity.fs e apportare le seguenti modifiche alla funzione 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/") )
Sostituisci fivepm.azurewebsites.net con l'URL della tua distribuzione API JSON. Esegui l'applicazione e fai clic sul pulsante nell'emulatore. Tra un po', dovresti vedere l'API JSON restituita con il risultato dell'API.
Analisi JSON
Ci siamo quasi! In questo momento, la nostra app sta visualizzando il JSON grezzo ed è piuttosto illeggibile. Il passaggio successivo, quindi, è l'analisi del JSON e l'output di una stringa più leggibile. Per analizzare JSON, possiamo utilizzare la libreria Newtonsoft.JSON dal server.
Passa al gestore di pacchetti NuGet e cerca Newtonsoft.JSON. Installa e torna al file MainActivity.fs. Importalo aggiungendo "open Newtonsoft.Json".
Ora aggiungi il tipo TZInfo al progetto. Potremmo riutilizzare TZInfo dal server, ma poiché in realtà abbiamo bisogno solo di due dei campi, possiamo definire un tipo personalizzato qui:
type TZInfo = { tzName:string localTime: string }
Aggiungi la definizione del tipo sopra la funzione principale e ora modifica la funzione principale in questo modo:
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 )
Ora il risultato dell'API JSON viene deserializzato nell'oggetto TZInfo e utilizzato per costruire una stringa. Esegui l'app e fai clic sul pulsante. Dovresti vedere la stringa formattata apparire sullo schermo.
Sebbene questa applicazione sia molto semplice e forse non raffinata, abbiamo creato un'applicazione mobile che consuma e F# API JSON, trasforma i dati e li mostra all'utente. E abbiamo fatto tutto in F#. Sentiti libero di giocare con i vari controlli e vedere se puoi migliorare il design.
E ci siamo! Una semplice applicazione mobile F# e un'API JSON F# e indica all'utente dove sono le cinque.
Avvolgendo
Oggi abbiamo esaminato la creazione di una semplice API Web e di una semplice applicazione Android utilizzando solo F#, dimostrando sia l'espressività del linguaggio F# che la forza dell'ecosistema F#. Tuttavia, abbiamo a malapena scalfito la superficie dello sviluppo di F#, quindi scriverò un altro paio di post per costruire su ciò di cui abbiamo discusso oggi. Inoltre, spero che questo post ti abbia ispirato a creare le tue applicazioni F#!
Puoi trovare il codice che abbiamo usato per questo tutorial su GitHub.