Учебное пособие по F#. Как создать полнофункциональное приложение F#
Опубликовано: 2022-03-11В последние годы функциональное программирование приобрело репутацию особенно строгой и продуктивной парадигмы. Функциональные языки программирования привлекают внимание не только в сообществах программистов, но и во многих крупных компаниях, которые начинают использовать функциональные языки программирования для решения коммерческих задач.
Например, Walmart начал использовать Clojure, функциональный диалект Lisp на основе JVM, для своей инфраструктуры проверки; Jet.com, крупная платформа электронной коммерции (теперь принадлежащая Walmart), использует F# для создания большинства своих микросервисов; и Jane Street, частная торговая фирма, в основном используют OCaml для построения своих алгоритмов.
Сегодня мы изучим программирование на F#. F# — один из языков функционального программирования, популярность которого растет благодаря его гибкости, надежной интеграции с .NET и высокому качеству доступных инструментов. Для целей этого руководства по F# мы создадим простой веб-сервер и связанное с ним мобильное приложение, используя только F# как для внешнего, так и для внутреннего интерфейса.
Почему стоит выбрать F# и для чего используется F#?
В сегодняшнем проекте мы будем использовать только F#. Есть несколько причин, по которым мы предпочитаем F#:
- Интеграция с .NET: F# имеет очень тесную интеграцию с остальным миром .NET и, следовательно, свободный доступ к большой экосистеме хорошо поддерживаемых и тщательно документированных библиотек для решения широкого круга задач программирования.
- Краткость: F# чрезвычайно лаконичен благодаря мощной системе вывода типов и лаконичному синтаксису. Задачи программирования часто могут быть решены гораздо более элегантно с использованием F#, чем C# или Java. По сравнению с ним код F# может показаться очень оптимизированным.
- Инструменты разработчика: F# имеет тесную интеграцию с Visual Studio, которая является одной из лучших IDE для экосистемы .NET. Для тех, кто работает на платформах, отличных от Windows, в коде Visual Studio есть множество плагинов. Эти инструменты делают программирование на F# чрезвычайно продуктивным.
Я мог бы продолжать рассказывать о преимуществах использования F#, но без лишних слов, давайте углубимся!
Идея нашего руководства по F#
В США есть популярная поговорка: «Где-то пять часов» .
В некоторых частях мира 17:00 — самое раннее время, когда социально приемлемо выпить или выпить традиционную чашку чая.
Сегодня мы создадим приложение, основанное на этой концепции. Мы создадим приложение, которое в любой момент времени выполняет поиск в различных часовых поясах, выясняет, где сейчас пять часов, и предоставляет эту информацию пользователю.
Бэкэнд
Настройка веб-сервера
Мы начнем с создания серверной службы, которая выполняет функцию поиска часового пояса. Мы будем использовать Suave.IO для создания JSON API.
Suave.IO — это интуитивно понятная веб-инфраструктура с облегченным веб-сервером, которая позволяет очень быстро кодировать простые веб-приложения.
Для начала перейдите в Visual Studio и запустите новый проект консольного приложения F#. Если этот параметр недоступен для вас, вам может потребоваться установить функции F# с помощью установщика Visual Studio. Назовите проект «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
Это позволит импортировать основные пакеты, необходимые для создания базового веб-сервера. Теперь замените код в 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 для определения обработчиков маршрутов, код должен быть довольно читабельным. По сути, веб-приложение возвращается со статусом 200 и «Hello World!» строка, когда она попадает с запросом GET на маршруте «/». Запустите приложение (F5 в Visual Studio) и перейдите на localhost:8080, и вы должны увидеть «Hello World!» в окне вашего браузера.
Рефакторинг кода сервера
Теперь у нас есть веб-сервер! К сожалению, он мало что делает, так что давайте добавим ему немного функциональности! Во-первых, давайте переместим функциональность веб-сервера в другое место, чтобы создать некоторые функции, не беспокоясь о веб-сервере (позже мы подключим его к веб-серверу). Определите отдельную функцию таким образом:
// 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
Теперь измените основную функцию на следующую и убедитесь, что мы сделали это правильно.
[<EntryPoint>] let main argv = runWebServer argv 0
Нажмите F5, и наш «Hello World!» сервер должен работать как прежде.
Получение часовых поясов
Теперь давайте создадим функциональность, определяющую часовой пояс, в котором сейчас пять часов. Мы хотим написать некоторый код для перебора всех часовых поясов и определения часового пояса, который ближе всего к 17:00.
Кроме того, мы не хотим возвращать часовой пояс, который очень близок к 17:00, но немного раньше (например, 16:58), потому что для целей этой демонстрации предполагается, что он не может быть раньше 5:00. :00 вечера, однако близко.
Начнем с получения списка часовых поясов. В 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, местное время, как далеко от 17: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
И вы должны увидеть объект tzInfo для стандартного времени Dateline, напечатанный на вашем экране.
Сортировка, фильтрация и конвейерная обработка, о боже!
Теперь, когда у нас есть список этих объектов tzInfo, мы можем отфильтровать и отсортировать эти объекты, чтобы найти часовой пояс, в котором он находится 1) после 17:00 и 2) ближайший к 17:00 из часовых поясов в 1). Измените свою основную функцию следующим образом:
[<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 и передать ее через наше приложение. Это довольно просто благодаря пакету JSON.NET от NewtonSoft. Вернитесь к диспетчеру пакетов 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, мы можем развернуть его, чтобы он был доступен в Интернете. Один из самых простых способов развернуть это приложение — через службу приложений Microsoft Azure, которую можно понимать как управляемую службу IIS. Чтобы выполнить развертывание в службе приложений Azure, перейдите на https://portal.azure.com и перейдите в Службу приложений. Создайте новое приложение и перейдите в центр развертывания на своем портале. Портал может быть немного сложным, если вы впервые пользуетесь им, поэтому, если у вас возникнут проблемы, обязательно ознакомьтесь с одним из многочисленных руководств по использованию Службы приложений.
Вы должны увидеть различные варианты развертывания. Вы можете использовать любой, который вам нравится, но для простоты мы можем использовать опцию FTP.
Служба приложений ищет файл web.config в корне вашего приложения, чтобы узнать, как запустить ваше приложение. Поскольку наш веб-сервер является важным консольным приложением, мы можем опубликовать приложение и интегрировать его с сервером IIS с помощью HttpPlatformHandler. В 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 в папку wwwroot вашего FTP-сайта службы приложений.
Теперь мы хотим собрать и опубликовать наше приложение, но перед этим нам нужно внести небольшое изменение в код сервера. Перейдите к функции 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. В веб-конфигурации мы передаем %HTTP_PLATFORM_PORT% в качестве первого аргумента, поэтому мы должен быть установлен.
Соберите приложение в режиме выпуска, опубликуйте приложение и скопируйте опубликованную папку в wwwroot. Перезапустите приложение, и вы должны увидеть результат JSON API на сайте *.azurewebsites.net
.
Теперь наше приложение развернуто!
Передняя часть
Теперь, когда у нас развернут сервер, мы можем создать внешний интерфейс. Для внешнего интерфейса мы создадим приложение для Android с использованием Xamarin и F#. Этот стек, как и наша внутренняя среда, тесно интегрирован с 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, чтобы запустить эмулятор и запустить приложение в режиме отладки.
Добавление компонентов пользовательского интерфейса
Давайте добавим некоторые компоненты пользовательского интерфейса и сделаем его более полезным. Откройте ресурс/макеты и перейдите к файлу Main.axml.
Вы должны увидеть визуальное представление макета основного действия. Вы можете редактировать различные элементы пользовательского интерфейса, нажимая на элементы. Вы можете добавить элементы, перейдя на панель инструментов и выбрав элемент, который хотите добавить. Переименуйте кнопку и добавьте textView ниже кнопки. XML-представление вашего AXML должно выглядеть примерно так:
<?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 URL-адресом собственного развертывания JSON API. Запустите приложение и нажмите на кнопку в эмуляторе. Через некоторое время вы должны увидеть возврат 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 }
Добавьте определение типа над основной функцией, а теперь измените основную функцию следующим образом:
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# и API F# JSON, сообщающее пользователю, где сейчас пять часов.
Подведение итогов
Сегодня мы рассмотрели создание простого веб-API и простого приложения для Android с использованием только F#, продемонстрировав как выразительность языка F#, так и мощь экосистемы F#. Тем не менее, мы едва коснулись темы разработки F#, поэтому я напишу еще пару сообщений, чтобы развить то, что мы сегодня обсудили. Кроме того, я надеюсь, что эта статья вдохновила вас на создание собственных приложений F#!
Вы можете найти код, который мы использовали для этого руководства, на GitHub.