F# 教程:如何構建全棧 F# 應用程序

已發表: 2022-03-11

近年來,函數式編程作為一種特別嚴謹和富有成效的範式而享有盛譽。 不僅函數式編程語言在程序員社區中受到關注,而且許多大公司也開始使用函數式編程語言來解決商業問題。

例如,沃爾瑪已經開始使用 Clojure,一種基於 JVM 的函數式 Lisp 方言,用於其結賬基礎設施; Jet.com 是一個大型電子商務平台(現在歸沃爾瑪所有),它使用 F# 構建其大部分微服務; Jane Street 是一家自營交易公司,主要使用 OCaml 來構建他們的算法。

今天,我們將探索 F# 編程。 F# 是一種函數式編程語言,由於其靈活性、強大的 .NET 集成和高質量的可用工具而越來越多地被採用。 出於本 F# 教程的目的,我們將為前端和後端僅使用 F# 構建一個簡單的 Web 服務器和一個相關的移動應用程序。

為什麼選擇 F# 以及 F# 的用途是什麼?

對於今天的項目,我們將只使用 F#。 選擇 F# 作為我們選擇的語言有幾個原因:

  • .NET 集成: F# 與 .NET 世界的其他部分進行了非常緊密的集成,因此可以隨時訪問一個由得到良好支持和完整記錄的庫組成的大型生態系統,以解決廣泛的編程任務。
  • 簡潔性 F# 非常簡潔,因為它具有強大的類型推斷系統和簡潔的語法。 與 C# 或 Java 相比,使用 F# 可以更優雅地解決編程任務。 相比之下,F# 代碼看起來非常精簡。
  • 開發人員工具: F# 享有強大的集成 Visual Studio,這是 .NET 生態系統的最佳 IDE 之一。 對於那些在非 Windows 平台上工作的人來說,Visual Studio 代碼中有大量的插件。 這些工具使 F# 中的編程非常高效。

我可以繼續討論使用 F# 的好處,但事不宜遲,讓我們開始吧!

F# 教程背後的理念

在美國,有一句流行的說法: “某處五點鐘”

在世界的某些地方,下午 5:00 是最早被社會接受的喝一杯或一杯傳統茶的時間。

今天,我們將基於這個概念構建一個應用程序。 我們將構建一個應用程序,它可以在任何給定時間搜索各個時區,找出五點鐘的位置,並將該信息提供給用戶。

後端

設置 Web 服務器

我們將從創建執行時區搜索功能的後端服務開始。 我們將使用 Suave.IO 構建 JSON API。

F# 教程圖解:Web 服務器設置

Suave.IO 是一個直觀的 web 框架,帶有一個輕量級的 web 服務器,可以非常快速地編寫簡單的 web 應用程序。

首先,轉到 Visual Studio 並啟動一個新的 F# 控制台應用程序項目。 如果您無法使用此選項,您可能需要使用 Visual Studio 安裝程序安裝 F# 功能。 將項目命名為“FivePM”。 創建應用程序後,您應該會看到如下內容:

 [<EntryPoint>] let main argv = printfn "%A" argv 0 // return an integer exit code

這是一段非常簡單的啟動代碼,它打印出參數並以狀態碼 0 退出。隨意更改打印語句並嘗試代碼的各種功能。 “%A”格式化程序是一種特殊的格式化程序,可以打印您傳入的任何類型的字符串表示形式,因此可以隨意打印整數、浮點數甚至複雜類型。 一旦您對基本語法感到滿意,就可以安裝 Suave。

安裝 Suave 的最簡單方法是通過 NuGet 包管理器。 轉到項目 -> 管理 NuGet 包,然後單擊瀏覽選項卡。 搜索 Suave 並單擊安裝。 一旦您接受要安裝的軟件包,您應該一切就緒! 現在回到您的 program.fs 屏幕,我們已準備好開始構建服務器。

要開始使用 Suave,我們需要先導入包。 在程序的頂部,鍵入以下語句:

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

這將導入構建基本 Web 服務器所需的基本包。 現在將 main 中的代碼替換為以下代碼,它定義了一個簡單的應用程序並在端口 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

代碼應該看起來非常簡單,即使您不熟悉 F# 語法或 Suave 定義路由處理程序的方式,代碼也應該具有相當的可讀性。 本質上,Web 應用程序返回 200 狀態和“Hello World!” 在“/”路由上被 GET 請求命中時的字符串。 繼續運行應用程序(在 Visual Studio 中按 F5)並導航到 localhost:8080,您應該會看到“Hello World!” 在您的瀏覽器窗口中。

