F#の初級者がクロスプラットフォームで自己完結型単一実行ファイルとしてデプロイできるコマンドラインアプリケーションをサクッと作れるようになるためのガイド。 細かい文法要素は省略している。
対象読者は他言語経験がある程度あってF#のチュートリアルを終えたくらいの人を想定している。 自身が関数型プログラミングに入門してから日が浅いので、関数型プログラミングに慣れていなくても(たぶん?)大丈夫。
下記の環境で検証している。
- OS: Arch Linux
- .NET Core SDK v7.0.111
- FSharp.SystemCommandLine v0.17.0-beta4
- エディタ: Neovim
$ dotnet --list-sdks
7.0.111 [/usr/share/dotnet/sdk]
$ dotnet --list-runtimes
Microsoft.NETCore.App 7.0.11 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
F#はOCaml由来の簡潔で堅牢な構文を持ち、見通しの良いプログラムを書ける。 また、.NETと統合されているためC#で書かれた広範なライブラリ群の機能をそのまま呼び出すことができる。
.NETに乗っかっているとWindowsの各機能へのアクセスが楽にできるため、 Windowsのシステム管理ツールやセキュリティツールの開発にF#を採用すると便利そうだなと思っている。
- 共通言語ランタイム (CLR) の概要
- P/Invoke - ネイティブライブラリとのFFI機構
- .NET での COM 相互運用 - COM との相互運用についての解説
- Microsoft.Management.Infrastructure - マネージドなWMIへの接続インタフェース
後述するビルドオプションをセットすれば自己完結型の単一実行ファイルを発行でき、デプロイや配布が楽になる。
公式のチュートリアルをどうぞ。
「F# for Fun and Profit」やWikibooksもおすすめ。
.NETのSDK(開発キット)は、下記のページからダウンロードできる。 今回は .NET Core 7.0をインストールする。
インストールガイドはこちらから。
.NET SDKにはfsi (F#インタラクティブ)というREPL環境が備わっており 対話的にF#プログラムを実行できる。
F#インタラクティブは dotnet fsi
で起動できる。式の末尾には ;;
を入力すること。
終了時は Ctrl-D
を押下するとREPL環境を抜けられる。
$ dotnet fsi
Microsoft (R) F# インタラクティブ バージョン F# 7.0 のための 12.4.0.0
Copyright (C) Microsoft Corporation. All rights reserved.
ヘルプを表示するには次を入力してください: #help;;
> let foo = "F#";;
val foo: string = "F#"
> printfn "Hello, %A" foo
- ;;
Hello, "F#"
val it: unit = ()
>
F#のコンソールアプリケーションを作成してみる。
プロジェクトとなるディレクトリを作り、その中でdotnet new console -lang=F#
を実行する。
$ mkdir SampleCommandLineApp && cd SampleCommandLineApp
$ dotnet new console -lang=F#
実行すると、メインプログラム Program.fs
およびプロジェクトファイル SampleCommandLineApp.fsproj
が生成される。
プロジェクトを作成すると、すでにHello, World!のコードが生成されているので何も書かずに世界に挨拶できる。
$ cat Program.fs
// For more information see https://aka.ms/fsharp-console-apps
printfn "Hello from F#"
dotnet run
を実行すると Hello from F#
と出力される。
.NET、特にC#でコマンドラインアプリケーションを作成するには、 System.CommandLineというパッケージがよく使われる。 System.CommandLineは執筆時点ではプレビュー段階だが、 高機能なコマンドラインアプリケーションを作るのによく使われる機能がひと通り備わっている。
また、F#のコマンドラインライブラリとしてはArgu, docopt.fs, FSharp.CommandLineなどがあるが、 今回はSystem.CommandLineのF#用バインディングであるFSharp.SystemCommandLineを使ってみる。 System.CommandLineの機能が使え、それでいて簡潔で分かりやすい書き方ができるので気に入った。
パッケージを追加するには dotnet add package
コマンドを使う。
非安定版を使うので --prerelase
フラグを付けて実行する。
$ dotnet add package FSharp.SystemCommandLine --prerelease
エディタを開いてプログラムをProgram.fs
に記述していく。
F#にはコンピュテーション式 という仕組みがあり、これによりライブラリの呼び出し時に特定のドメインに特化した簡潔な書き方ができる。
FSharp.SystemCommandLineでは、コマンドを表すコンピュテーション式に説明文・位置引数・オプション・ハンドラ関数・サブコマンドを埋め込んでいく形でプログラミングしていく。
まずはFSharp.SystemCommandLine
などの名前空間を open
しておく。
open System
open System.IO
open FSharp.SystemCommandLine
F#のプログラムでは暗黙的なエントリーポイントが使用されるが、
メインとなる関数に[<EntryPoint>]
属性を付けることでそこからプログラムが開始となる。
[<EntryPoint>]
let main (argv: string[]) : int =
let handler (text: string, flag: bool) :int =
printfn "Running rootCommand!"
printfn "text: %A, flag: %A" text flag
0
rootCommand argv {
description "Sample command line application"
inputs (
Input.OptionRequired<string>([ "-t"; "--text" ], "Some text"),
Input.Option<bool>([ "-f"; "--flag" ], "Some flag")
)
setHandler handler
// addCommand listCommand
// addCommand installCommand
// addCommand removeCommand
}
description
にはヘルプメッセージに表示させるための簡単なコマンドの説明文を書く。inputs
には入力パラメータ、つまりコマンドラインオプションや位置引数の定義を書く。- ハンドラ関数は引数としてそれぞれの入力パラメータを受け取り、終了ステータスとしてint型の値を返す。
FSharp.SystemCommandLineの Input
型には、コマンドラインオプションを表す Option<'T>'
や位置引数を表す Argument<'T>
というメソッドが定義されている。
オプションや引数を定義するには、次のような式を inputs ( ... )
に追加する。
// -t または --text という名前の、後ろに文字列をとるオプション
Input.Option<string>([ "-t"; "--text" ], "Some text")
// -c または --count という名の、後ろに数値をとる必須のオプション
Input.OptionRequired<int>([ "-c"; "--count" ], "Some count")
// -f または --flag という名前の、後ろに値を取らないフラグ
Input.Option<bool>([ "-f"; "--flag" ], "Some flag")
// 複数の値をとる位置引数
Input.Argument<string array>("names", "Some names"),
基本的な使い方をまとめると次のようになる。
- 型引数に
string
やint
を指定すると後ろに値を取るオプションを定義できる。 - 型引数に
bool
を指定すると後ろに値を取らないフラグを定義できる。 - 型引数に
string array
のように配列を指定すると 複数の値を取るオプションや位置引数を定義できる。 - 型引数に
System.IO.FileInfo
型やSystem.IO.DirectoryInfo
型を指定して ファイルシステムのパスとして解釈させることもできる。 Option<'T>
の代わりにOptionRequired<'T>
を使うと必須のオプションにできる。Option<'T>
の代わりにOptionMaybe<'T>
(Maybeモナド)を使うと ハンドラの関数の引数をoption型で受け取ることができる。
Maybeモナド、F#のoption型についてはこちら。
inputs ( ... )
の定義を変更したら、型エラーになってしまうのでハンドラ関数の引数も併せて変更すること。
パッケージマネージャーに似た「list」「install」「remove」という名前を持つサブコマンドを実装してみる。
サブコマンドは command
コンピュテーション式で定義する。
let listCommand: CommandLine.Command =
let handler (count: int) =
printfn "Running listCommand!"
printfn "count: %A" count
0
command "list" {
description "Show a list of something"
inputs (Input.Option<int>([ "-c"; "--count" ], "Some count"))
setHandler handler
}
let installCommand: CommandLine.Command =
let handler (files: FileInfo array) =
printfn "files: %A" files
0
command "install" {
description "Install something"
inputs (Input.Argument<FileInfo array>("files", "File names"))
setHandler handler
}
let removeCommand: CommandLine.Command =
let handler (names: string array, force: bool) =
printfn "names: %A, force: %A" names force
0
command "remove" {
description "Remove something"
inputs (
Input.Argument<string array>("names", "Some names"),
Input.Option<bool>([ "-f"; "--force" ], "Remove something forcefully")
)
setHandler handler
}
それぞれのサブコマンドを定義したら、rootCommand
コンピュテーション式内の addCommand
でサブコマンドを追加する。
// ...
rootCommand argv {
description "Sample command line application"
setHandler handler
inputs (
// ...
)
addCommand listCommand
addCommand installCommand
addCommand removeCommand
}
System.CommandLineでアプリケーションを作成すると、ヘルプメッセージ、-?, -h, --help
オプション、バージョンオプションなどが自動で生成される。
プログラムを実行するには、dotnet run
コマンドを使用する。
引数は --
の後ろに指定したものがアプリケーションへ渡される。
$ dotnet run -- --help
Description:
Sample command line application
Usage:
SampleCommandLineApp [command] [options]
Options:
--version Show version information
-?, -h, --help Show help and usage information
-t, --text <text> (REQUIRED) Some text
-f, --flag Some flag
Commands:
list Show a list of something
install <files> Install something
remove <names> Remove something
$ dotnet run -- install ./path/to/file1 ./path/to/file2
files: [|./path/to/file1; ./path/to/file2|]
ヘルプメッセージや指定した引数が出力されればOK!
作ったツールを誰かに使ってもらう際、個別にランタイムをインストールしてもらうよりも単一の実行ファイルでそのまま実行できる形でビルドし配布した方が都合が良い場合も多い。
今回は、.NETのランタイムに依存しない、ネイティブライブラリを内包した単一の実行ファイルとしてビルドしてみる。
.NETのビルドオプションを変更するにはプロジェクトファイルSampleCommandLineApp.fsproj
を編集し、PropertyGroup
配下にエントリを追加する。
なお、「ReadyToRunコンパイル」を行うとアプリケーションの読み込み時に Just-In-Time (JIT)コンパイラで行う必要がある作業量を減らすことにより、起動時のパフォーマンスが向上する。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<!-- 自己完結型の実行ファイルとしてビルド -->
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<!-- 自己展開されるネイティブライブラリを実行ファイルに埋め込む -->
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<!-- 実行ファイルを圧縮する -->
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<!-- ReadyToRunコンパイルを行い、アプリケーションの起動時間と待機時間を向上させる) -->
<PublishReadyToRun>true</PublishReadyToRun>
<!-- ターゲットとなるランタイム識別子(RID) -->
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FSharp.SystemCommandLine" Version="0.17.0-beta4" />
</ItemGroup>
</Project>
詳しくは下記のドキュメントを参照。
dotnet publish コマンドを実行し、アプリケーションをビルドする。
$ dotnet publish -c Release
MSBuild version 17.4.8+6918b863a for .NET
Determining projects to restore...
All projects are up-to-date for restore.
SampleCommandLineApp -> /home/sheepla/ghq/github.com/sheepla/sandbox/fsharp/SampleCommandLineApp/bin/Release/net7.0/linux-x64/SampleCommandLineApp.dll
SampleCommandLineApp -> /home/sheepla/ghq/github.com/sheepla/sandbox/fsharp/SampleCommandLineApp/bin/Release/net7.0/linux-x64/publish/
ビルドが終わると、bin/Release/net<バージョン>/<ターゲット>/publish/
に実行ファイルが吐き出される。
ls -l bin/Release/net7.0/linux-x64/publish/SampleCommandLineApp
.rwxr-xr-x 41M sheepla 14 10月 22:36 bin/Release/net7.0/linux-x64/publish/SampleCommandLineApp
ターゲットとなるランタイムは dotnet publish
コマンドの -r, --runtime
オプションにて指定する。
ランタイムの識別子(RID)は下記から確認できる。
GitHubのAwesomeリストにライブラリ・エディタのプラグインなどがまとまっている。
F#楽しいよ!