F#のシグネチャファイルについて
この記事は F# アドベントカレンダー 2020 の13日目の記事となります。
はじめに
数ある F# の機能の中でも使われることが比較的少ないシグネチャファイルについて簡単にまとめました。
シグネチャ(signature)
とは、型やメソッド、関数などの名前や引数の数・型の順序・戻り値などの組み合わせのことを指しています。
すなわち シグネチャファイル
とは型や関数などの定義をまとめているファイルということになります。
シグネチャファイルについてはソースコードから生成できることもあり、ソースコードをひとまず完成させてから出力している人も多いかもしれません。
昨今では Functional DDD への関心の高まりとともにドメインを記述する用途として .fsi
を利用することも多くなったことから、今までの自動生成方式ではなく手書きする機会が増えた人もいるかもしれません。
今回はそのシグネチャファイルの書き方などについて簡便に紹介していきます。
シグネチャファイル
自動生成方法
.NET Core / .NET X 時代となり、シグネチャファイルの生成方法が従来の fsc
の場合と dotnet
の場合とで指定方法が微妙に変化しました。
.NET Core以降でシグネチャファイルを生成するには .fsproj
に要素を追加する必要があります。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<OtherFlags>--sig:test.fsi</OtherFlags> <!-- この行を追加 -->
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>
</Project>
要素の中身については従来の --sig
と同様の指定で問題ありません。
--sig:<出力ファイル名>.fsi
手書き方法
手書きするには自分で fsiファイル
を作成し、中身を記述していく必要があります。
通常の F# とは微妙に違うキーワードを用いたりするので、これから各機能ごとに記述例を紹介していきます。
今回は以下のようなプロジェクト構成で fsiファイル
を手書きしていきます。
F# はファイルの並び順に意味があるため、ItemGroup 内のファイル順はこの通りにしなければなりません。
複数の fsiファイル
を作成することもあると思いますが、その場合は必ず fsファイル
の上に fsiファイル
が来るように記述してください。
モジュール
次のようなモジュールをシグネチャで表現してみます。
namespace Project
module Sample =
// <値束縛>
// int
let n = 100
// <リテラル>
// string
[<Literal>]
let s = "literal"
// <関数 >
// int -> int -> int
let f x y =
x * y
// <SRTP>
// ^T -> ^T -> ^a
let inline f' (x:^T) (y:^T) =
x + y
// <ジェネリクス>
// 'T -> string
let g (x:'T) =
x.ToString()
以下がシグニチャファイルとなります。
module Sample
val n : int
[<Literal()>]
val s : string = "literal"
val f : x:int -> y:int -> int
val inline f' :
x: ^T -> y: ^T -> ^a when ^T : (static member ( + ) : ^T * ^T -> ^a)
val g : x:'T -> string
パッと見てわかる違いとして大きく2点が挙げられます。
まず1点目として、let
ではなく val
を利用して定義がなされています。
これについてはあまり変化がないため、問題なく対応できると思います。
次に2点目として SRTP
の型制約を直接記述されています。
これはちょっと複雑です。今回の例は非常に簡単なシグネチャの関数を用意したため、型制約自体もそこまで複雑でないためあまり問題はなさそうですが、これが実務レベルの関数になった途端に手書きが困難になると思います。
とはいえ頑張って慣れて書けるようになる必要があるのですが、最近ではIDE上でこの手の型制約はマウスオーバーで表示してくれるのでそれを手書きするなどして徐々に慣れていくようにしましょう。
最悪、実際のコードを書いて自動生成させることもできるので、SRTPについては手書きをあきらめるというのも時間との相談になりますがアリだとは思います。
レコード
次のようなレコード型をシグネチャで表現してみます。
namespace Project
open System
type Point = { X: float; Y: float }
type Person = { Name: string; Age: uint } with
static member create name age =
if String.IsNullOrWhiteSpace name
then None
else Some { Name = name; Age = age }
member this.greet person =
printfn $"Hello, %s{person.Name}(%d{person.Age})!! I'm %s{this.Name}(%d{person.Age})."
以下がシグネチャファイルとなります。
namespace Project
type Point = { X: float; Y: float; }
type Person = { Name: string; Age: uint } with
static member create : name:string -> age:uint -> Person option
member greet : person:Person -> unit
レコード型の中身については、シグネチャファイル内で同じものをすでに定義していることがわかります。
また、メンバメソッドについては実装内容はなく、そのシグネチャが定義されているだけであることがわかります。
それ以外については、特筆すべき点がないほど F# の通常のコードと変わり映えしないのですんなりと受け入れることができると思います。
判別共用体
次のような判別共用体をシグネチャで表現してみます。
namespace Project
open System
type Shape =
| Rectangle of width : float * length : float
| Circle of radius : float
| Prism of width : float * float * height : float
[<Struct>]
type Age = Age of uint
module Age =
let unwrap (Age age) =
age
Age
のように判別共用体にも関わらず1つの要素しか持たない形を見慣れていない方も多いかと思います。
この辺は型安全なコーディングを促進させようとすると頻出する表現なので覚えておくと良いかもしれません。
また、Functional DDD を行う上で大変重要な役割を担っていたりもするので興味があるかたは Domain Modeling Made Functional を一読してみると良いかもしれません。
以下がシグネチャファイルとなります。
namespace Project
type Shape =
| Rectangle of width: float * length: float
| Circle of radius: float
| Prism of width: float * float * height: float
[<Struct>]
type Age = Age of uint
module Age =
val unwrap : Age -> uint
レコード型と同様に実際の実装ファイルと全く同じ表現となっていることがわかります。
気を付けるべきはやはりモジュールの中での関数の定義が val
で始まることくらいなものです。
インターフェイス・抽象クラス
次のようなインターフェイス・抽象クラスをシグネチャで表現してみます。
namespace Project
// インターフェース
type IGreetable =
abstract member greet : string -> unit
// 抽象クラス
[<AbstractClass>]
type Human() =
abstract Name : string with get
abstract Age : uint with get
以下がシグネチャファイルとなります。
namespace Project
type IGreetable =
abstract member greet : string -> unit
[<AbstractClass>]
type Human =
new : unit -> Human
abstract member Age : uint
abstract member Name : string
ほぼ実際の実装ファイルと変わりがないですが、抽象クラスのところに注意が必要です。
今回は引数が unit型
のデフォルトコンストラクタとしているので、new の定義が unit -> Human
となっています。
これが例えば (name:string, age:uint)
のタプルを受け取っていた場合は string * age -> Human
となるので注意しましょう。
クラス・構造体
次のようなクラス・構造体をシグネチャで表現してみます。
namespace Project
type Person (name: string, age: uint)=
member __.Name = name
member __.Age = age
member __.greet (person:Person) =
printfn $"Hello, %s{person.Name}!! I'm %s{name}"
[<Struct>]
type Point (x: float, y: float) =
member this.X = x
member this.Y = y
member this.move (x: float, y: float) =
Point(this.X + x, this.Y + y)
以下がシグネチャファイルとなります。
namespace Project
type Person =
new : name:string * age:uint -> Person
member Age : uint
member Name : string
member greet : person:Person -> unit
[<Struct>]
type Point =
new : x:float * y:float -> Point
member X : float
member Y : float
member move : x:float * y:float -> Point
抽象クラスで紹介したとおり、new
の部分については注意が必要です。
それ以外については、素朴な実装の場合あまり気にするところはありません。
問題となるのはインターフェイス実装や継承がある場合でしょう。
前項で作成したインターフェイスと抽象クラスを Person
に実装・継承した場合を見てみましょう。
namespace Project
type Person (name: string, age: uint)=
inherit Human()
override __.Name = name
override __.Age = age
interface IGreetable with
member __.greet name =
printfn $"Hello, %s{name}!! I'm %s{name}"
上記のような場合、シグネチャファイルは以下のような形となります。
namespace Project
type Person =
inherit Human
interface IGreetable
new : name:string * age:uint -> Person
override Name : string
override Age : uint
ここで注目すべき点が1箇所あります。
それは inherit / override
と interface
の部分です。
inherit
は クラスの継承
のことですが、ここではその内部で定義されているものをシグニチャファイル上で明示的に override
することが書かれています(ここでは Name と Age)。
反対に interface
の場合は、明記されていません(ここでは greet)。
このようにインターフェイスと抽象クラスでは実装/継承時の記述内容に差が生まれるので注意が必要です。
これは補足になりますが、Human
のコンストラクタに複数の引数が指定されている場合でも、その情報は記述する必要がありません。
おわりに
本当は全部の機能を書いておきたかったのですが、時間の都合上無理でした。
時間を見つけてこの記事に追記していこうと思います。
Discussion