F#のシグネチャファイルについて

8 min読了の目安(約7200字TECH技術記事

この記事は F# アドベントカレンダー 2020 の13日目の記事となります。

はじめに

数ある F# の機能の中でも使われることが比較的少ないシグネチャファイルについて簡単にまとめました。
シグネチャ(signature) とは、型やメソッド、関数などの名前や引数の数・型の順序・戻り値などの組み合わせのことを指しています。
すなわち シグネチャファイル とは型や関数などの定義をまとめているファイルということになります。

シグネチャファイルについてはソースコードから生成できることもあり、ソースコードをひとまず完成させてから出力している人も多いかもしれません。
昨今では Functional DDD への関心の高まりとともにドメインを記述する用途として .fsi を利用することも多くなったことから、今までの自動生成方式ではなく手書きする機会が増えた人もいるかもしれません。

今回はそのシグネチャファイルの書き方などについて簡便に紹介していきます。

シグネチャファイル

自動生成方法

.NET Core / .NET X 時代となり、シグネチャファイルの生成方法が従来の fsc の場合と dotnet の場合とで指定方法が微妙に変化しました。
.NET Core以降でシグネチャファイルを生成するには .fsproj に要素を追加する必要があります。

Sample.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ファイル を手書きしていきます。

image.png

F# はファイルの並び順に意味があるため、ItemGroup 内のファイル順はこの通りにしなければなりません。
複数の fsiファイル を作成することもあると思いますが、その場合は必ず fsファイル の上に fsiファイル が来るように記述してください。

モジュール

次のようなモジュールをシグネチャで表現してみます。

Sample.fs
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()

以下がシグニチャファイルとなります。

Sample.fsi
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上でこの手の型制約はマウスオーバーで表示してくれるのでそれを手書きするなどして徐々に慣れていくようにしましょう。

image.png

最悪、実際のコードを書いて自動生成させることもできるので、SRTPについては手書きをあきらめるというのも時間との相談になりますがアリだとは思います。

レコード

次のようなレコード型をシグネチャで表現してみます。

Sample.fs
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})."

以下がシグネチャファイルとなります。

Sample.fsi
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# の通常のコードと変わり映えしないのですんなりと受け入れることができると思います。

判別共用体

次のような判別共用体をシグネチャで表現してみます。

Sample.fs
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 を一読してみると良いかもしれません。

以下がシグネチャファイルとなります。

Sample.fsi
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 で始まることくらいなものです。

インターフェイス・抽象クラス

次のようなインターフェイス・抽象クラスをシグネチャで表現してみます。

Sample.fs
namespace Project
  // インターフェース
  type IGreetable =
    abstract member greet : string -> unit

  // 抽象クラス
  [<AbstractClass>]
  type Human() =
    abstract Name : string with get
    abstract Age : uint with get

以下がシグネチャファイルとなります。

Sample.fsi
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 となるので注意しましょう。

クラス・構造体

次のようなクラス・構造体をシグネチャで表現してみます。

Sample.fs
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)

以下がシグネチャファイルとなります。

Sample.fsi
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 に実装・継承した場合を見てみましょう。

Sample.fs
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}"

上記のような場合、シグネチャファイルは以下のような形となります。

Sample.fsi
namespace Project
  type Person =
      inherit Human
      interface IGreetable
      new : name:string * age:uint -> Person
      override Name : string
      override Age : uint

ここで注目すべき点が1箇所あります。

それは inherit / overrideinterface の部分です。
inheritクラスの継承 のことですが、ここではその内部で定義されているものをシグニチャファイル上で明示的に override することが書かれています(ここでは Name と Age)。
反対に interface の場合は、明記されていません(ここでは greet)。
このようにインターフェイスと抽象クラスでは実装/継承時の記述内容に差が生まれるので注意が必要です。

これは補足になりますが、Human のコンストラクタに複数の引数が指定されている場合でも、その情報は記述する必要がありません。

おわりに

本当は全部の機能を書いておきたかったのですが、時間の都合上無理でした。
時間を見つけてこの記事に追記していこうと思います。