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。
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 结果。
现在我们的应用程序已部署!
前端
现在我们已经部署了服务器,我们可以构建一个前端。 对于前端,我们将使用 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 上找到我们用于本教程的代码。