F#チュートリアル:フルスタックF#アプリを構築する方法

公開: 2022-03-11

近年、関数型プログラミングは、特に厳密で生産的なパラダイムとしての評判を得ています。 関数型プログラミング言語がプログラマーコミュニティ内で注目を集めているだけでなく、多くの大企業も関数型プログラミング言語を使用して商業上の問題を解決し始めています。

たとえば、ウォルマートは、JVMベースの機能的なLisp方言であるClojureをチェックアウトインフラストラクチャに使用し始めました。 大規模なeコマースプラットフォーム(現在はWalmartが所有)であるJet.comは、F#を使用してほとんどのマイクロサービスを構築しています。 自己勘定取引会社のジェーンストリートは、主にOCamlを使用してアルゴリズムを構築しています。

今日は、F#プログラミングについて説明します。 F#は、その柔軟性、強力な.NET統合、および利用可能なツールの高品質により、採用が増えている関数型プログラミング言語の1つです。 このF#チュートリアルでは、フロントエンドとバックエンドの両方にF#のみを使用して、単純なWebサーバーと関連するモバイルアプリを構築します。

F#を選択する理由とF#の用途

今日のプロジェクトでは、F#のみを使用します。 選択する言語としてF#を選択する理由はいくつかあります。

  • .NET統合: F#は、他の.NETの世界と非常に緊密に統合されているため、幅広いプログラミングタスクを解決するために、十分にサポートされ、完全に文書化されたライブラリの大規模なエコシステムにすぐにアクセスできます。
  • 簡潔さ: F#は、その強力な型推論システムと簡潔な構文により、非常に簡潔です。 プログラミングタスクは、多くの場合、C#やJavaよりもF#を使用してはるかにエレガントに解決できます。 F#コードは、比較すると非常に合理化されているように見えます。
  • 開発者ツール: F#は、.NETエコシステムに最適なIDEの1つであるVisualStudioとの強力な統合を楽しんでいます。 Windows以外のプラットフォームで作業している場合は、VisualStudioCodeに豊富なプラグインがあります。 これらのツールは、F#でのプログラミングを非常に生産的にします。

F#を使用する利点については続けることができますが、それ以上の苦労はせずに、詳しく見ていきましょう。

F#チュートリアルの背後にあるアイデア

アメリカでは「どこか5時だ」ということわざがあります。

世界の一部の地域では、午後5時が、飲み物や伝統的なお茶を飲むことが社会的に受け入れられる最も早い時間です。

今日は、このコンセプトに基づいてアプリケーションを構築します。 いつでもさまざまなタイムゾーンを検索し、5時の位置を見つけて、その情報をユーザーに提供するアプリケーションを構築します。

バックエンド

Webサーバーのセットアップ

まず、タイムゾーン検索機能を実行するバックエンドサービスを作成します。 Suave.IOを使用してJSONAPIを構築します。

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で終了する非常に単純なスターターコードです。printステートメントを自由に変更して、コードのさまざまな機能を試してみてください。 「%A」フォーマッタは、渡した型の文字列表現を出力する特別なフォーマッタであるため、整数、浮動小数点数、または複合型を自由に出力できます。 基本的な構文に慣れたら、Suaveをインストールします。

Suaveをインストールする最も簡単な方法は、NuGetパッケージマネージャーを使用することです。 [プロジェクト]->[NuGetパッケージの管理]に移動し、[参照]タブをクリックします。 Suaveを検索し、[インストール]をクリックします。 インストールするパッケージを受け入れると、すべての設定が完了します。 これでprogram.fs画面に戻り、サーバーの構築を開始する準備が整いました。

Suaveの使用を開始するには、最初にパッケージをインポートする必要があります。 プログラムの上部に、次のステートメントを入力します。

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

これにより、基本的なWebサーバーの構築に必要な基本的なパッケージがインポートされます。 次に、メインのコードを次のコードに置き換えます。これは、単純なアプリを定義し、ポート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ステータスと「HelloWorld!」で返されます。 「/」ルートでGETリクエストがヒットしたときの文字列。 先に進み、アプリケーション(Visual StudioではF5)を実行し、localhost:8080に移動すると、「HelloWorld!」と表示されます。 ブラウザウィンドウで。

サーバーコードのリファクタリング

