个性化阅读
专注于IT技术分析

F#教程:如何构建完整的F#应用程序

点击下载

本文概述

近年来, 函数式编程作为一种特别严格且富有成效的范例而享有盛誉。函数式编程语言不仅在程序员界引起了关注, 而且许多大公司也开始使用函数式编程语言来解决商业问题。

例如, 沃尔玛已经开始将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是最早在社交上可以喝一杯或喝杯传统茶的时间。

今天, 我们将基于此概念构建一个应用程序。我们将构建一个应用程序, 该应用程序可以在任何给定的时间搜索各个时区, 找出现在的5点钟, 然后将该信息提供给用户。

后端

设置Web服务器

我们将从制作执行时区搜索功能的后端服务开始。我们将使用Suave.IO来构建JSON API。

F#教程插图:Web服务器设置

Suave.IO是带有轻量级Web服务器的直观Web框架, 该服务器允许快速地对简单的Web应用程序进行编码。

首先, 请转到Visual Studio并启动一个新的F#控制台应用程序项目。如果你无法使用此选项, 则可能需要使用Visual Studio Installer安装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服务器!不幸的是, 它并不能起到很多作用-因此, 让我们为它提供一些功能!首先, 让我们将网络服务器功能移到其他位置, 以便我们构建某些功能而不必担心网络服务器(稍后我们将其连接到网络服务器)。因此定义一个单独的函数:

// 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 pm的时区。

提取时区图

此外, 我们并不是真的想返回一个非常接近5:00 pm但稍早于(例如4:58 pm)的时区, 因为在此演示中, 前提是不能在5之前:00 pm, 但关闭。

首先, 获取时区列表。在F#中, 这非常容易, 因为它与C#集成得很好。在顶部添加”开放系统”, 然后将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到多远的信息。等等。为此, 让我们在主函数上方定义一个自定义类型:

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

并且你应该会在屏幕上看到Dateline Standard时间的tzInfo对象。

排序, 过滤和管道, 我的天哪!

现在我们有了这些tzInfo对象的列表, 我们可以对这些对象进行过滤和排序, 以找到1)5:00 pm之后的时区和2)最接近1)中时区5:00 pm的时区。像这样更改你的主要功能:

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

现在, 我们应该有了所需的时区。

将时区获取器重构为其自己的功能

现在, 让我们将代码重构为自己的功能, 以便以后使用。因此定义一个函数:

// 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并对我们的主要功能进行一些更改:

[<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的应用程序服务, 该服务可以理解为托管IIS服务。若要部署到Azure App服务, 请转到https://portal.azure.com并转到App Service。创建一个新应用, 然后导航到门户中的部署中心。如果你是第一次访问该门户网站, 可能会有些不知所措, 因此, 如果你有困难, 请务必查阅其中的许多教程之一以使用App Service。

你应该看到各种各样的部署选项。你可以使用任何你喜欢的东西, 但是为了简单起见, 我们可以使用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函数, 并将前三行更改为以下内容:

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:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical"

    android:layout_width="match_parent"

    android:layout_height="match_parent">

  <Button android:id="@+id/myButton"

    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:id="@+id/textView1" />

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

        )

用你自己的JSON API部署的URL替换fourpm.azurewebsites.net。运行该应用程序, 然后单击仿真器中的按钮。稍后, 你应该会看到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上找到我们用于本教程的代码。我还构建了一个带有Web前端和一些其他功能的版本, 你可以在https://fivepm.azurewebsites.net上找到这些版本。

赞(0)
未经允许不得转载:srcmini » F#教程:如何构建完整的F#应用程序

评论 抢沙发

评论前必须登录!