2019 年 9 月
Volume 34 Number 9
[F#]
.NET Core の F# ですべて思いのまま
著者: Phillip Carter
F# は、.NET 向けの関数型プログラミング言語です (bit.ly/2y4PeQG)。この言語はクロスプラットフォームで、すべての .NET と同様、オープン ソースです (github.com/dotnet/fsharp)。F# を使用すると .NET で関数型プログラミングが可能になるため、Microsoft には F# の熱心なファンがいます。この概念に慣れていない人のために説明すると、関数型プログラミングの理論の枠組みで強調されるのは、以下のような手法です。
- データの操作に使用される主要なコンストラクトは関数
- ステートメントの代わりに式を使用
- 変数の値は不変
- 命令型プログラミングではなく宣言型プログラミング
これらの特徴によって、F# は .NET に以下のようなすばらしい機能をもたらします。
- ファーストクラスの関数 (値として渡したり、他の関数から返したりすることができます)
- ステートメントではなく式と値を重視する軽量型構文
- 組み込みの不変性と null 以外の型
- 豊富なデータ型と高度なパターン マッチングの手法
図 1 に一般的な F# のコード例を示します。
図 1 一般的な F# のコード
// Group data with Records
type SuccessfulWithdrawal = {
Amount: decimal
Balance: decimal
}
type FailedWithdrawal = {
Amount: decimal
Balance: decimal
IsOverdraft: bool
}
// Use discriminated unions to represent data of 1 or more forms
type WithdrawalResult =
| Success of SuccessfulWithdrawal
| InsufficientFunds of FailedWithdrawal
| CardExpired of System.DateTime
| UndisclosedFailure
let handleWithdrawal amount =
// Define an inner function to hide it from callers
// Returns a WithdrawalResult
let withdrawMoney amount =
callDatabaseWith amount // Let's assume this has been implemented :)
let withdrawalResult = withdrawMoney amount
// The F# compiler enforces accounting for each case!
match w with
| Success s -> printfn "Successfully withdrew %f" s.Amount
| InsufficientFunds f -> printfn "Failed: balance is %f" f.Balance
| CardExpired d -> printfn "Failed: card expired on %O" d
| UndisclosedFailure -> printfn "Failed: unknown :("
上記のようなコア機能に加えて、F# では .NET 全体との相互運用性が確保されており、オブジェクトやインターフェイスなどが完全にサポートされています。より高度な F# のプログラミング手法では、オブジェクト指向 (OO) の機能と関数型コードを巧妙に組み合わせることが少なくありませんが、その場合でも、関数型プログラミングのパラダイムは犠牲になりません。
また、F# には、コンピュテーション式、測定単位、レコードや判別共用体などの強力な型など、ユーザーが使いやすく特別なさまざまな機能があります。詳細については、bit.ly/2JSnipy で F# の言語リファレンスを参照してください。さらに、F# では安全性と正確性を重視するプログラミング スタイルが推奨されます。多くの F# 開発者は、安全性と正確性がそれほど重視されていない他の言語でかなりの経験を積んだ後に、F# を使うようになりました。この言語は、非同期、タプル、パターン マッチング、実装間近の null 許容参照型の機能セットなど、C# に最近取り入れられた数多くの機能に影響を与えています。
また、F# には活気のあるコミュニティがあり、そのメンバーは .NET の限界を越えて優れたオープン ソースのコンポーネントを作り出すことに情熱を持っています。このコミュニティは、.NET にとってきわめて革新的で大きな価値があるもので、各種の UI ライブラリ、データ処理ライブラリ、テスト手法、Web サービスなどを .NET 向けに率先して開発してきました。
豊富な機能と、熱心な開発者による活気のあるコミュニティのおかげで、多くの人が楽しみと仕事の両方で F# を使うようになっています。F# は、世界中の金融機関、e コマース システム、科学計算、データ サイエンスや機械学習に取り組んでいる組織、コンサルタント会社などで活用されています。また、Microsoft でも幅広く使われています。F# は Microsoft Research で使用される主な言語の 1 つとして、Q# 開発プラットフォーム、Azure と Office 365 のパーツ、さらには F# コンパイラや Visual Studio の F# 用ツールを支えています。つまり、あらゆる場所で活用されているのです。
興味がわきましたか? ありがとうございます。この記事では、.NET Core 上で F# を使っていくつかの優れた機能を実現する方法について説明します。
概要
.NET Core で F# を使用するための基本から始めて、.NET CLI から OS 上にコンソール アプリケーションやライブラリ プロジェクトを作成する方法など、さらに興味深い高度な機能について段階的に説明していきます。このツールセット自体はシンプルで必要最小限のものですが、Visual Studio Code や公式の F# プラグインである Ionide (ionide.io) などのエディターと組み合わせてアプリケーション全体を開発するのに十分な機能を備えています。好みに応じて Vim や Emacs を使うこともできます。
次に、Web サービスを構築するために必要ないくつかのテクノロジの概要を簡単に説明します。それぞれのテクノロジには注意すべきトレードオフがあります。その後、これらのテクノロジの 1 つを使用した例を示します。
最後に、Span<'T> など、.NET Core の高パフォーマンス コンストラクトのいくつかを利用してアロケーションを減らし、使用しているシステム内のホット パスを実際に高速化する方法について簡単に説明します。
.NET CLI から F# を使い始める
.NET CLI を使用すると、F# を簡単に使い始めることができます。まず、最新の .NET SDK がインストールされていることを確認します。
次に、相互に接続されているいくつかのプロジェクトを作成してみましょう。モダンなターミナルではタブ補完がサポートされているため、少し入力するだけで、使用している環境で多くのコマンドを補完できるはずです。最初に、新しいソリューションを作成します。
dotnet new sln -o FSNetCore && cd FSNetCore
これにより、FSNetCore という名前の新しいディレクトリが作成されて、そのディレクトリにソリューション ファイルが作成され、FSNetCore ディレクトリに移動します。
次に、ライブラリ プロジェクトを作成してソリューション ファイルにフックします。
dotnet new lib -lang F# -o src/Library
dotnet sln add src/Library/Library.fsproj
そのライブラリにパッケージを追加します。
dotnet add src/Library/Library.fsproj package Newtonsoft.Json
これにより、Json.NET パッケージがプロジェクトに追加されます。
ライブラリ プロジェクト内の Library.fs ファイルを以下のように変更します。
module Library
open Newtonsoft.Json
let getJsonNetJson value =
sprintf "I used to be %s but now I'm %s thanks to JSON.NET!"
value (JsonConvert.SerializeObject(value))
これは 1 つの F# 関数を含むモジュールです。この関数では、JSON.NET を使用して、ジェネリック値を残りのメッセージと共に JSON 文字列にシリアル化します。
次に、このライブラリを使用するコンソール アプリを作成します。
dotnet new console -lang F# -o src/App
dotnet add src/App/App.fsproj reference src/Library/Library.fsproj
dotnet sln add src/App/App.fsproj
コンソール アプリ プロジェクト内の Program.fs の内容を以下の内容に置き換えます。
open System
[<EntryPoint>]
let main argv =
printfn "Nice command-line arguments! Here's what JSON.NET has to say about them:"
argv
|> Array.map Library.getJsonNetJson
|> Array.iter (printfn "%s")
0 // Return an integer exit code
これにより、各コマンドライン引数が取得され、ライブラリ関数で定義された文字列に変換された後、それらの文字列が反復処理されて出力されます。
これで、プロジェクトを実行できます。
dotnet run -p src/App Hello World
これにより、以下の出力がコンソールに表示されます。
Nice command-line arguments! Here's what JSON.NET has to say about them:
I used to be Hello but now I'm ""Hello"" thanks to JSON.NET!
I used to be World but now I'm ""World"" thanks to JSON.NET!
簡単ですね。次に、単体テストを追加し、それをソリューションとライブラリに接続してみましょう。
dotnet new xunit -lang F# -o tests/LibTests
dotnet add tests/LibTests/LibTests.fsproj reference src/Library/Library.fsproj
dotnet sln add tests/LibTests/LibTests.fsproj
ここで、テスト プロジェクト内の Tests.fs の内容を以下の内容に置き換えます。
module Tests
open Xunit
[<Fact>]
let ``Test Hello`` () =
let expected = """I used to be Hello but now I'm "Hello" thanks to JSON.NET!"""
let actual = Library.getJsonNetJson "Hello"
Assert.Equal(expected, actual)
この単純なテストでは、出力が正しいことを確認します。テスト名が 2 つのバッククォートで囲まれていることに注意してください。これで、より自然な名前をテストで使用できるようになります。この形式は F# のテストでは一般的です。また、引用符で囲まれた文字列を F# のコードに埋め込む場合は、三重引用符で囲まれた文字列を使用します。好みに応じて、バックスラッシュを使用することもできます。
これで、テストを実行できます。
dotnet test
出力を確認すると、テストをパスしたことがわかります。
Starting test execution, please wait...
Test Run Successful.
Total tests: 1
Passed: 1
Total time: 4.9443 Seconds
以上です。最小限のツールセットを使用して、単体テストを実行済みのライブラリと、そのライブラリ コードを実行するコンソール アプリを構築できます。Visual Studio Code や Ionide プラグインなどの F# コード エディターと組み合わせて使用すれば、これだけで完全なソリューションを構築できます。実際に、プロフェッショナルな F# 開発者の多くは、このツールセットだけを使って日常の業務をこなしています。完全なソリューションについては、GitHub の bit.ly/2Svquuc を参照してください。
以上の内容だけでもかなり楽しめますが、ライブラリ プロジェクトやコンソール アプリよりもさらに興味深いものに目を向けてみましょう。
F# で Web アプリを構築する
F# は、ライブラリ プロジェクトやコンソール アプリだけでなく、はるかに多くの目的で使用できます。F# の開発者が構築する一般的なソリューションの中には、Web アプリや Web サービスがあります。これらを構築するには、主に 3 つの選択肢があります。それぞれについて簡単に説明しましょう。
Giraffe (bit.ly/2Z4zPeP) は、「関数型プログラミングの ASP.NET Core へのバインディング」だと考えるとよいでしょう。 これは基本的に、ルートを F# の関数として公開するミドルウェア ライブラリです。Giraffe は、Web サービスや Web アプリの構築方法について比較的寛容な「最小限の」ライブラリだと言えます。関数型の手法を使用してルートを構成するためのすばらしい方法がいくつか用意されており、JSON や XML の操作などを簡略化する便利な組み込み関数がいくつか含まれていますが、プロジェクトをどのように構成するかはユーザー次第です。多くの F# 開発者が Giraffe を使用している理由は、この柔軟性と、内部で ASP.NET Core が使用されているために堅牢な基盤を備えていることにあります。
Saturn (bit.ly/2YjgGsl) は、「関数型プログラミングの ASP.NET Core へのバインディングにツール一式が含まれているもの」だと言えます。 MVC パターンのフォームが使用されていますが、オブジェクト指向プログラミングの抽象化は行われず、関数的に処理されます。Saturn は Giraffe を基盤として構築されており、コアの抽象化は共有されていますが、.NET Core を使用して Web アプリを構築する方法についての寛容性は大幅に低くなっています。また、いくつかの .NET CLI コマンドライン ツールによって、データベース モデルの生成、データベースの移行、Web コントローラーとビューのスキャフォールディングを行う機能が提供されます。Saturn には、寛容性が低い代わりに Giraffe よりも多くの組み込み関数が含まれていますが、ユーザーは Saturn で要求されるアプローチを受け入れる必要があります。
Suave (bit.ly/2YmDRSJ) は、Giraffe と Saturn よりずっと以前から存在しています。Suave は Giraffe に一番影響を与えたライブラリです。両方のライブラリで Web サービス向けに F# で使用されているプログラミング モデルは、Suave で最初に構築されたからです。主な違いとして、Suave では ASP.NET Core が基盤になっておらず、独自の OWIN 準拠の Web サーバーが使用されていることが挙げられます。この Web サーバーは、Mono 経由で低電力のデバイスに組み込むことができるため、移植性が非常に高くなっています。Suave のプログラミング モデルは、ASP.NET Core の抽象化を扱う必要がないため、Giraffe よりもいくらか単純です。
まず、Giraffe でシンプルな Web サービスを構築します。.NET CLI を使用すると簡単です。
dotnet new -i "giraffe-template::*"
dotnet new giraffe -lang F# -V none -o GiraffeApp
cd GiraffeApp
これで、build.bat または sh build.sh を実行してアプリケーションをビルドできます。
プロジェクトを実行します。
dotnet run -p src/GiraffeApp
次に、localhost 経由で、テンプレート /api/hello で指定されたルートに移動できます。
https://localhost:5001/api/hello に移動すると、以下のような出力が得られます。
{"text":"Hello world, from Giraffe!"}
それはすばらしいことですが、この出力が生成されるしくみを見ていきましょう。そのために、Program.fs ファイルを開き、webApp 関数を確認します。
let webApp =
choose [
subRoute "/api"
(choose [
GET >=> choose [
route "/hello" >=> handleGetHello
]
])
setStatusCode 404 >=> text "Not Found" ]
ここで行われている処理は、いくらか複雑な関数型プログラミングの概念に支えられています。しかし、根本的にはドメイン固有言語 (DSL) なので、使用するためにそのしくみをすべて理解する必要はありません。基本的なしくみは次のとおりです。
webApp 関数は、choose という関数で構成されています。choose 関数は、サーバーに要求が行われると ASP.NET Core ランタイムによって実行されます。要求が確認され、一致するルートが検索されます。ルートが見つからない場合は、一番下に定義されている 404 ルートにフォールバックします。
subRoute を定義しているので、choose 関数ではすべての子ルートがクロールされます。クロール対象の定義は、内部の choose 関数とそのルートのリストで指定されています。ご覧のように、ルートのリスト内には、ルート パターンとして "/hello" という文字列を指定するルートがあります。
このルートは、実際には route という名前の別の F# 関数です。route 関数は、ルート パターンを指定する文字列パラメーターを受け取り、>=> 演算子で合成されます。これは、「このルートは、後に続く関数に対応している」という意味になります。 この種の合成は Kleisli 合成と呼ばれています。その理論的な基礎を理解していなくても >=> 演算子は使用できますが、堅固な数学的基盤があることは知っておく価値があります。すでに説明したように、F# の開発者は正確性を重視していますが、数学的基盤よりも正確なものはないでしょう。
>=> 演算子の右側には handleGetHello という関数がありますが、これは別の場所で定義されています。この関数が定義されている HttpHandlers.fs というファイルを開いてみましょう。
let handleGetHello =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let response = {
Text = "Hello world, from Giraffe!"
}
return! json response next ctx
}
「通常の」 F# 関数とは異なり、このハンドラー関数は実際にはラムダとして定義されています。これはファーストクラスの関数です。F# であまり一般的ではないこのスタイルが選ばれた理由は、ラムダで受け取る 2 つのパラメーター (next と ctx) は通常、基盤となっている ASP.NET Core ランタイムによって作成されてハンドラーに渡され、必ずしもユーザー コードで処理する必要はないからです。プログラマとしての観点から見ると、これらのパラメーターを自分でやり取りする必要はありません。
ラムダ関数で定義されているこれらのパラメーターは、ASP.NET Core ランタイム内で定義されており、そのランタイム自体によって使用される抽象化です。ラムダ関数の本文内でこれらのパラメーターを使用すると、シリアル化して ASP.NET Core パイプラインに流したいオブジェクトを作成できます。response という値は、1 つのラベル Text を含む F# レコード型のインスタンスです。Text は文字列なので、シリアル化する文字列が指定されています。この型の定義は、Models.fs ファイル内にあります。この関数はその後、next および ctx パラメーターを使用して response の JSON でエンコードされた表現を返します。
Giraffe パイプラインを確認できるもう 1 つの方法は、関数の最初と最後に小さなボイラープレートを配置し、ASP.NET Core パイプラインの抽象化に準拠して、その間に必要なものを追加することです。
let handlerName =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
// Do anything you want here
//
// ... Well, anything within reason!
//
// Eventually, you’ll construct a response value of some kind,
// and you’ll want to serialize it (as JSON, XML or whatever).
//
// Giraffe has multiple middleware-function utilities you can call.
return! middleware-function response next ctx
}
前もって多くのことを学ぶ必要があると思えるかもしれませんが、これは Web アプリや Web サービスを構築するためのとても生産的で簡単な方法です。このしくみを説明するために、名前を指定したら挨拶を出力する新しいハンドラー関数を HttpHandlers.fs ファイルに追加します。
let handleGetHelloWithName (name: string) =
fun (next: HttpFunc) (ctx: HttpContext) ->
task {
let response = {
Text = sprintf "Hello, %s" name
}
return! json response next ctx
}
前に示したように、ASP.NET Core ミドルウェアに準拠するために必要なボイラープレートを使用して、このハンドラーを設定しました。主な違いは、ここで追加するハンドラー関数では文字列を入力として受け取ることです。前に示したのと同じ応答の型を使用します。
次に、Program.fs ファイルに新しいルートを追加しますが、入力として任意の文字列を指定したいので、route 関数以外のものを使用する必要があります。Giraffe では、この目的に最適な routef 関数が定義されています。
let webApp =
choose [
subRoute "/api"
(choose [
GET >=> choose [
route "/hello" >=> handleGetHello
// New route function added here
routef "/hello/%s" handleGetHelloWithName
]
])
setStatusCode 404 >=> text "Not Found" ]
routef 関数では、以下の 2 つの入力を受け取ります。
- ルートとその入力 (この場合は、%s を含む文字列) を表すフォーマット文字列
- ハンドラー関数 (前に定義したもの)
ここでは >=> 演算子を指定していないことに気付くでしょう。その理由は、routef には 2 つのパラメーター、つまり、文字列パターン (F# フォーマット文字列で指定) と、F# フォーマット文字列で指定された型を操作するハンドラーがあるためです。これは、入力として文字列パターンのみを受け取る route 関数とは対照的です。この場合、routef とここで使用するハンドラーを他のものと合成する必要がないため、追加のハンドラーを合成するために >=> 演算子を使用することはありません。ただし、特定の HTTP 状態コードを設定するなどの操作を行う場合は、>=> で合成して処理します。
これで、アプリを再ビルドして https://localhost:5001/api/hello/phillip に移動できます。実行すると、以下のようになります。
{"text":"Hello, Phillip”}
以上です。簡単ですね。あらゆるライブラリやフレームワークと同様、学習する必要がある事柄はいくつかありますが、抽象化の概念に慣れれば、必要な操作を行うルートやハンドラーを追加するのは非常に簡単です。
Giraffe の機能の詳細については、ドキュメント (bit.ly/2GqBVhT) を参照してください。この記事でデモを行った実行可能なサンプル アプリは、bit.ly/2Z21yNq にあります。
実行速度の向上
次に、F# の実用的なアプリケーションから離れて、パフォーマンス面について詳しく見ていきましょう。
トラフィックの多い Web サービスのようなものを構築する場合は、パフォーマンスが重要です。具体的には、GC でクリーンアップされる不要なアロケーションを避けることが、実行時間の長い Web サーバー プロセスに対する効果的な対策の 1 つとして挙げられます。
F# と .NET Core を使用しているときは、この場合、Span<'T> のような型が役立ちます。Span はデータのバッファーを参照できるウィンドウのようなもので、データの読み取りと操作に使用できます。Span<'T> では、ランタイムでさまざまなパフォーマンス向上が保証されるように、その使用方法に多くの制限があります。
この点について、サンプルで説明します (Steven Toub 氏による「Span のすべて:.NET の新しい頼みの綱を探索する」を msdn.com/magazine/mt814808 で参照してください)。ここでは、BenchmarkDotNet を使用して結果を測定します。
まず、.NET Core でコンソール アプリを作成します。
dotnet new console -lang F# -o Benchmark && cd Benchmark
dotnet add package benchmarkdotnet
次に、図 2 に示すように、このアプリを変更して、通常の実装が含まれるルーチンと Span<'T> を使用した実装が含まれるルーチンでベンチマークを実行します。
図 2 Span<'T> を使用する場合と使用しない場合の Parsing ルーチンのベンチマーク
open System
open BenchmarkDotNet.Attributes
open BenchmarkDotNet.Running
module Parsing =
/// "123,456" --> (123, 456)
let getNums (str: string) (delim: char) =
let idx = str.IndexOf(delim)
let first = Int32.Parse(str.Substring(0, idx))
let second = Int32.Parse(str.Substring(idx + 1))
first, second
let getNumsFaster (str: string) (delim: char) =
let sp = str.AsSpan()
let idx = sp.IndexOf(delim)
let first = Int32.Parse(sp.Slice(0, idx))
let second = Int32.Parse(sp.Slice(idx + 1))
struct(first, second)
[<MemoryDiagnoser>]
type ParsingBench() =
let str = "123,456"
let delim = ','
[<Benchmark(Baseline=true)>]
member __.GetNums() =
Parsing.getNums str delim |> ignore
[<Benchmark>]
member __.GetNumsFaster() =
Parsing.getNumsSpan str delim |> ignore
[<EntryPoint>]
let main _ =
let summary = BenchmarkRunner.Run<ParsingBench>()
printfn "%A" summary
0 // Return an integer exit code
Parsing という名前のモジュールには、指定された区切り記号で文字列を分割し、その文字列の各半分を表すタプルを返す、2 つの関数が含まれています。ただし、getNumsFaster という名前の一方の関数では、アロケーションを解消するために Span<'T> と構造体タプルの両方を使用しています。ご覧のとおり、結果は大きく異なります。
ベンチマークを実行して結果を生成します。
dotnet run -c release
これにより、結果が生成され、markdown、HTML、その他の形式で共有できます。
このベンチマークは、以下のハードウェアとランタイム環境を備えたノート PC で実行されました。
- BenchmarkDotNet v0.11.5
- macOS Mojave 10.14.5 (18F132) [Darwin 18.6.0]
- Intel Core i7-7700HQ CPU 2.80GHz (Kaby Lake)、1 CPU、8 論理コアおよび 4 物理コア
- .NET Core SDK=3.0.100-preview5-011568 (64 ビット)
図 3 にその結果を示します。
図 3 ベンチマーク結果
メソッド | 平均 | エラー | 標準偏差 | 比率 | Gen0 | Gen1 | Gen2 | 割り当て済み |
GetNums | 90.17 ナノ秒 | 0.6340 ナノ秒 | 0.5620 ナノ秒 | 1.00 | 0.5386 | - | - | 88 バイト |
GetNumsFaster | 60.01 ナノ秒 | 0.2480 ナノ秒 | 0.2071 ナノ秒 | 0.67 | - | - | - | - |
すばらしい結果です。getNumsFaster ルーチンでは、割り当てられる追加のバイト数が 0 になるだけでなく、33% も高速になりました。
この結果の重要性に納得できない場合は、データに対して 100 回の変換を実行する必要があり、そのすべてが通信量の多い Web サービスのホット パスで発生するシナリオを考えてみてください。そのようなサービスへの要求が 1 秒あたり数百万というオーダーになった場合、各変換 (または一部の変換) でアロケーションが発生すると、パフォーマンスに関するかなり重大な問題に直面することになります。しかし、Span<'T> や構造体タプルといった型を使用すれば、多くの場合、これらのアロケーションが発生しなくなります。ベンチマークに示されているように、指定された操作を実行するための実測時間が大幅に短縮されることもあります。
まとめ
これまで説明してきたように、.NET Core で F# を使用すると非常に多くのことが可能になります。すぐに使い始めることができ、Web アプリケーションの構築にも簡単に取りかかることができます。さらに、Span<'T> のようなコンストラクトを使用できるので、F# はパフォーマンスが重視される作業にも使用できます。
F# は .NET Core で発展を続けており、そのコミュニティは成長し続けています。読者の皆様がコミュニティに参加して、F# と .NET ですばらしい成果をあげることを願っています。
Phillip Carter は、Microsoft の .NET チームのメンバーです。彼は、F# 言語とそのツール、F# のドキュメント、C# コンパイラ、および Visual Studio 向けの .NET プロジェクト統合ツールに取り組んでいます。
この記事のレビューに協力してくれた技術スタッフのDustin Moris Gorski に感謝します。