Open11

🦒F# Giraffeで作るWebアプリケーション

sheeplasheepla

はじめに

このスクラップは、F#によるWebアプリケーション開発に関する知見と.NET周辺の覚えておきたい事項をまとめたものです。
なるべく実用性を意識しつつ、F#や.NETの前提知識が少なくてもスムーズに開発を始められるように書いていきます。

対象読者

他の言語でのWebアプリケーション開発経験、特にバックエンドの一般的な知識があり、F#の基本的な文法を理解した人を想定しています。なお、.NET、ASP.NET Coreについての補足説明を多めに盛り込んでいるのでこれらについては事前知識があまりなくても始められるように書いてみたつもりです。

F#ってなに

F#は.NETの上で動作するマルチパラダイム言語。OCamlやMLなどの非純粋の静的型付け関数型言語に近い構文を持ち、イミュータブル指向でカリー化(引数の部分適用)、代数的データ型(Discriminated Union: 判別共用体)やパターンマッチ、コンピュテーション式(DSLを構築できるカスタマイズ可能な糖衣構文)、型プロバイダ(外部データに基づく型の自動的な逆生成)などの強力な言語機能を備えている。
また、.NETとの相互運用性が考慮されており、クラスやインタフェースを使ったオブジェクト指向プログラミングもサポートしている。

関数型の特徴があるとはいえ、RustやTypeScriptなどを書いた経験があれば比較的とっつきやすい部類ではないかと思う。個人的には「普通に書きやすいマルチパラダイム言語」の一つとして認知されてほしいな〜という想いがある。

.NET Coreを使えばクロスプラットフォームに対応したアプリケーションを簡単にビルドすることができる。
また、F#はC#などのCLRの上で動作する言語との相互運用性が考慮されており、.NETの標準APIや多くのC#向けのライブラリをC#と同じように呼び出すことができる。

いまだに「.NETってWindows限定なんでしょ」という認識の人をSNSで見かけますが何故なんでしょう...。.NETの公式ページにデカデカと「クロスプラットフォーム」って書いてあるので見てね!

F#についてのFAQ

  • 開発環境ってWindowsは必須? IDEって必須?: .NETといえばWindows+VisualStudioなどのIDEが鉄板の構成と思われているかもしれません。もちろん、統合された環境は便利ではありますが必ずしも必須ではありません。
    VSCodeなどの一般的なエディタを使う場合はIonideなどの拡張機能を使うことができます。
    ほとんどの操作は dotnetコマンドで完結し、Linux上のNeovimやVSCodeなどのテキストエディタで開発することもできます。デスクトップアプリを作る際にXAMLやWinFormsのデザイナ機能を使いたいときやその他の固有機能、高度なリファクタリング機能、コラボレーション機能等を使いたい際はVisual Studio等のIDEを使うことになるでしょう。
  • ランタイム依存のないアプリケーションをビルドしたいんだけどできる?:
    自己完結型ビルドを実行することでランタイムに依存しない実行ファイルを発行することができます。また、Native AOTによりGoやRustのようにネイティブの実行ファイルにコンパイルできます。ただし、Native AOTビルドはまだプレビュー段階でリフレクション機能などに一部の制約があります。これらは.NETの機能なのでC#においても同様です。
  • C#のライブラリってそのまま呼び出せる?: ほとんどの.NET向けのライブラリは open SomeLibrary.Foo.Bar let instance = new SomeLibraryClass() instance.DoSomething() のようにC#と同様に呼び出して簡単にその機能を使うことができます。ただし、コード生成、dynamic型、COM相互運用、シリアライズなどが絡む場合はそのままでは完全に動作しなかったり呼び出し時に明示的にキャストが必要だったりF#用のバインディングライブラリに頼る必要があったりします。とはいえ、.NETの標準そのものがカバーする範囲が広大なので機能そのもので困ることはあまりないのではないかと思います。(どちらかというとこの相互運用の部分がやや難易度が高いです。)

Giraffeとは

GiraffeはASP.NET Coreの上に構築された F# 向けの関数型Webフレームワーク。
F#の関数型スタイルを活かしながら、ASP.NET Coreのミドルウェアや依存性注入などの機能をそのまま利用することができる。

https://giraffe.wiki/

なぜF# + Giraffeで作るか

F#およびGiraffeを使う利点としては、つぎの要素が挙げられる。

  • ASP.NET Coreのエコシステムを活用できる(ビルド・デプロイ方法は共通する部分が多い)
  • .NETおよびASP.NET Coreの広範で堅牢な標準APIとC#向けのライブラリをほぼそのまま使える
  • カスタム演算子と関数合成による簡潔なルーティングとDSLによるビューの構築により、ボイラープレートの比較的少ないコードで実現できる

また、技術書「関数型ドメインモデリング」が出たこともあり、F#などの関数型言語の特徴である代数的データ型・高階関数・イミュータブルデータ構造などを活用したドメインの表現方法にも注目が集まっているように思う。型を主軸にドメインやワークフローを表現するのは個人的にも好きなアプローチだと感じた。

https://www.kadokawa.co.jp/product/302405003608/

sheeplasheepla

開発環境の構築

.NET SDKのインストール