重構服務器代碼

現在我們有了一個網絡服務器! 不幸的是,它並沒有做很多事情——所以讓我們給它一些功能吧! 首先,讓我們將 Web 服務器功能移到其他地方,以便我們構建一些功能而不用擔心 Web 服務器(稍後我們將把它連接到 Web 服務器)。 因此定義一個單獨的函數:

 // 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

現在將 main 函數更改為以下內容,並確保我們做對了。

 [<EntryPoint>] let main argv = runWebServer argv 0

按 F5,然後我們的“Hello World!” 服務器應該像以前一樣運行。

獲取時區

現在讓我們構建確定五點鐘所在時區的功能。 我們想編寫一些代碼來遍歷所有時區並確定最接近下午 5:00 的時區。

獲取時區插圖

此外,我們真的不想返回非常接近下午 5:00 但稍早(例如下午 4:58)的時區,因為出於本演示的目的,前提是它不能早於 5 :00 pm,但很近。

讓我們從獲取時區列表開始。 在 F# 中,這非常容易,因為它與 C# 集成得非常好。 在頂部添加“open System”,並將您的 F# 應用程序更改為:

 [<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

運行應用程序,您應該會在控制台中看到所有時區、它們的偏移量和它們的顯示名稱的列表。

創建和使用自定義類型

現在我們有了時區列表,我們可以將它們轉換為對我們更有用的自定義數據類型,其中包含諸如 UTC 偏移量、本地時間、距本地時間下午 5:00 的距離等信息等。為此,讓我們在 main 函數的正上方定義一個自定義類型:

 type TZInfo = {tzName: string; minDiff: float; localTime: string; utcOffset: float}

現在我們可以將我們從上一步獲得的時區信息轉換成這個 TZInfo 對象的列表。 因此改變你的主要功能:

 [<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

您應該會看到日期線標準時間的 tzInfo 對像打印到您的屏幕上。

排序、過濾和管道,哦,我的!

現在我們有了這些 tzInfo 對象的列表,我們可以對這些對象進行過濾和排序,以找到 1) 下午 5:00 之後和 2) 1) 中最接近下午 5:00 的時區的時區。 像這樣更改您的主要功能:

 [<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

現在我們應該有我們正在尋找的時區。

將時區 Getter 重構為自己的函數

現在讓我們將代碼重構為它自己的函數,以便我們以後可以使用它。 因此定義一個函數:

 // 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

運行代碼,您應該會看到與以前相同的輸出。

JSON 編碼返回數據

現在我們可以獲得時區數據,我們可以將信息轉換為 JSON 並通過我們的應用程序提供它。 這非常簡單,這要歸功於 NewtonSoft 的 JSON.NET 包。 返回 NuGet 包管理器並找到 Newtonsoft.Json,然後安裝包。 現在返回 Program.fs 並對我們的 main 函數做一個小改動:

 [<EntryPoint>] let main argv = printfn "%s" <| JsonConvert.SerializeObject(getClosest()) 0

現在運行代碼,而不是 TZInfo 對象,您應該會看到 JSON 打印到您的控制台。

將時區信息連接到 JSON API

將它連接到我們的 JSON API 非常簡單。 只需對 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

運行應用程序,並導航到 localhost:8080。 您應該在瀏覽器窗口中看到 JSON。

部署服務器

現在我們有了 JSON API 服務器,我們可以部署它,以便在 Internet 上訪問它。 部署此應用程序的最簡單方法之一是通過 Microsoft Azure 的 App Service,它可以理解為託管的 IIS 服務。 若要部署到 Azure 應用服務,請轉到 https://portal.azure.com 並轉到應用服務。 創建一個新應用程序,然後導航到門戶中的部署中心。 如果您是第一次使用該門戶,可能會有點不知所措,因此,如果您遇到問題,請務必參考其中一個使用應用服務的眾多教程。

您應該會看到各種部署選項。 您可以使用任何您喜歡的方式,但為了簡單起見,我們可以使用 FTP 選項。

App 服務在應用程序的根目錄中查找 web.config 文件以了解如何運行應用程序。 由於我們的 Web 服務器是必不可少的控制台應用程序,因此我們可以使用 HttpPlatformHandler 發布應用程序並與 IIS 服務器集成。 在 Visual Studio 中,將 XML 文件添加到您的項目並將其命名為 web.config。 用以下 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>

使用從部署中心獲得的憑據連接到 FTP 服務器(您需要單擊 FTP 選項)。 將 web.config 移動到應用服務 FTP 站點的 wwwroot 文件夾。

現在我們要構建和發布我們的應用程序,但在此之前,我們需要對服務器代碼進行一些小改動。 轉到您的 runServer 函數並將前 3 行更改為以下內容:

 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])