これでWebサーバーができました! 残念ながら、それはそれほど多くのことをしません-それでそれにいくつかの機能を与えましょう! まず、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を押すと、「HelloWorld!」 サーバーは以前と同じように機能するはずです。

タイムゾーンの取得

それでは、5時のタイムゾーンを決定する機能を構築しましょう。 すべてのタイムゾーンを反復処理し、午後5時に最も近いタイムゾーンを決定するコードを記述します。

タイムゾーンの取得の図

さらに、午後5時に非常に近いが、少し前(たとえば、午後4時58分)のタイムゾーンを返すことは実際には望んでいません。このデモンストレーションの目的では、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時からの距離などの情報が含まれます。そのために、メイン関数のすぐ上にカスタムタイプを定義しましょう。

 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

また、画面に印刷されたDatelineStandard時間のtzInfoオブジェクトが表示されます。

並べ替えとフィルタリングと配管、オーマイ!

これらのtzInfoオブジェクトのリストができたので、これらのオブジェクトをフィルタリングおよびソートして、1)午後5時以降および2)1)のタイムゾーンの午後5時に最も近いタイムゾーンを見つけることができます。 次のようにメイン関数を変更します。

 [<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がコンソールに出力されます。

タイムゾーン情報をJSONAPIに接続する

これをJSONAPIに接続するのは非常に簡単です。 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サーバーができたので、インターネット上でアクセスできるようにデプロイできます。 このアプリケーションを展開する最も簡単な方法の1つは、マネージドIISサービスとして理解できるMicrosoftAzureのAppServiceを使用することです。 Azure App Serviceにデプロイするには、https://portal.azure.comにアクセスして、AppServiceにアクセスします。 新しいアプリを作成し、ポータルのデプロイメントセンターに移動します。 ポータルは、初めての場合は少し圧倒される可能性があるため、問題が発生した場合は、AppServiceを使用するための多くのチュートリアルの1つを参照してください。

展開のためのさまざまなオプションが表示されます。 好きなものを使用できますが、簡単にするために、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サイトにJSONAPIの結果が表示されます。

これで、アプリケーションがデプロイされました。

フロントエンド

F#フロントエンドの図

サーバーがデプロイされたので、フロントエンドを構築できます。 フロントエンドでは、XamarinとF#を使用してAndroidアプリケーションを構築します。 このスタックは、バックエンド環境と同様に、VisualStudioとの緊密な統合を楽しんでいます。 もちろん、F#エコシステムはかなりの数のフロントエンド開発オプション(WebSharper、Fable / Elmish、Xamarin.iOS、DotLiquidなど)をサポートしていますが、簡潔にするために、この投稿ではXamarin.Androidのみを使用して開発します。将来の投稿のためにそれら。

セットアップ

Androidアプリをセットアップするには、新しいプロジェクトを開始し、XamarinAndroidオプションを選択します。 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#AndroidXamarinのスターターコードです。 コードは現在、ボタンがクリックされた回数を追跡し、現在のカウント値を表示します。 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を独自のJSONAPIデプロイメントのURLに置き換えます。 アプリケーションを実行し、エミュレーターのボタンをクリックします。 少しすると、JSONAPIがAPI結果とともに返されるのがわかります。

JSONの解析

もうすぐ着きます! 現在、アプリは生のJSONを表示しており、かなり判読できません。 次のステップは、JSONを解析し、より人間が読める文字列を出力することです。 JSONを解析するには、サーバーのNewtonsoft.JSONライブラリを使用できます。

NuGetパッケージマネージャーに移動し、Newtonsoft.JSONを検索します。 インストールして、MainActivity.fsファイルに戻ります。 「openNewtonsoft.Json」を追加してインポートします。

次に、TZInfoタイプをプロジェクトに追加します。 サーバーからTZInfoを再利用することもできますが、実際には2つのフィールドしか必要ないため、ここでカスタムタイプを定義できます。

 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で、5時の位置をユーザーに通知します。

まとめ

今日は、F#のみを使用して単純なWeb APIと単純なAndroidアプリケーションを構築する手順を説明し、F#言語の表現力とF#エコシステムの強さの両方を示しました。 ただし、F#開発の表面をかじっただけなので、今日説明した内容に基づいて、さらにいくつかの投稿を作成します。 さらに、この投稿が、独自のF#アプリケーションを構築するきっかけになったと思います。

このチュートリアルで使用したコードはGitHubにあります。