ライブラリ開発者のためのObsoleteAttribute

F# Advent Calendar 2013の10日目です。

ライブラリを開発していると、どうしても古くなり使って欲しくないAPIが出てくると思います。古いAPIは思い切って削除しても良いのですが、互換性を考えてしばらくの間、残しておくのがライブラリ利用者にとって親切でしょう。

ただ、残しておいても利用者は気づきにくいので、.NET FrameworkではObsoleteAttributeを使って、コンパイラの警告として利用者に知らせることができます。

簡単な使い方は、MSDNを参照してください。

http://msdn.microsoft.com/ja-jp/library/system.obsoleteattribute.aspx

しかしこのObsoleteAttribute、F# では期待通りに動かないパターンがいくつかあります。 動くパターンと動かないパターンをそれぞれコードで説明しようと思います。期待と反する動作をする場合にはコード中のコメントに"!"を付けました。

レコード

レコード定義にObsoleteAttributeをつけた例です。 レコードの場合、型推論された時、Obsoleteなレコード型の警告が出ません。ただし、let f x = x.Aのパターンで推論された場合は警告が出ます。中途半端ですね。

[<Obsolete>]
type OldRecord = {
  A: int
}

(* レコード生成 *)
let old = { A = 1 }             (* 警告なし! *)
let old2: OldRecord = { A = 1 } (* 警告 *)
let old3 = { OldRecord.A = 1 }  (* 警告なし! *)

(* 関数定義 *)
let f x = x.A                   (* 警告 *)
let g { A = a } = a             (* 警告なし! *)
let h (x: OldRecord) = x        (* 警告 *)

次は、レコードのメンバに対してObsoleteAttributeを付けた例です。Obsoleteなメンバの場合は期待通り動作するようです。

type OldRecord = {
  [<Obsolete>]
  OldMember: int
  A: int
}

(* レコード生成 *)
let old = { OldMember = 1; A = 1 }                       (* 警告 *)
let old2: OldRecord = { OldMember = 1; A = 1 }           (* 警告 *)
let old3 = { OldRecord.OldMember = 1; OldRecord.A = 1 }  (* 警告 *)

(* 関数定義 *)
let f x = x.A               (* 警告なし *)
let g x = x.OldMember       (* 警告 *)
let h { A = a } = a         (* 警告なし *)
let i { OldMember = o } = o (* 警告 *)
let j (x: OldRecord) = x    (* 警告なし *)

判別共用体

判別共用体については、どのパターンも期待通り動作します。

判別共用体の定義にObsoleteAttributeを付けた例です。

[<Obsolete>]
type OldUnion =
  | A
  | B of int

let a = A    (* 警告 *)
let b = B(1) (* 警告 *)

let g (x: OldUnion) = x                 (* 警告 *)
let g = function  A -> "a" | B _ -> "b" (* 警告 *)

次は、判別共用体のケースに対してObsoleteAttributeを付けた例です。

type OldUnion =
  | [<Obsolete>] A
  | B of int

let a = A    (* 警告 *)
let b = B(1) (* 警告なし *)

let g (x: OldUnion) = x                (* 警告なし *)
let h = function A -> "a" | B _ -> "b" (* 警告 *)
let i = function B _ -> "b" | _ -> "a" (* 警告なし *)

クラス

クラス定義に対して、ObsoleteAttributeを付けた例です。 変数の型を推論した場合に警告が出ません。また、F# では型とコンストラクタ呼び出しに同じ名前を使いますが、別物のようです。

[<Obsolete>]
type OldClass(a: int) =
  member val A = a with get, set
  
let a = OldClass(1)           (* 警告なし! *)
let b: OldClass = OldClass(1) (* 警告 *)

let f (x: OldClass) = x.A     (* 警告 *)

実はこの、変数の型を推論した場合に警告が出ない問題は、C#の、変数のvar宣言にも存在します。

次は、メンバに対して、ObsoleteAttributeを付けた例です。期待通りに動作します。

type OldClass(a: int, b: int) =
  [<Obsolete>]
  member val OldMember = a with get, set
  member val B = b with get, set

let a = OldClass(1, 1).OldMember (* 警告 *)
let b = OldClass(1, 1).B         (* 警告 *)

次は、コンストラクタに対して、OsboleteAttributeを付けた例です。期待通りに動作します。

type OldClass [<Obsolete>](a: int, b: int) =
  [<Obsolete>]
  new () = OldClass(1, 1)  (* 警告 *)
  new (a) = OldClass(a, 2) (* 警告 *)

  member val A = a with get, set
  member val B = b with get, set

let a = OldClass(1, 2) (* 警告 *)
let b = OldClass()     (* 警告 *)
let c = OldClass(2)    (* 警告なし *)

モジュール

モジュールの定義に対して、ObsoleteAttributeを付けた例です。期待通りに動作します。

[<Obsolete>]
module OldModule =
  let x = 1

let a = OldModule.x (* 警告 *)
open OldModule      (* 警告 *)
let b = a           (* 警告なし *)

次は、モジュール内の変数に対してObsoleteAttributeを付けた例です。期待通りに動作します。

module OldModule =
  [<Obsolete>]
  let oldVal = 1

let a = OldModule.oldVal (* 警告 *)
open OldModule           (* 警告なし *)
let b = oldVal           (* 警告 *)

次は、AutoOpenAttributeのついたモジュールに対して、ObsoleteAttributeを付けた例です。 親モジュールをopenし、間接的にopenされた場合は警告が出ません。しかし、明示的にopenした場合は警告が出ます。

module Module =
  [<Obsolete>]
  [<AutoOpen>]
  module OldModule2 =
    let x = 2

open Module            (* 警告なし! *)
let a = x              (* 警告なし *)
open Module.OldModule  (* 警告 *)

まとめ

さまざまな例を挙げ、ObsoleteAttributeの動作を見てきました。 結論としては、「型推論、AutoOpenといった暗黙的に使用される場合は警告が出ない」と言えそうです。 せっかくObsoleteAttributeを付けたのにライブラリ利用者に気づいてもらえなくて悲しいです。

この問題に対して、次の対策がとれそうです。

  • レコードをObsoleteにする場合は全てのメンバーにもObsoleteAttributeを付ける
  • クラスをObsoleteにする場合はコンストラクタにもObsoleteAttributeを付ける
  • AutoOpenされるモジュールをObsoleteにする場合はモジュール内の要素全てにObsoleteAttributeを付ける

どれも面倒くさいですね!ていうかバグなんじゃねーのこれ?