WindowsやmacOSの場合はインストーラーをダウンロードしてポチポチすればインストールできる。また、wingetやHomebrewを使ってコマンドラインからインストールすることもできる。

:: Windows (winget)
winget install --id Microsoft.DotNet.SDK.9
# macOS (Homebrew)
brew install --cask dotnet-sdk

Linuxの場合はAPTなどの各ディストリビューションごとのパッケージマネージャを使う方法と、インストール用のスクリプト(dotnet-install.sh)を使う方法がある。

https://dotnet.microsoft.com/ja-jp/download

https://learn.microsoft.com/ja-jp/dotnet/core/install/

dotnet-install.shを使う場合、スクリプトをダウンロードして実行すればディストリビューション固有のパッケージマネージャを使わずにユーザー空間にSDKやランタイムをインストールすることができる。

wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh
chmod +x ./dotnet-install.sh

./dotnet-install.sh --version latest # LTS(長期サポート版)の最新バージョンをインストールする
./dotnet-install.sh --version latest --runtime aspnetcore # SDKの代わりに.NETランタイムをインストールする
./dotnet-install.sh --channel 9.0 # 特定のメジャーバージョンをインストールする

~/.dotnetにPATHが通っていることを確認する。
インストールが終わったら利用するバージョンの.NET SDKインストールされているか確認する。

dotnet --help
dotnet --info

dotnet-install.sh を使ってインストールしたSDKやランタイムをアンインストールする手段はまだ公式では用意されていない。ただ、SDKは~/.dotnet/sdk、ランタイムは ~/.dotnet/shared にダウンロードされるため手動で削除すればよい。
ということで、特定のバージョンのSDK/ランタイムをサクっと消すためのスクリプトも自作したのでもし良ければどうぞ。

https://github.com/sheepla/dotnet-uninstall

REPLでF#と戯れてみる(dotnet fsi)

.NET CLIにはF#のREPL兼 F#スクリプト(*.fsx) の実行環境であるFSI: F# Interactiveが付属している。dotnet fsiを実行するとREPL環境に入る。F#の式を打ち込み、末尾に;;を付けると式が評価される。 REPL環境を抜けるには Ctrl-D または #quit;;と入力する。
ビルドやテストなどに使う簡易的なスクリプトもF#スクリプトファイル(HogeHoge.fsx)に書いておけば dotnet fsi HogeHoge.fsx で実行できる。

$ dotnet fsi

> let rec fib n =
-     match n with
-     | 0 -> 0
-     | 1 -> 1
-     | n -> fib (n - 1) + fib (n - 2)
- ;;
val fib: n: int -> int

> fib 10
- ;;
val it: int = 55

> #quit;;
$

Hello, World!

dotnet new console -lang=F# -o FSharpHelloWorld
cd FSharpHelloWorld
dotnet run

Hello From F#と出力されればOK!

ビルドと実行

フォーマッター(fantomas)の導入

F#のコードを自動整形するため、fantomasを導入する。VSCodeのIonide 拡張機能経由でインストールするか、 dotnet tool install コマンドでインストールすることができる。

https://github.com/fsprojects/fantomas

dotnet tool install fantomas

スタイルは .editorconfigに記述する。
スタイルの一覧はConfiguration - fantomas で確認できる。また、スタイルを変更しながら対話的に動作を確認できるオンラインプレビューアもある。

https://fsprojects.github.io/fantomas-tools/#/fantomas/preview

デフォルトのスタイルがあまり見慣れない見た目なのでこんな感じにカスタマイズしている。

# .editorconfig
[*.{fs,fsx,fsi}]

# 1 行あたりの最大文字数
max_line_length = 80

# インデントサイズ
indent_size = 4

# クラスのコンストラクタの前にスペースを入れるかどうか
fsharp_space_before_class_constructor = false

# ドット付きアクセスの最大許容幅
fsharp_max_dot_get_expression_width = 60

# 複数行にわたる波カッコのスタイル
# 選択肢:
# - cramped (詰めたスタイル)
# - aligned (同じインデントレベルで整列する(C#風?))
# - stroustrup (開き波カッコの前で改行せず、閉じ波カッコの前で改行する(Java風?))
fsharp_multiline_bracket_style = aligned 

# 複数行ラムダ式の終わりに改行を入れるかどうか
fsharp_multi_line_lambda_closing_newline = true

# 複数行のコンピュテーション式の前に改行を入れるかどうか
fsharp_newline_before_multiline_computation_expression = false

# Elmライクなスタイルの構文を有効にする(実験的機能)
# 関数適用における最後の(2つの)配列またはリスト引数にStroustrupスタイルを適用する
fsharp_experimental_elmish = true

# 連続する空行の最大数
fsharp_keep_max_number_of_blank_lines = 1

Dockerを使う

.NETには公式のDockerイメージが Microsoft Artifact Registryに公開されている。

https://learn.microsoft.com/ja-jp/dotnet/core/docker/introduction

詳しくは公式のチュートリアルを参照:

https://learn.microsoft.com/ja-jp/dotnet/core/docker/build-container?tabs=linux&pivots=dotnet-9-0

sheeplasheepla

🦒 Giraffeによる開発を始める

