2014年8月16日土曜日

F# のアセンブリ表現

F#は言語としてはとても素晴らしい設計をしていますが、コンパイルされたアセンブリは結構残念だったりします。F# + Entity Framewrok で ASP.NET WebAPI サーバ立てたら、返ってくる JSON がおかしいという記事を見かけたのでスコープについてまとめておこうと思います。 まずF#言語でアクセス制御するためにはpublic internal privateの3つのキーワードが用意されています。次にアセンブリでのアクセス制御についてはCorHdr.hに定義されています。関連部分を引用すると

// TypeDef/ExportedType attr bits, used by DefineTypeDef. typedef enum CorTypeAttr { // Use this mask to retrieve the type visibility information. tdVisibilityMask = 0x00000007, tdNotPublic = 0x00000000, // Class is not public scope. tdPublic = 0x00000001, // Class is public scope. tdNestedPublic = 0x00000002, // Class is nested with public visibility. tdNestedPrivate = 0x00000003, // Class is nested with private visibility. tdNestedFamily = 0x00000004, // Class is nested with family visibility. tdNestedAssembly = 0x00000005, // Class is nested with assembly visibility. tdNestedFamANDAssem = 0x00000006, // Class is nested with family and assembly visibility. tdNestedFamORAssem = 0x00000007, // Class is nested with family or assembly visibility. ... } CorTypeAttr; // MethodDef attr bits, Used by DefineMethod. typedef enum CorMethodAttr { // member access mask - Use this mask to retrieve accessibility information. mdMemberAccessMask = 0x0007, mdPrivateScope = 0x0000, // Member not referenceable. mdPrivate = 0x0001, // Accessible only by the parent type. mdFamANDAssem = 0x0002, // Accessible by sub-types only in this Assembly. mdAssem = 0x0003, // Accessibly by anyone in the Assembly. mdFamily = 0x0004, // Accessible only by type and sub-types. mdFamORAssem = 0x0005, // Accessibly by sub-types anywhere, plus anyone in assembly. mdPublic = 0x0006, // Accessibly by anyone who has visibility to this scope. // end member access mask ... } CorMethodAttr;
という感じです。 先にC#言語のアクセス制御の説明をしておくとわかりやすいでしょうか。C#言語ではpublic private protected internal protected internalの5種類です。これがアセンブリとどのような対応をしているかというと非常にわかりやすく
C#クラスアセンブリ表現
publictdPublic
internaltdNotPublic
C#メンバーアセンブリ表現
publicmdPublic
privatemdPrivate
protectedmdFamily
internalmdAssem
protected internalmdFamORAssem
となります。 さて本題のF#言語ですが、C#より種類が少ないはずなのに予想を超える複雑さをしています。まずクラスですが、
F#クラスアセンブリ表現C#相当クラス
publictdPublicpublic
privatetdNotPublicinternal
internaltdNotPublicinternal
と順当です。次にメンバーですが
F#メンバーアセンブリ表現C#相当メンバー
publicmdPublicpublic
privatemdAsseminternal
internalmdAsseminternal
…はい、F#上ではクラス外からアクセスできなくなるprivateですがC#のinternalに相当しアクセス可能となります。まだ簡単に見えますか? 恐ろしいのはここからです。F#にもC#にも自動実装プロパティがあります。プロパティの値を保持するためにコンパイラが自動的にフィールド(backing field)を用意しプロパティアクセッサを実装する機能です。当然、コンパイラによる自動的なフィールドである以上、プログラムからアクセスできるべきではありません。実際、C#ではmdPrivate、privateフィールドと同等です。さてF#はそうではありません。
F#メンバーアセンブリ表現C#相当メンバー
publicmdPublicpublic
privatemdAsseminternal
internalmdAsseminternal
backing fieldmdAsseminternal
…はい、privateメンバーと同様にC#のinternal fieldに相当します。まだ簡単に見えますか? 実はこの表はまだ不完全です。F#ではクラス自身のスコープがメンバーのスコープに影響を与えます。
F#クラスF#メンバーアセンブリ表現C#相当メンバー
publicpublicmdPublicpublic
privatemdAsseminternal
internalmdAsseminternal
backing fieldmdAsseminternal
privatepublicmdAsseminternal
privatemdAsseminternal
internalmdAsseminternal
backing fieldmdAsseminternal
internalpublicmdAsseminternal
privatemdAsseminternal
internalmdAsseminternal
backing fieldmdAsseminternal
…要するにpublicクラスのpublicメンバーだけがC#のpublic相当であり、それ以外はなんであれ全てC#のinternal相当です。ちなみにmutableでないフィールドにfdInitOnly(C#におけるreadonly)が付けられていないため、アセンブリ内からであれば書き換え可能という問題もあります。 これを踏まえて要望をまとめておきます。
  • backing fieldはmdPrivateにして欲しい
  • privateメンバーもmdPrivateにして欲しい
  • internal / privateクラスであってもpublicメンバーはmdPublicにして欲しい
といったところでしょうか。