這允許應用程序查看傳入的參數並將第一個參數用作端口號,而不是將端口硬編碼為 8080。在 web 配置中,我們將 %HTTP_PLATFORM_PORT% 作為第一個參數傳遞,所以我們應該設置。

以發布模式構建應用程序,發布應用程序,並將發布的文件夾複製到wwwroot。 重新啟動應用程序,您應該會在*.azurewebsites.net站點上看到 JSON API 結果。

現在我們的應用程序已部署!

前端

F#前端圖解

現在我們已經部署了服務器,我們可以構建一個前端。 對於前端,我們將使用 Xamarin 和 F# 構建一個 Android 應用程序。 與我們的後端環境一樣,此堆棧與 Visual Studio 深度集成。 當然,F# 生態系統支持相當多的前端開發選項(WebSharper、Fable/Elmish、Xamarin.iOS、DotLiquid 等),但為了簡潔起見,我們將在本文中僅使用 Xamarin.Android 進行開發並離開他們為以後的職位。

配置

要設置 Android 應用程序,請啟動一個新項目並選擇 Xamarin Android 選項。 確保您已安裝 Android 開發工具。 設置項目後,您應該在主代碼文件中看到類似的內容。

 [<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 )

這是 F# Android Xamarin 的入門代碼。 該代碼當前僅跟踪按鈕被單擊的次數並顯示當前計數值。 您可以通過按 F5 啟動模擬器並在調試模式下啟動應用程序來查看它的工作原理。

添加 UI 組件

讓我們添加一些 UI 組件並使其更有用。 打開資源/佈局並導航到 Main.axml。

您應該看到主要活動佈局的可視化表示。 您可以通過單擊元素來編輯各種 UI 元素。 您可以通過轉到工具箱並選擇要添加的元素來添加元素。 重命名按鈕並在按鈕下方添加一個 textView。 您的 AXML 的 XML 表示應該類似於以下內容:

 <?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 引用字符串資源文件,因此打開您的 resources/values/strings.xml 並進行以下更改:

 <?xml version="1.0" encoding="utf-8"?> <resources> <string name="fivePM">It\'s 5PM Somewhere!</string> <string name="app_name">5PM Finder</string> </resources>

現在我們已經構建了前端 AXML。 現在讓我們將它連接到一些代碼。 導航到 MainActivity.fs 並對 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/") )

將 Fivepm.azurewebsites.net 替換為您自己的 JSON API 部署的 URL。 運行應用程序並單擊模擬器中的按鈕。 稍後,您應該會看到 JSON API 與您的 API 結果一起返回。

解析 JSON

我們快要到了! 現在,我們的應用程序正在顯示原始 JSON,而且相當不可讀。 然後,下一步是解析 JSON 並輸出更易讀的字符串。 要解析 JSON,我們可以使用服務器中的 Newtonsoft.JSON 庫。

導航到 NuGet 包管理器並蒐索 Newtonsoft.JSON。 安裝並返回 MainActivity.fs 文件。 通過添加“open Newtonsoft.Json”來導入它。

現在將 TZInfo 類型添加到項目中。 我們可以重用來自服務器的 TZInfo,但由於我們實際上只需要兩個字段,我們可以在這裡定義一個自定義類型:

 type TZInfo = { tzName:string localTime: string }

在 main 函數上方添加類型定義,現在更改 main 函數:

 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 )

現在 JSON API 結果被反序列化為 TZInfo 對象並用於構造字符串。 運行應用程序並單擊按鈕。 您應該會看到格式化的字符串彈出到屏幕上。

雖然這個應用程序非常簡單,可能還不夠完善,但我們已經構建了一個使用 F# JSON API、轉換數據並將其顯示給用戶的移動應用程序。 我們在 F# 中完成了這一切。 隨意玩弄各種控件,看看是否可以改進設計。

我們終於得到它了! 一個簡單的 F# 移動應用程序和一個 F# JSON API,並告訴用戶五點鐘在哪裡。

包起來

今天我們介紹了僅使用 F# 構建一個簡單的 Web API 和一個簡單的 Android 應用程序,展示了 F# 語言的表現力和 F# 生態系統的力量。 然而,我們幾乎沒有觸及 F# 開發的皮毛,所以我將在我們今天討論的內容的基礎上再寫幾篇文章。 此外,我希望這篇文章能啟發您構建自己的 F# 應用程序!

您可以在 GitHub 上找到我們用於本教程的代碼。