テンプレートの入手

Giraffeには公式のプロジェクトテンプレートが用意されている。
.NETのテンプレートは dotnet newコマンドで管理できる。

newという名前だが、テンプレートの管理全般をこのサブコマンドが担うのでtemplateと読み替えた方が分かりやすいかもしれない。

dotnet new search giraffe # nuget.orgからGiraffeのテンプレートを検索する
dotnet new install giraffe # Giraffeのテンプレートをインストールする
dotnet new list # テンプレートの一覧を確認

Hello, Giraffe!

dotnet new giraffe -o GiraffeHello を実行してGiraffeのプロジェクトテンプレートから新しいプロジェクトを作成する。-o 【プロジェクト名】指定するとプロジェクト名のディレクトリが作成され、その配下にソースや設定ファイルが展開される。指定しない場合はカレントディレクトリにプロジェクトが作成される。

dotnet new giraffe -o GiraffeHello
cd GiraffeHello

プロジェクトの構成は次のようになっている。

 GiraffeHello/
 ├── GiraffeHello.fsproj -- F#のプロジェクトファイル
 ├── Program.fs -- プログラムのエントリーポイント
 ├── web.config -- ASP.NET Coreの設定ファイル
 └── WebRoot/ -- Webコンテンツのルート
     └── main.css

ASP.NET Coreの設定ファイル web.config を編集し、ASP.NET Coreモジュールのバージョンを変更する。

    <!-- web.config -->
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <system.webServer>
        <handlers>
---       <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
+++       <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
        </handlers>
        <aspNetCore processPath="dotnet" arguments="GiraffeHello.dll" stdoutLogEnabled="false" stdoutLogFile="logs/stdout" />
      </system.webServer>
    </configuration>

F#プロジェクトファイル(*.fsproj)を編集し、現在利用している.NET SDKのバージョンに合わせる。

    <?xml version="1.0" encoding="utf-8"?>
    <Project Sdk="Microsoft.NET.Sdk.Web">
      <PropertyGroup>
---    <TargetFramework>net8.0</TargetFramework>
+++    <TargetFramework>net9.0</TargetFramework>
        <AssemblyName>GiraffeHello</AssemblyName>
        <EnableDefaultContentItems>false</EnableDefaultContentItems>
        <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Giraffe" Version="6.4.0" />
        <PackageReference Include="Giraffe.ViewEngine" Version="1.4.*" />
      </ItemGroup>
      <ItemGroup>
        <Compile Include="Program.fs" />
      </ItemGroup>
      <ItemGroup>
        <None Include="web.config" CopyToOutputDirectory="PreserveNewest" />
        <Content Include="WebRoot\**\*">
          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        </Content>
      </ItemGroup>
    </Project>

dotnet watch runを実行すると、ホットリロード可能な開発サーバーが立ち上がる。
ソースファイルを編集して上書きすることで自動的にリロードされる。

└─▶   dotnet watch run # ホットリロード可能な開発サーバーを起動
dotnet watch 🔥 Hot reload enabled. For a list of supported edits, see https://aka.ms/dotnet/hot-reload.
  💡 Press "Ctrl + R" to restart.
dotnet watch ⌚ Building /tmp/tmp.R6LLN5ZlbL/GiraffeHello/GiraffeHello.fsproj ...
dotnet watch 🔨 Build succeeded: /tmp/tmp.R6LLN5ZlbL/GiraffeHello/GiraffeHello.fsproj
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /tmp/tmp.R6LLN5ZlbL/GiraffeHello

ブラウザを開き、

アプリケーションの発行とデプロイ

.NETでは、アプリケーションを配布・デプロイ可能な形にパッケージングを行うことを「発行(publish)」という。アプリケーションの発行を行うには、dotnet publishを実行する。
実行すると、bin/Release/netX.X/publish/にアプリケーションが出力される。

.NETのデプロイモデルには大きく分けて「ランタイム依存」と「自己完結型」の2種類がある。

  • ランタイム依存: .NETのランタイムをアプリケーションに同梱せずに実行する環境にインストールされたランタイムを利用する。配布サイズが小さくなる、アプリケーションを再発行せずにランタイムのバージョン更新ができるといった利点がある。
  • 自己完結型: .NETのランタイムをアプリケーションに組み込むため配布サイズが大きくなり、各プラットフォームごとに発行を行う必要がある一方で、ランタイムのバージョン違いなどの要因によるトラブルを避けられデプロイが簡単になるといった利点がある。

これらを切り替えるには、プロジェクトファイル(*.fsproj)のビルドオプションを変更する。

<PropertyGroup>
  <OutputType>Exe</OutputType>
  <TargetFramework>net8.0</TargetFramework>
  <SelfContained>true</SelfContained><!-- true => 自己完結型 -->
  <RuntimeIdentifier>linux-x64</RuntimeIdentifier><!-- ランタイム識別子 -->
</PropertyGroup>

デプロイ先でアプリケーションを起動するには、自己完結型の場合はアプリケーションをそのまま実行する。ランタイム依存の場合は dotnet 【アプリケーション名】.dll で実行する。
ランタイム識別子(RID)は次のカタログを参照。

https://learn.microsoft.com/ja-jp/dotnet/core/rid-catalog

