Tutorial F#: Cum să construiți o aplicație F# full-stack
Publicat: 2022-03-11În ultimii ani, programarea funcțională și-a câștigat reputația ca paradigmă deosebit de riguroasă și productivă. Nu numai că limbajele de programare funcționale câștigă atenție în cadrul comunităților de programatori, dar multe companii mari încep să folosească limbaje de programare funcționale pentru a rezolva probleme comerciale.
De exemplu, Walmart au început să folosească Clojure, un dialect funcțional Lisp bazat pe JVM, pentru infrastructura de checkout; Jet.com, o platformă mare de comerț electronic (deținută acum de Walmart), folosește F# pentru a construi majoritatea microserviciilor sale; și Jane Street, o firmă de tranzacționare proprie, folosesc în primul rând OCaml pentru a-și construi algoritmii.
Astăzi, vom explora programarea F#. F# este unul dintre limbajele de programare funcționale care înregistrează o adoptare în creștere datorită flexibilității, integrării puternice .NET și calității înalte a instrumentelor disponibile. În scopul acestui tutorial F#, vom construi un server web simplu și o aplicație mobilă asociată folosind doar F# atât pentru front-end, cât și pentru back-end.
De ce să alegeți F# și pentru ce este folosit F#?
Pentru proiectul de astăzi, nu vom folosi decât F#. Există mai multe motive pentru a prefera F# ca limbă preferată:
- Integrare .NET: F# are o integrare foarte strânsă cu restul lumii .NET și, prin urmare, acces facil la un ecosistem mare de biblioteci bine susținute și complet documentate pentru rezolvarea unei game largi de sarcini de programare.
- Concizie: F# este extrem de concis datorită sistemului său puternic de inferență și sintaxei concise. Sarcinile de programare pot fi adesea rezolvate mult mai elegant folosind F# decât C# sau Java. Codul F# poate părea foarte simplificat prin comparație.
- Instrumente pentru dezvoltatori: F# se bucură de o integrare puternică Visual Studio, care este unul dintre cele mai bune IDE-uri pentru ecosistemul .NET. Pentru cei care lucrează pe platforme non-Windows, există o abundență de pluginuri în codul de studio vizual. Aceste instrumente fac programarea în F# extrem de productivă.
Aș putea continua despre beneficiile utilizării F#, dar fără alte prelungiri, haideți!
Ideea din spatele tutorialului nostru F#
În Statele Unite, există o zicală populară: „Este ora cinci pe undeva” .
În unele părți ale lumii, ora 17:00 este prima oră când este acceptabil din punct de vedere social să bei o băutură sau o ceașcă tradițională de ceai.
Astăzi, vom construi o aplicație bazată pe acest concept. Vom construi o aplicație care, la orice moment, caută prin diferitele fusuri orare, află unde este ora cinci și oferă acele informații utilizatorului.
The Back End
Configurarea serverului web
Vom începe prin a realiza serviciul back-end care realizează funcția de căutare a fusului orar. Vom folosi Suave.IO pentru a construi un API JSON.
Suave.IO este un cadru web intuitiv cu un server web ușor, care permite codificarea foarte rapidă a aplicațiilor web simple.
Pentru a începe, accesați Visual Studio și începeți un nou proiect F# Console Application. Dacă această opțiune nu vă este disponibilă, poate fi necesar să instalați funcționalitatea F# cu Visual Studio Installer. Denumiți proiectul „FivePM”. Odată ce aplicația dvs. este creată, ar trebui să vedeți ceva de genul acesta:
[<EntryPoint>] let main argv = printfn "%A" argv 0 // return an integer exit code
Aceasta este o bucată foarte simplă de cod de pornire care imprimă argumentul și iese cu codul de stare 0. Simțiți-vă liber să schimbați instrucțiunea de tipărire și să experimentați cu diferite funcții ale codului. Formatatorul „%A” este un formatator special care tipărește reprezentarea șirului de orice tip pe care îl treceți, așa că nu ezitați să imprimați numere întregi, float sau chiar tipuri complexe. Odată ce vă simțiți confortabil cu sintaxa de bază, este timpul să instalați Suave.
Cel mai simplu mod de a instala Suave este prin intermediul managerului de pachete NuGet. Accesați Proiect -> Gestionați pachetele NuGet și faceți clic pe fila de răsfoire. Căutați Suave și faceți clic pe instalare. Odată ce acceptați pachetele de instalat, ar trebui să fiți gata! Acum reveniți la ecranul program.fs și suntem gata să începem construirea serverului.
Pentru a începe să folosim Suave, va trebui să importam mai întâi pachetul. În partea de sus a programului, tastați următoarele instrucțiuni:
open Suave open Suave.Operators open Suave.Filters open Suave.Successful
Acest lucru va importa pachetele de bază necesare pentru a construi un server web de bază. Acum înlocuiți codul principal cu următorul, care definește o aplicație simplă și o servește pe portul 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
Codul ar trebui să arate foarte simplu, chiar dacă nu sunteți familiarizat cu sintaxa F# sau cu modul Suave de a defini gestionatorii de rute, codul ar trebui să fie destul de lizibil. În esență, aplicația web revine cu statutul 200 și „Hello World!” șir atunci când este lovit cu o solicitare GET pe ruta „/”. Continuați și rulați aplicația (F5 în Visual Studio) și navigați la localhost:8080 și ar trebui să vedeți „Hello World!” în fereastra browserului dvs.
Refactorizarea codului serverului
Acum avem un server web! Din păcate, nu face un întreg - așa că hai să-i dăm niște funcționalități! Mai întâi, să mutăm funcționalitatea serverului web în altă parte, astfel încât să construim unele funcționalități fără să ne facem griji pentru serverul web (o vom conecta la serverul web mai târziu). Definiți o funcție separată astfel:
// 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
Acum schimbați funcția principală la următoarea și asigurați-vă că am făcut-o corect.
[<EntryPoint>] let main argv = runWebServer argv 0
Apăsați F5 și „Hello World!” serverul ar trebui să funcționeze ca înainte.
Obținerea fusurilor orare
Acum să construim funcționalitatea care determină fusul orar în care este ora cinci. Vrem să scriem un cod pentru a repeta prin toate fusurile orare și pentru a determina fusul orar care este cel mai apropiat de ora 17:00.
În plus, nu dorim să returnăm un fus orar care este foarte aproape de ora 17:00, dar este puțin înainte (de exemplu, 16:58), deoarece, în scopul acestei demonstrații, premisa este că nu poate fi înainte de 5. :00 pm, oricât de aproape.
Să începem prin a obține o listă de fusuri orare. În F# acest lucru este foarte ușor, deoarece se integrează atât de bine cu C#. Adăugați „Sistem deschis” în partea de sus și schimbați aplicația F# la aceasta:
[<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
Rulați aplicația și ar trebui să vedeți în consolă o listă cu toate fusurile orare, decalajele și numele lor afișat.
Crearea și utilizarea unui tip personalizat
Acum că avem lista de fusuri orare, le putem converti într-un tip de date personalizat care ne este mai util, ceva care conține informații precum decalajul UTC, ora locală, cât de departe este de 5:00 pm în ora locală. , etc. În acest scop, să definim un tip personalizat, chiar deasupra funcției principale:
type TZInfo = {tzName: string; minDiff: float; localTime: string; utcOffset: float}
Acum putem transforma și informațiile despre fusul orar pe care le-am obținut de la ultimul pas într-o listă a acestor obiecte TZInfo. Schimbați funcția principală astfel:
[<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
Și ar trebui să vedeți obiectul tzInfo pentru Dateline Standard Time imprimat pe ecran.
Sortare și filtrare și canalizare, Oh My!
Acum că avem o listă cu aceste obiecte tzInfo, putem filtra și sorta aceste obiecte pentru a găsi fusul orar în care este 1) după ora 17:00 și 2) cel mai apropiat de 17:00 din fusurile orare din 1). Schimbați funcția principală astfel:
[<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
Și acum ar trebui să avem fusul orar pe care îl căutăm.
Refactorizarea Getter-ului fus orar la propria sa funcție
Acum să refactorizăm codul la propria sa funcție, astfel încât să îl putem folosi mai târziu. Definiți o funcție astfel:
// 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
Rulați codul și ar trebui să vedeți aceeași ieșire ca înainte.

Codificarea JSON a datelor returnate
Acum că putem obține datele fusului orar, putem transforma informațiile în JSON și le putem servi prin aplicația noastră. Acest lucru este destul de simplu, datorită pachetului JSON.NET de la NewtonSoft. Reveniți la managerul de pachete NuGet și găsiți Newtonsoft.Json și instalați pachetul. Acum reveniți la Program.fs și faceți o mică modificare în funcția noastră principală:
[<EntryPoint>] let main argv = printfn "%s" <| JsonConvert.SerializeObject(getClosest()) 0
Rulați codul acum și în loc de obiectul TZInfo, ar trebui să vedeți JSON tipărit pe consolă.
Conectarea informațiilor despre fusul orar la API-ul JSON
Este foarte simplu să conectați acest lucru la API-ul nostru JSON. Pur și simplu faceți următoarele modificări în funcția 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
Rulați aplicația și navigați la localhost:8080. Ar trebui să vedeți JSON în fereastra browserului dvs.
Implementarea serverului
Acum că avem serverul API JSON, îl putem implementa astfel încât să fie accesibil pe internet. Una dintre cele mai ușoare modalități de a implementa această aplicație este prin intermediul serviciului de aplicații Microsoft Azure, care poate fi înțeles ca un serviciu IIS gestionat. Pentru a implementa serviciul Azure App, mergeți la https://portal.azure.com și accesați App Service. Creați o nouă aplicație și navigați la centrul de implementare din portalul dvs. Portalul poate fi puțin copleșitor dacă este prima dată, așa că, dacă aveți probleme, asigurați-vă că consultați unul dintre numeroasele tutoriale disponibile pentru utilizarea App Service.
Ar trebui să vedeți o varietate de opțiuni pentru implementare. Puteți folosi orice vă place, dar de dragul simplității, putem folosi opțiunea FTP.
Serviciul App caută un fișier web.config la rădăcina aplicației pentru a ști cum să ruleze aplicația. Deoarece serverul nostru web este o aplicație de consolă esențială, putem publica aplicația și ne putem integra cu serverul IIS folosind HttpPlatformHandler. În Visual Studio, adăugați un fișier XML la proiect și numiți-l web.config. Completați-l cu următorul 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>
Conectați-vă la serverul FTP folosind acreditările obținute de la centrul de implementare (va trebui să faceți clic pe opțiunea FTP). Mutați web.config în folderul wwwroot al site-ului dvs. FTP al serviciului de aplicații.
Acum dorim să construim și să publicăm aplicația noastră, dar înainte de a o face, trebuie să facem o mică modificare a codului serverului. Accesați funcția runServer și modificați primele 3 linii în următoarele:
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])
Ceea ce permite aplicației să se uite la argumentul transmis și să folosească primul argument ca număr de port, mai degrabă decât ca portul să fie codificat la 8080. În configurația web, trecem %HTTP_PLATFORM_PORT% ca prim argument, așa că ar trebui setat.
Creați aplicația în modul de lansare, publicați aplicația și copiați folderul publicat în wwwroot. Reporniți aplicația și ar trebui să vedeți rezultatul API-ului JSON pe site-ul *.azurewebsites.net
.
Acum aplicația noastră este implementată!
Front End
Acum că avem serverul implementat, putem construi un front end. Pentru front-end, vom construi o aplicație Android folosind Xamarin și F#. Această stivă, ca și mediul nostru back-end, se bucură de o integrare profundă cu Visual Studio. Desigur, ecosistemul F# acceptă destul de multe opțiuni de dezvoltare front-end (WebSharper, Fable/Elmish, Xamarin.iOS, DotLiquid etc), dar, de dragul conciziei, vom dezvolta doar folosind Xamarin.Android pentru această postare și vom lăsa le pentru postările viitoare.
Configurare
Pentru a configura aplicația Android, începeți un nou proiect și selectați opțiunea Xamarin Android. Asigurați-vă că aveți instalate instrumente de dezvoltare Android. Odată ce proiectul este configurat, ar trebui să vedeți ceva de genul acesta în fișierul de cod principal.
[<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 )
Acesta este codul de pornire pentru F# Android Xamarin. În prezent, codul ține evidența de câte ori a fost apăsat un buton și afișează valoarea curentă a numărului. Puteți vedea că funcționează apăsând F5 pentru a lansa emulatorul și pornind aplicația în modul de depanare.
Adăugarea componentelor UI
Să adăugăm câteva componente UI și să o facem mai utilă. Deschideți resursa/aspectele și navigați la Main.axml.
Ar trebui să vedeți o reprezentare vizuală a aspectului activității principale. Puteți edita diferitele elemente ale UI făcând clic pe elemente. Puteți adăuga elemente accesând caseta de instrumente și selectând elementul pe care doriți să îl adăugați. Redenumiți butonul și adăugați un textView sub butonul. Reprezentarea XML a AXML-ului dvs. ar trebui să arate similar cu acesta:
<?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 face referire la fișierul de resurse șiruri, așa că deschideți resursele/valorile/strings.xml și faceți următoarele modificări:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="fivePM">It\'s 5PM Somewhere!</string> <string name="app_name">5PM Finder</string> </resources>
Acum am construit AXML front-end. Acum să-l conectăm la un cod. Navigați la MainActivity.fs și faceți următoarele modificări în funcția 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/") )
Înlocuiți fivepm.azurewebsites.net cu adresa URL a propriei dvs. implementări JSON API. Rulați aplicația și faceți clic pe butonul din emulator. În scurt timp, ar trebui să vedeți revenirea API-ului JSON cu rezultatul API-ului dvs.
Se analizează JSON
Suntem aproape acolo! În acest moment, aplicația noastră afișează JSON brut și este destul de ilizibil. Următorul pas, deci, este analizarea JSON și scoaterea unui șir mai citit de om. Pentru a analiza JSON, putem folosi biblioteca Newtonsoft.JSON de pe server.
Navigați la managerul de pachete NuGet și căutați Newtonsoft.JSON. Instalați și reveniți la fișierul MainActivity.fs. Importă-l adăugând „deschide Newtonsoft.Json”.
Acum adăugați tipul TZInfo la proiect. Am putea reutiliza TZInfo de pe server, dar din moment ce avem nevoie de doar de două dintre câmpuri, putem defini un tip personalizat aici:
type TZInfo = { tzName:string localTime: string }
Adăugați definiția tipului deasupra funcției principale și acum modificați funcția principală astfel:
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 )
Acum, rezultatul API-ului JSON este deserializat în obiectul TZInfo și folosit pentru a construi un șir. Rulați aplicația și faceți clic pe butonul. Ar trebui să vedeți șirul formatat iese pe ecran.
În timp ce această aplicație este foarte simplă și poate nerafinată, am construit o aplicație mobilă care consumă și F# JSON API, transformă datele și le afișează utilizatorului. Și am făcut totul în F#. Simțiți-vă liber să vă jucați cu diferitele comenzi și să vedeți dacă puteți îmbunătăți designul.
Și iată-l avem! O aplicație mobilă F# simplă și un API JSON F# și îi spune utilizatorului unde este ora cinci.
Încheierea
Astăzi am parcurs construirea unui API web simplu și a unei aplicații Android simplă folosind doar F#, demonstrând atât expresivitatea limbajului F#, cât și puterea ecosistemului F#. Cu toate acestea, abia am zgâriat suprafața dezvoltării F#, așa că voi mai scrie câteva postări pentru a construi pe ceea ce am discutat astăzi. În plus, sper că această postare v-a inspirat să vă construiți propriile aplicații F#!
Puteți găsi codul pe care l-am folosit pentru acest tutorial pe GitHub.