sheeplasheepla

📋️【補足】.NETについての基礎知識

「.NET」という用語とその関連技術

  • .NET は、マルチプラットフォーム(Windows / macOS / Linux)対応のアプリケーションを構築するための開発プラットフォーム。
  • C#やF#など複数の言語で使用でき、ライブラリとツールの共通基盤を提供する。

標準仕様

  • .NET Standard:
    異なる.NET実装間で共通に使えるAPIセットを定義した仕様。複数のターゲット(.NET Framework / .NET / Monoなど)に対応するライブラリを作るときに使われる。現在は役割を終え、.NET 5以降は単一の「.NET」に集約された。

ソフトウェアフレームワークの実装

  • .NET (旧称: .NET Core):
    クロスプラットフォーム対応の最新.NET実装。現在の主流で、Windows / Linux / macOS で動作する。バージョン5以降は「Core」の名前が外れ、単に「.NET」と呼ばれる。

  • Mono:
    .NET FrameworkのLinux向け実装として登場したオープンソースのプロジェクト。現在はXamarinやUnityなど特定用途向けに使われている。

  • .NET Framework:
    Windows専用の古い.NET実装。レガシーアプリの保守や業務向けWindowsアプリで使用される。現在は新規開発向けではなく、主に保守フェーズにある。

Webアプリケーションフレームワークの実装

  • ASP.NET Core:
    クロスプラットフォーム対応の軽量なWebフレームワーク。MVCやMinimal API、gRPCなど複数のスタイルをサポート。GiraffeはこのASP.NET Core上で動作するF#向けのWebフレームワーク。

  • ASP.NET:
    .NET Framework上で動作する旧来のWebフレームワーク。Web Formsや古いMVCのバージョンを含む。現在は保守フェーズ。

デスクトップUIフレームワークの実装

  • Windows Forms (WinForms):
    古くからあるWindows向けGUIフレームワーク。イベント駆動型。業務系アプリで根強く使われている。

  • WPF (Windows Presentation Foundation):
    より柔軟でリッチなUIを構築できるWindows専用フレームワーク。XAMLベースのUI定義。WinFormsより新しい。

  • Xamarin:
    モバイルアプリ(iOS / Android)をC#で開発できるフレームワーク。現在はMAUIに統合されつつある。

  • UWP (Universal Windows Platform):
    Windows 10向けに導入されたモダンなアプリフレームワーク。現在はWinUIに役割を移している。

  • AvaloniaUI:
    WPFライクな記法でクロスプラットフォーム対応のGUIアプリを構築できるOSSのUIフレームワーク。F#とも相性が良い。

  • WinUI:
    Microsoftの最新UIフレームワーク。Windowsアプリのネイティブな見た目を提供。MAUIやUWPの後継的な位置づけ。

  • MAUI (.NET Multi-platform App UI):
    .NET 6以降で登場した新しいクロスプラットフォームUIフレームワーク。デスクトップとモバイル両方を一つのコードベースで開発可能。Xamarinの後継。

.NETのビルドオプション

  • .NET SDKでは dotnet build コマンドでアプリケーションをビルドする。主に以下のようなオプションがある:

    • --configuration (Debug / Release)
      ビルド構成を指定。通常、開発時は Debug、リリース用には Release を使用する。

    • --runtime
      対象とするランタイム(OSとCPUアーキテクチャ)を指定。例: win-x64, linux-arm64など。

    • --self-contained
      アプリに.NETランタイムを同梱して配布可能にする。実行環境に.NETがインストールされていなくても動作する。

    • --output
      ビルド成果物の出力先フォルダを指定。

.NETのリリースサイクル

  • .NETは毎年11月に新バージョンがリリースされる。

  • LTS(Long Term Support)とSTS(Standard Term Support)の2種類がある。

    • LTS: 長期サポート版(3年間のサポート)。偶数バージョン(例: .NET 6, .NET 8)が該当。
    • STS: 通常サポート版(18か月のサポート)。奇数バージョン(例: .NET 7, .NET 9)が該当。

ソリューションとプロジェクト

  • ソリューション (.sln):
    複数のプロジェクトをとりまとめるコンテナとして機能するファイル。IDEやツールにプロジェクト間の関係を示す。

  • ソリューション (.slnx):
    .NET 9から導入された新フォーマット。人間にも読みやすく、管理が容易。dotnet sln migrateで従来のslnから新フォーマットの.slnxへ変換することができる。

  • プロジェクト (.fsproj / .csproj):
    各言語(F#, C#など)ごとに定義されるプロジェクトファイル。依存ライブラリ、ビルド対象、出力形式などを定義。

通常、大規模アプリではプロジェクトを機能単位で分割し、ソリューションで一括管理する。
小規模な開発では単一プロジェクトだけで完結させることも多い。

# ソリューションを作成
dotnet new sln -n MyApp

# プロジェクトを追加
dotnet sln MyApp.sln add src/MyApp.fsproj
sheeplasheepla

🗄️ データベースアクセス

データベースへの接続

各データベースに接続するには次のようなプロバイダが必要となる。対応するプロバイダのパッケージを dotnet add package でインストールして、コネクションを .Open() することで各データベースに接続できる。このあたりはC#と同じ。

  • SQLite: Microsoft.Data.SQLite
  • MySQL: MySqlConnector
  • PostgreSQL: Npgsql
  • Oracle Database: Oracle.ManagedDataAccess
  • Microsoft SQL Server: System.Data.SqlClient または Microsoft.Data.SqlClient (推奨)
// SQLite
open Microsoft.Data.Sqlite

use conn = new SqliteConnection("Data Source=test.db")
conn.Open()
// MySQL
open MySqlConnector

use conn = new MySqlConnection("server=localhost;user=root;password=pass;database=test")
conn.Open()
// PostgreSQL
open Npgsql

use conn = new NpgsqlConnection("Host=localhost;Username=postgres;Password=pass;Database=test")
conn.Open()
// Oracle
open Oracle.ManagedDataAccess.Client

use conn = new OracleConnection("User Id=userid;Password=pass;Data Source=localhost:1521/XEPDB1")
conn.Open()
// SQL Server
open Microsoft.Data.SqlClient

use conn = new SqlConnection("Server=localhost;Database=test;User Id=sa;Password=pass;")
conn.Open()

use はF#の let とC#の using を合わせたようなもので、値を束縛しつつスコープを抜けたときに Dispose() メソッドが暗黙的に呼ばれるようにすることができる。
https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/resource-management-the-use-keyword

接続文字列を安全に構築する(ConnectionStringBuilder)

DBへ接続するために必要な情報を表す接続文字列(ConnectionString)は生の文字列で組み立てるとエスケープ忘れやインジェクションの問題が発生しやすい。そのため、各DBMS用の接続文字列ビルダを使って構造的に接続文字列組み立てを行うとよい。

各DBMS用のNugetパッケージが必要になる。

  • SQLite: System.Data.SQLite.SQLiteConnectionStringBuilder
  • MySQL: MySql.Data.MySqlClient.MySqlConnectionStringBuilder
  • PostgreSQL: Npgsql.NpgsqlConnectionStringBuilder
  • Oracle: Oracle.ManagedDataAccess.Client.OracleConnectionStringBuilder

実際に接続する際は次のように記述する。

open System
open System.Data.Common

// SQLite
let buildSQLiteConnectionString dataSource =
    let builder = new System.Data.SQLite.SQLiteConnectionStringBuilder()
    builder.DataSource <- dataSource
    builder.Version <- 3
    builder.ToString()

// MySQL
let buildMySQLConnectionString server database user password =
    let builder = new MySql.Data.MySqlClient.MySqlConnectionStringBuilder()
    builder.Server <- server
    builder.Database <- database
    builder.UserID <- user
    builder.Password <- password
    builder.SslMode <- MySql.Data.MySqlClient.MySqlSslMode.Required
    builder.ToString()

// PostgreSQL
let buildPostgresConnectionString host database user password =
    let builder = new Npgsql.NpgsqlConnectionStringBuilder()
    builder.Host <- host
    builder.Database <- database
    builder.Username <- user
    builder.Password <- password
    builder.SslMode <- Npgsql.SslMode.Require
    builder.ToString()

// Oracle
let buildOracleConnectionString host serviceName user password =
    let builder = new Oracle.ManagedDataAccess.Client.OracleConnectionStringBuilder()
    builder.DataSource <- $"{host}/{serviceName}"
    builder.UserID <- user
    builder.Password <- password
    builder.ToString()

生のクエリを実行してみる

F#で生のSQLクエリを実行するには、System.Data.Common.DbConnectionDbCommand を使うのが標準的。SQLインジェクション防止の観点からパラメータ化クエリを使うとよい。

open System.Data.Common

let executeQuery (connection: DbConnection) (sql: string) (parameters: (string * obj) list) =
    use cmd = connection.CreateCommand()
    cmd.CommandText <- sql
    // パラメータを追加
    for (name, value) in parameters do
        let p = cmd.CreateParameter()
        p.ParameterName <- name
        p.Value <- value
        cmd.Parameters.Add(p) |> ignore

    // 接続をオープン
    connection.Open()

    // クエリを実行
    use reader = cmd.ExecuteReader()

    // 結果をリストに変換
    let results = ResizeArray<Map<string,obj>>()
    while reader.Read() do
        let row =
            [ for i in 0 .. reader.FieldCount - 1 ->
                reader.GetName(i), reader.GetValue(i) ]
            |> Map.ofList
        results.Add(row)
    results |> Seq.toList

クエリビルダ+マイクロORMであるDapper.FSharpを使う(おすすめ!)

C#では、軽量なO/RマッパーであるDapperがよく使われる。これは、DBのレコードとオブジェクトとの間のマッピングのみを担当するシンプルなもので、SQL自体は文字列として渡して実行するためクエリを透過的に扱うことができる。ドキュメントでは「micro-ORM」であると説明されている。

https://github.com/DapperLib/Dapper

F#の場合、Dapper.FSharpを使うと、コンピュテーション式を使ったDSLでクエリを直感的に書けるようになり、F#のレコード型に簡単にマッピングすることができる。

https://github.com/Dzoukr/Dapper.FSharp

フル機能のORMであるEFCore.FSharpを使う

クエリを抽象化して扱いたい場合やリレーションが複雑な場合はマイグレーションを行いながら段階的に開発できるEntitiyFrameworkCore (EFCore)が使われる。EF CoreはMicrosoft公式のフル機能のORMであり、C#における開発では広く使われている。
こちらを使う場合、エンティティをクラスで表現する必要があるためF#らしさはあまりなくC#っぽい書き方になる。F#用のサポートとして EFCore.FSharp も用意されているためこちらを使うとよい。

https://learn.microsoft.com/ja-jp/ef/core/
https://github.com/efcore/EFCore.FSharp

sheeplasheepla

🔷 F#のモジュールやパッケージについての基礎知識

F#のモジュールシステムはC#などの他の言語とは異なり少し独特なため、ここに整理しておく。

エントリーポイント

F#のエントリーポイントは [<EntryPoint>] 属性を付けた val main : string[] -> int すなわちコマンドライン引数を文字列配列で受け取って終了ステータスをintで返す関数である必要がある。型の記述は省略してもよい。

// Program.fs
[<EntryPoint>]
let main (argv: string[]) : int =
    printfn "Hello, F#"
    0

エントリーポイントとなる関数を書かずに省略することもできる。

// Program.fs
printfn "Hello, F#"

GUIアプリケーションやライブラリなどのプロジェクトでは省略するか別の方法で提供されることもある。
エントリーポイントの記述されるソースファイル名はProgram.fsとなっていることが多いが、 *.fsproj ファイルによるコンパイル対象ソースの指定で変更することもできる。

評価順序

F#のコードはモジュールや関数や値を定義した順、つまり基本的には上から下に順番に評価(evaluate)される。関数の定義より先にその呼び出そうとするとエラーとなる。複数ファイルの場合も同様で、後述するようにソースファイルのコンパイル順序にも注意する必要がある。

let a = 1
let f x = x + 1
f a |> printfn "%d" // => 2
let a = 1
f a |> printfn "%d" // error FS0039: The value or constructor 'f' is not defined.
let f x = x + 1

名前空間とモジュール

  • 名前空間: 他の .NET 言語の名前空間と同様に、アセンブリ全体にまたがって利用され、型名の衝突を防ぐために階層的に整理するためのもの。名前空間自体には値や関数の定義はできず、あくまでグループ化のためのコンテナとして機能する。モジュールとは異なり、名前空間自体に直接実行可能なコードを持たせることはできない。
  • モジュール: 関数・値・型などの具体的な実装を階層化してまとめるためのF#の機能。ひとつのF#ソース内でネストさせることができ、private修飾子をつけて値や関数などを隠蔽することもできる。

ソースファイルの追加とコンパイル順序の管理

開発中のプロジェクト内で新しくF#ソース(*.fs)を追加する際は、F#プロジェクトファイル(*.fsproj)の<ItemGroup>ノードの配下に<Compile Include="【ソースファイルへのパス】" />を追加する必要がある。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <!-- 依存するサブモジュール -->
    <Compile Include="src/ModuleA.fs" />
    <Compile Include="src/ModuleB.fs" />
    <Compile Include="src/ModuleC.fs" />
    <!-- エントリーポイントのあるモジュール -->
    <Compile Include="src/Program.fs" />
  </ItemGroup>
        
</Project>

外部パッケージのインストール

外部パッケージは dotnet add [<PROJECT>] package <PACKAGE_NAME> コマンドでインストールする。
--version <VERSION> を指定してインストールパッケージのバージョンを明示的に指定することもできる。

実行すると、F#プロジェクトファイルの <ItemGroup>ノードの配下に <PackageReference Include="【パッケージ名】" Version="【バージョン】" />が追加される。

直接F#プロジェクトファイルをいじって依存パッケージを追加することもできる。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>

  <ItemGroup>
    <!-- Dapper.FSharpとSystem.Data.SQLiteをインストールした場合 -->
    <PackageReference Include="Dapper.FSharp" Version="4.9.0" />
    <PackageReference Include="System.Data.SQLite" Version="1.0.119" />
  </ItemGroup>

</Project>

また、パッケージは nuget.orgで見つけることができる。

https://nuget.org

sheeplasheepla

🦒 Giraffeの基本

アプリケーションのセットアップ部分のカスタマイズ

HttpHandlerと >=>演算子によるハンドラの合成

GiraffeではHttpHandlerという型を持つ関数を合成することによりルーティングを実現する。言い換えればGiraffeにおけるリクエストのハンドリングの基本単位になる。これは、ASP.NET Coreのミドルウェアに相当する。

type HttpFuncResult = Task<HttpContext option>
type HttpFunc = HttpContext -> HttpFuncResult
type HttpHandler = HttpFunc -> HttpContext -> HttpFuncResult

compose関数を使って2つのHTTPハンドラを合成することができる。

let handler1: HttpHandler = (route "/")
let handler2: HttpHandler = Successful.OK "Hello, World" 
let app = compose handler1 handler2

また、Giraffeには compose のエイリアスである >=>というカスタム演算子が用意されている。F#の演算子は単なる関数であり let (【演算子として定義する記号】) 【引数1】 【引数2】= 【定義】 のように書いて定義することができる。これにより handler1 >=> handler2 >=> handler3 >=> ... のようにこの演算子を連結させることで複数のHttpHandlerを合成して簡潔で可読性の高いパイプラインを構成することができる。

Giraffeのドキュメントでは fish operator (🐟️ おさかな演算子?) と説明されている。かわいい。

https://github.com/giraffe-fsharp/Giraffe/blob/e548019a6f3fc8de9b837b86b2cc93a23da6d949/src/Giraffe/Core.fs#L99-L112

let app =
    route "/"
    >=> setHttpHeader "X-Foo" "Bar"
    >=> setStatusCode 200
    >=> setBodyFromString "Hello World"

ルーティング

  • route: 最も基本的なルート指定。引数にパスを受け取り、そのパスにマッチした場合は次のハンドラへ渡す。
  • subRoute: ルートのすでにフィルタリングされた部分を繰り替えさずにルートを分岐させる。ネストするパスごとに分岐させるときに使う。
  • GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE, CONNECT: リクエストが特定のHTTPメソッドの場合に次のハンドラへ渡す。
  • choose: ハンドラのリストを引数で受け取りそれぞれを反復処理して最初の結果を返す。複数のハンドラにルートを振り分けるときに使う。

HttpHandlerの作成とハンドリングのパターン

HttpHandler を自分で作るには次のように書く。戻り値は HttpFuncResult すなわちHttpContextoptionTaskを返すようにする。

let myHttpHandler : HttpHandler =
    fun (next: HttpFunc) (ctx: HttpContext) ->
        // ここにハンドリングロジックを記述する

HttpHandlerを使ってリクエストをハンドリングする際のシナリオとしては、 継続早期リターンスキップ の3種類のパターンが考えられる。

継続(Continue): 何かのアクションを実行した後、常に次のパイプラインに渡す(例: 特定のヘッダーを付与する)

let setHttpHeader key value : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        ctx.SetHttpHeader key value // 特定のヘッダーを付与してそのまま別のハンドラにコンテキストを渡す
        next ctx

早期リターン(Return Early): 残りのパイプラインを続行せずに脱出する (例: レスポンスを返す)

let earlyReturn : HttpFunc = Some >> Task.FromResult

let checkUserIsLoggedIn : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        // ユーザーが認証されている場合は次のハンドラにコンテキストを渡す
        if isNotNull ctx.User && ctx.User.Identity.IsAuthenticated
        then next ctx
         // それ以外は 401 Unauthorizedを返して早期リターン
        else setStatusCode 401 earlyReturn ctx

スキップ(Skip): 特定のパターンに合致する場合はスキップする (例: 特定のHTTPメソッドに対し特定のハンドラを割り当ててそれ以外のメソッドは無視する)

let skip : HttpFuncResult = Task.FromResult None

let GET : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        // 要求メソッドがGETの場合のみ次のハンドラにコンテキストを渡す
        if HttpMethods.IsGet ctx.Request.Method
        then next ctx
        // それ以外のメソッドの場合は無視する
        else skip

【補足】コンピュテーション式

F#では seq { ... }, task { ... }, async { ... }といった構文が頻繁に登場する。これをコンピュテーション式という。コンピュテーション式は、非決定的な計算・非同期計算・有効計算・生成的計算といった複雑な計算の流れを命令的に書けるようにするための糖衣構文。
例えば、task { ... } コンピュテーション式の中では let!, use!, return!, and! といった特殊なキーワードが使えるようになり、通常の同期的なコードとは異なるタスクべースの非同期コードに展開される。

https://learn.microsoft.com/ja-jp/dotnet/fsharp/language-reference/computation-expressions

【補足】Taskべースの非同期

F#の非同期プリミティブには AsyncTask の2種類のモデルがある。

  • Async はF#のために設計された未実行の非同期処理の計算式(遅延評価)を表す。Async.RunSynchronouslyAsync.Start を呼ばれたときに初めて中の処理が実行される。
  • Task はC#で標準的に使われる実行中の非同期処理の状態(即時評価)を表す。

GiraffeではASP.NET Coreとの相互運用性のためにTaskベースの非同期モデルを採用している。

F#の非同期まわりの概念は公式ドキュメントを参照

https://learn.microsoft.com/ja-jp/dotnet/fsharp/language-reference/async-expressions

リクエストのハンドリング

クエリパラメータを取得するにはHttpContextからctx.TryGetQueryStringValue(【パラーメータ名】)を使い値を取り出す。

let someHttpHandler : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        let someValue =
            match ctx.TryGetQueryStringValue "q" with
            | None   -> "default value"
            | Some q -> q
        // ...
        // Task<HttpContext option>を返す

パスの一部をパラメータとして取得するには、routefと書式設定文字列を使う。

routef "/foo/%s/%s/%s" (fun (s1, s2, s3) -> text (sprintf "%s%s%s" s1 s2 s3))

モデルのバインディング

リクエストのペイロードをJSONとしてデシリアライズしてF#のレコード型にバインドするには、HttpContextctx.BindJsonAsync<'T>メソッドを使う。同様にXML、クエリ文字列、フォームのバインディングも ctx.BindXmlAsync<'T>()ctx.BindQueryString(?cultureInfo: CultureInfo)ctx.BindFormAsync(?cultureInfo: CultureInfo)を使うことができる。

type Person = { Name : string }

let sayHelloWorld : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        task {
            let! person = ctx.BindJsonAsync<Person>()
            let greeting = sprintf "Hello World, from %s" person.Name
            return! text greeting next ctx
        }

ファイルのアップロードの受け付け

ファイルのアップロードを受け付けるには、HttpContextctx.Request.Form.Filesを使う。

let fileUploadHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        task {
            return!
                (match ctx.Request.HasFormContentType with
                | false -> RequestErrors.BAD_REQUEST "Bad request"
                | true  ->
                    ctx.Request.Form.Files
                    |> Seq.fold (fun acc file -> sprintf "%s\n%s" acc file.FileName) ""
                    |> text) next ctx
        }

let webApp = route "/upload" >=> fileUploadHandler

バリデーション

ロギングとエラーハンドリング

ASP.NET Coreには組み込みのロギングAPIが用意されている。Giraffeからも簡単に呼び出すことができる。
Webホストの構築時にロガーをセットすると、HttpContextctx.GetLogger<【モジュール名】>() または ctx.GetLogger(【カテゴリ名】) でロガーを取り出してログを記録できる。

let configureLogging (builder : ILoggingBuilder) =
    // ログレベルのフィルタをセット (任意)
    let filter (l : LogLevel) = l.Equals LogLevel.Error
    // ロガーを構築する
    builder.AddFilter(filter)
           .AddConsole()      // コンソールロガーをセット
           .AddDebug()        // デバッグロガーをセット

           // その他追加のロガーがあれば記述
    |> ignore

[<EntryPoint>]
let main _ =
    Host.CreateDefaultBuilder()
        .ConfigureWebHostDefaults(
            fun webHostBuilder ->
                webHostBuilder
                    .Configure(configureApp)
                    .ConfigureServices(configureServices)
                    // セットアップ時にロギングを追加
                    .ConfigureLogging(configureLogging)
                    |> ignore)
        .Build()
        .Run()
    0
let someHttpHandler : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        // HttpContextからロガー(ILogger)を取り出す
        let loggerA = ctx.GetLogger<ModuleName>()
        let loggerB = ctx.GetLogger("someHttpHandler")

        // ログを書き込む
        loggerA.LogCritical("Something critical")
        loggerB.LogInformation("Logging some random info")
        // ...

        // Task<HttpContext option>を返す

https://learn.microsoft.com/en-gb/aspnet/core/fundamentals/logging/?view=aspnetcore-9.0&tabs=aspnetcore2x

sheeplasheepla

✒️フロントエンドを作る

UIに高度なインタラクション求められる場合は、VueやReactなどでフロントエンドを実装し「GiraffeによるREST APIサーバーバックエンド+ReactによるSPAフロントエンド」という構成にすることもできるが、Giraffe.ViewEngineやGiraffe.Htmxを使ってをサーバー側でビューを作ることもできる。

Giraffe.ViewEngineによるHTMLビューの構築

GiraffeにはHTMLビューをサーバーサイドで構築するための Giraffe.ViewEngine が用意されている。これはF#の軽量なDSLを使ってXmlNode を組み立てるものであり、構文は Elm に似ている。

https://giraffe.wiki/view-engine

HTMXと統合する(Girafe.Htmx)

Giraffe.Htmx を使うとGiraffe.ViewEngineのDSLに HTMX を統合してシンプルにUIの動作を書くことができる。
簡素なUIを手早く作りたい場合に便利。

https://github.com/bit-badger/Giraffe.Htmx

https://www.youtube.com/watch?v=NaFJbyT8mlU****

Razorと統合する(Giraffe.Razor)

https://github.com/giraffe-fsharp/Giraffe.Razor

DotLiquidと統合する(Giraffe.DotLiquid)

https://github.com/giraffe-fsharp/Giraffe.DotLiquid?tab=readme-ov-file

F#のDSLとBlazorでUIを作る(Fun.Blazor)

https://slaveoftime.github.io/Fun.Blazor.Docs/

sheeplasheepla

📕 よく使うライブラリ

JSONのシリアライズとデシリアライズ

  • Newtonsoft.Json: 広く使われている.NET用のJSONのシリアライズ/デシリアライズライブラリ。
    -System.Text.Json: Microsoftによる公式実装。おすすめ。
  • FSharp.SystemTextJson: System.Text.JsonにDiscriminated UnionやOption型などF#固有のデータ構造に対応させるためのサポート。

CSVの読み書き

  • CsvHelper: 高機能なCSV読み書き用ライブラリ。

F#による関数プログラミングの補助

その他

.NETの主要なライブラリは、GitHubの「Awesome .NET」にまとまっている。

https://github.com/quozd/awesome-dotnet

sheeplasheepla

🛡️ 認証(AuthN)、認可(AuthZ)

ASP.NET Coreにはすぐに利用できる認証(AuthN)、認可(AuthZ)オプションが用意されている。Giraffeでは事前に用意されたHttpHandler群を使って簡単に制御できる。

認証

認可