文字列比較の罠
突然のF# Advent Calendar 2014 - connpassの24日目です。
F# のdisりネタを紹介したいと思います! こんなふざけた挙動のくせに調子乗らないで下さい!
文字列比較の挙動
F# の文字列比較には2種類の方法があります。演算子(<, <=, >, >=)
を使う方法とCompareTo
メソッドを使う方法です。
"B" < "b" "B".CompareTo("b") < 0
それぞれの結果がどうなるか分かりますか?
> "B" < "b";; val it : bool = true > "B".CompareTo("b") < 0;; val it : bool = false
結果が違いますね! ていうかこんな挙動誰も知らないよ! 同じだと思うだろ普通!!! 職場でも、@pocketberserker が嵌ってくれました。
C# ではどうなってんだよ?と思ったのですが、そもそも C# では演算子を使った文字列比較は出来ないんですよねー。
また、List.sort
とEnumerable.OrderBy
の結果も違います。
> List.sort [ "a"; "b"; "A"; "B"; ];; val it : string list = ["A"; "B"; "a"; "b"] > [ "a"; "b"; "A"; "B"; ].OrderBy(fun x -> x);; val it : IOrderedEnumerable<string> = seq ["a"; "A"; "b"; "B"]
そして、OCamlの実行結果です。
# "B" < "b";; - : bool = true # List.sort compare [ "a"; "b"; "A"; "B"; ];; - : string list = ["A"; "B"; "a"; "b"]
どうやらOCamlの挙動と合わせたようです。確かにF#にはML互換のコンパイラオプションもありますが、そのオプションを使った時だけにして欲しかったなぁ……。
ListModule
を使ってEnumerable.OrderBy
と挙動を同じにしたい場合は、次のようにします。
> [ "a"; "b"; "A"; "B"; ] |> List.sortWith (fun x y -> x.CompareTo(y));; val it : string list = ["a"; "A"; "b"; "B"]
ちなみに、こう書くと型推論に失敗しますが、これはまた別のdisりネタ。
List.sortWith (fun x y -> x.CompareTo(y)) [ "a"; "b"; "A"; "B"; ]
ドキュメント
この件のドキュメントは見つけられませんでした。誰か見つけたら教えてー。
実装
社内チャットで相談したところ、実装を見ようということになりました。俺が F# のリポジトリを clone している間に、@bleis がソースを読んで、@k_disposeがILを読んで、サクッと解決されてしまいました。「ILに追い付いてきた」とか競ってました。なんなんコイツら。
(<)演算子
"B" < "b"
で出力されるILをILSpyで見てみると、
LanguagePrimitives.HashCompare.GenericLessThanIntrinsic<string>("B", "b");
と表示されます。CompareTo
を呼んでいないぞ? つぎはGenericLessThanIntrinsic
の実装を見てみましょう。ファイルはprim-types.fs:1239辺りです。
let GenericLessThanIntrinsic (x:'T) (y:'T) = try (# "clt" (GenericComparisonWithComparerIntrinsic fsComparer x y) 0 : bool #) with | e when System.Runtime.CompilerServices.RuntimeHelpers.Equals(e, NaNException) -> false
こんな感じにどんどん潜って行くと、次のコードに突き当たります。ファイルはprim-types.fs:957辺りです。
let rec GenericCompare (comp:GenericComparer) (xobj:obj,yobj:obj) = (*if objEq xobj yobj then 0 else *) //System.Console.WriteLine("xobj = {0}, yobj = {1}, NaNs = {2}, PERMode.mode = {3}", [| xobj; yobj; box PER_NAN_PAIR_DETECTED; box PERMode.mode |]) match xobj,yobj with | null,null -> 0 | null,_ -> -1 | _,null -> 1 #if INVARIANT_CULTURE_STRING_COMPARISON // Use invariant culture comparison for strings | (:? string as x),(:? string as y) -> System.String.Compare(x, y, false, CultureInfo.InvariantCulture) #else // Use Ordinal comparison for strings | (:? string as x),(:? string as y) -> System.String.CompareOrdinal(x, y) #endif
なにやらINVARIANT_CULTURE_STRING_COMPARISON
フラグでString.Compare
を呼ぶかString.CompareOrdinal
を呼ぶか分岐しています?
ここのILは、
internal static int GenericCompare(LanguagePrimitives.HashCompare.GenericComparer comp, object xobj, object yobj) { //... if (text != null) { string text2 = yobj as string; if (text2 != null) { string strB = text2; string strA = text; return string.CompareOrdinal(strA, strB); }
String.CompareOrdinal
を呼んでいますね。つまり、FSharp.Core
はINVARIANT_CULTURE_STRING_COMPARISON
を指定せずにビルドされているようです。
String.CompareOrdinalは、文字に対応するChar
オブジェクトの数値を比較するメソッドです。
B
は0x42
、b
は0x62
だから、"B" < "b"
の結果がtrue
になるんですね。
List.sort
List.sort
の場合は、同じようにして、次のコードにたどり着きます。ファイルはprim-types.fs:1269辺りです。
let inline GenericComparisonFast<'T> (x:'T) (y:'T) : int = GenericComparisonIntrinsic x y //... when 'T : string = #if INVARIANT_CULTURE_STRING_COMPARISON // NOTE: we don't have to null check here because System.String.Compare // gives reliable results on null values. System.String.Compare((# "" x : string #) ,(# "" y : string #), false, CultureInfo.InvariantCulture) #else // NOTE: we don't have to null check here because System.String.CompareOrdinal // gives reliable results on null values. System.String.CompareOrdinal((# "" x : string #) ,(# "" y : string #)) #endif
ここでもINVARIANT_CULTURE_STRING_COMPARISON
が出てきました。
まとめ
文字列は比較する方法により結果が変わるので、コードレビューで意図通りの比較になっているか確認する等して、対策しましょう。
文字列比較用の演算子を定義するのも考えられますが、次の理由でやめたほうがいいと思います。
それでも俺はString.Compare
を使って欲しいんだ!って人は、INVARIANT_CULTURE_STRING_COMPARISON
を指定してFSharp.Core
をビルドすればいいみたいです。
AutoOpenAttributeについて
F# Advent Calendar 2014 - connpassの2日目です。
今回は、AutoOpenを紹介します。
AutoOpenとは、モジュールを自動でopenする仕組みです。主に言語やフレームワークでよく使うモジュールを提供するときに使います。主にライブラリ開発者に対する機能で、利用者は使うことは無いと思います。 実際に使うときは、AutoOpenAttributeを使用します。
しかし、あまりにも開くモジュールが多すぎると利用者はビックリしてしまうので、ほどほどにしましょう。
MSDNを見ても何の事か分からないので、F#の言語仕様書を見てみましょう。「Special Attributes and Types」という章にAutoOpenAttributeの記述があります。
When applied to an assembly and given a string argument, causes the namespace or module to be opened automatically when the assembly is referenced. When applied to a module without a string argument, causes the module to be opened automatically when the enclosing namespace or module is opened.
「アセンブリと、モジュールに対してAutoOpenAttributeを適用できる」と書いてあります。それぞれの使い方を説明します。
モジュールに適用する場合
よく使うパターンです。モジュールに対してAutoOpenをつけると、親のモジュール、名前空間がopenされた時に、自動でopenされます。
使い方はこんな感じ。
module A = let f x = x [<AutoOpen>] module B = let g x = x * 2 open A let x = g 1 (* Bモジュールの関数を使える *)
アセンブリに適用する場合
アセンブリにAutoOpenを付け、開きたいモジュール名を指定します。AutoOpenが付与されたアセンブリをプロジェクトの参照に追加しただけで、自動でopenされるようになります。
使い方はこんな感じ。例ではモジュール定義のすぐ下に書いてますが、実際に使うときはAssemblyInfo.fs
などに書くといいと思います。
module A = let f x = x module B = let g x = x * 2 [<assembly: AutoOpen("A.B")>] do ()
こちらの機能は、強力すぎるので使う機会が少ないと思いますし、使わないほうがいいと思います。
F# では、FSharp.Coreがこの方法を使っています。いつも使えるListモジュール等は、アセンブリに対するAutoOpenで提供されています。 ソースコードはこの辺りです。 特別扱いをするのではなく、言語の機能を使って提供しています。素晴らしいですね!
つまり、AutoOpenを外したFSharp.Coreを作れば、明示的にopenしないとListモジュールなどは使えないってことでしょうか? 既存のソースコードはコンパイルエラーになるはずです(試してないけど)。仕事場で隣の人が席を外している間に悪戯してやりましょう。
F# のネタリスト
知りたいこといっぱいあるので詳しい教えて下さい!
- F#に便利なVSアドインまとめとか読みたいです
- 判別共用体の名前付きフィールドについて知りたいです
- F#3.1のprintfのパフォーマンスについて知りたいです
- C#で定義されたクラスをF#から扱う時のtipsについて知りたいです
- F#3.1の新機能詳細(まとめじゃない)について知りたいです
- 判別共用体の値がタプルかそうじゃないかでの挙動の違いについて知りたいです
- useはコンピュテーション式の中でも使えるんだよってことを知りたいです
- リフレクションでOptionを扱う時の注意点を知りたいです
- アクティブパターンのアクティブっぷりが知りたいです
- CompilationRepresentationとかAutoOpenとかRequireQualifiedAccessとかF#コンパイラに関わる属性について知りたいです
- CustomEqualityAttribute, CustomComparisonAttributeとか知りたいです
- abstractとdefaultについて知りたいです
- リフレクションを多用しててobjを使わざるを得ないけど、なるべくobjを減らす方法を知りたいです
- FAKEというF#製ビルドツールについて知りたいです
- F#最速(予定)のFsAttoparsecというパーサコンビネータについて知りたいです
- F#コンパイラよりVSインテリセンスの方が賢い場合があるってことについて知りたいです
- TypeProviderについて知りたいです
- クラス・インターフェースを使った時の、キャストが必要なとき、必要ないときを知りたいです
- doを使えばスコープが作れるってことを知りたいです
- レコードとかクラスを作ったら、それを扱うモジュールと関数を作っておくと型推論が捗るよって知りたいです
- 型に別名つけたら一緒にシグネチャファイルも用意するとライブラリ利用者が喜ぶよって知りたいです
- よく使うけど標準ライブラリに定義されてないから、作っておくと良い関数知りたいです
- 入力補完のためならクラス使用を辞さない覚悟について知りたいです
- クラスのコンストラクタ呼び出し時にプロパティも同時に初期化する方法知りたいです→クラスのコンストラクタ呼び出し時にプロパティも同時に初期化する - ぐるぐる~
- Genericなクラス、関数でも<_>って書いとけば型推論が頑張ってくれること知りたいです
- openした結果モジュールが被ってしまった場合でも両方のモジュールの関数を扱えること知りたいです → id:rika0618 が教えてくれるそうです!
- Printfモジュールの使い方知りたいです
- F#のstaticイニシャライザが呼ばれたり呼ばれなかったりについて知りたいです
- C#ライブラリからnullが渡ってきた時の対処法知りたいです
- F#最強の今秋リリースするらしい柿っていうテスティングフレームワークについて知りたいです
- 型拡張について知りたいです
- 8.12 Type Extensions
- 静的に解決された型パラメータについて知りたいです
- Statically Resolved Type Parameters
- 6.4.8 Member Constraint Invocation Expressions
- 文字列を比較するとき注意しないといけないこと知りたいです
F# のデバッグ方法 #fsharp
F#のデバッグには、Visual Studioによる制限と、F#特有の、二種類の辛さが(C#に比べて)あります。本記事では、Visual Studio上での制限や、F#特有のデバッグ方法を紹介します。
また、Visual Studioしか知らないので、他の環境のデバッグについては、得意な人がきっと教えてくれます?
Visual Studio上での制限
Visual StudioでのF#のデバッグはMSDN(F# のデバッグ)に記事があります。特に注目したいのが、3つ目の制限の、
デバッガーは F# 式を認識しません。 F# のデバッグ中にデバッガー ウィンドウまたはダイアログ ボックスに式を入力するには、式を C# の構文に変換する必要があります。 F# の式を C# に変換するときは、C# では等価を示す比較演算子として == を使用しますが、F# では単一の = を使用することに注意してください。
です。ウォッチ式や、イミディエイトウィンドウでも同様に、C#の式が必要になります。F#の構文をC#へ変換する方法を紹介します。
モジュールに定義された関数、ラムダ式の呼び出し
関数呼び出しはメソッド呼び出しと同様の方法で呼び出します。let f x y z =...
という関数は、F#だとf 1 2 3
で呼び出せますが、C#へはf(1, 2, 3)
と変換します。カリー化されている場合でも、3引数のメソッドとして扱えます。実は、F#の関数は、C#からはメソッドとして見えるんです。
ローカル関数、ラムダ式の呼び出し
ローカル関数は関数値Microsoft.FSharp.Core.OptimizedClosures.FSharpFunc
として扱われます。f 1 2 3
と呼び出したい場合は、f.Invoke(1).Invoke(2).Invoke(3)
と変換します。
また、モジュールに定義された関数でも、関数値の場合は、Invoke
メソッドで呼び出さなければなりません。
見分ける方法は、関数の場合はシグネチャが'a -> 'b -> 'c
となっており、関数値の場合は、('a -> 'b -> 'b)
と括弧で囲われます。
演算子の呼び出し
演算子は、たとえば、let (++) x y = ...
という演算子は、3++4
とは書けません。op_PlusPlus(3, 4)
と変換します。
MSDN(演算子のオーバーロード (F#))の定義にしたがって、演算子をメソッド名に変換します。そして、メソッドとして呼び出します。
変換例を幾つか挙げます。
演算子 | メソッド |
---|---|
+ |
op_Addition |
++ |
op_PlusPlus |
+- |
op_PlusMinus |
また、型に定義された演算子は、staticメソッドとして呼び出します。
ブレークポイント
基本的にどこでも置けます。ラムダ式の中や、コンピュテーション式の中でも大丈夫です。 ただし、ラムダ式の中から外側の変数の値は見えないようです。コンピュテーション式もメソッド呼び出しとラムダ式に展開されるので、同様です。
シャドーイングした場合は、全てシャドーイング後の値になります。
コンピュテーション式の中でステップインすると、C#のクエリ式のように、コンピュテーション式の実装の方に迷い込んでしまうので、注意しましょう。
パイプライン演算子の間にブレークポイント
F#ではパイプライン演算子|>
をよく使います。ただし、簡単には途中の値を覗けません。覗く方法を幾つか紹介します。
式を変形する
簡単ですが面倒くさい方法です。計算過程を変数に束縛します。
f x |> g
上記式を次のように変換します。デバッグが終わったら元に戻します。
let a = f x g a
この方法は確かに値を覗けるのですが、修正が大きすぎてミスりそうです。ですが、もう少し修正が小さい方法があります。
ID関数
let bp x = x
のような関数を定義し、デバッグ時に見たいところに差し込みます。ブレークポイントはbp
関数の中に置いておきます。
先ほどと同じように、デバッグが終わったら削除します。
let bp x = x (* ここにブレークポイント *) f x |> bp |> g
もしくは、直接ラムダ式を書き、ラムダ式の中にブレークポイントを置きます。
f x |> (fun x -> x) |> g
パイプライン演算子を上書きする
今までに挙げた方法では、コードを書き換える必要がありました。The trace-mode pipeline operator | F# Snippetsで見つけたのですが、パイプライン演算子を上書きし、演算子の中にブレークポイントを置く方法があります。
let (|>) x f = let result = f x in x (* ここにブレークポイント *) f x |> g
そして、演算子を#ifディレクティブで囲っておけば、リリース時には元のパイプライン演算子に置き換わります。
ただし、この方法では、スコープ内の全てのパイプライン演算子を上書きしてしまうので、止めたい位置で止めるのが難しくなります。
どの方法も一長一短なので、好きな方法を使えばいいと思います。
Visual F# Power Tools の紹介
F# Meetup in Tokyo Part 1 - connpassの発表ではスライドを使わなかったので、紹介したサイトのURLをメモしようと思います。
Visual F# Power Tools
Visual F# Power Toolsとは、Visual Studioの貧弱なF# エディタに機能を追加してくれるツールです。Visual Studio 2012、2013のProfessional以上で利用できます。Expressでは使えません!!!
ユーザーガイドに機能一覧があります。サイト自体は英語ですが、各機能のページに動画gifがあるので、雰囲気でわかると思います。
ダウンロードとインストールは、VisualStudioGallery http://visualstudiogallery.msdn.microsoft.com/136b942e-9f2c-4c0b-8bac-86d774189cffから、
または、Visual Studioの拡張機能から行います。
不具合の報告はgithub https://github.com/fsprojects/VisualFSharpPowerToolsに、
要望はuser voice http://vfpt.uservoice.com/に
報告しましょう。
user voiceは気になる機能をvoteするだけでもOKです。職場ではたまに組織voteが行われています。
また、F#言語自体のuser voice https://fslang.uservoice.com/もあります。
F# Compiler Services
Visual F# Power ToolsはF# Compiler Servicesを使って実現されています。C#でいうRoslynです。
ライブラリ開発者のための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を付ける
どれも面倒くさいですね!ていうかバグなんじゃねーのこれ?
恐ろしい standalone オプション
この記事は、F# Advent Calendar 2012 14日目の記事です。
3ヶ月前の話しをします。僕はF#を使ってプロダクトに対するテストツールを作っていました。Webアプリケーションに対してリクエストを投げ、レスポンスやDBをチェックし、期待通りの動作をしているかチェックするツールです。
本番環境へデプロイして、テストツールを実行して動作を確認できれば、リリース完了となります。ですが、作業目前でとんでもない事実を知るのでありました。
テストツールを実行するマシンには! F#が!インストールされていない!インストールできない!!!
開発環境でしか動作しないツールを作るとかやる気あるんですかああああ
FShap.Core.dllを持っていけばいいじゃない
F#は実はただのライブラリなので、というかDSLなので、FShap.Core.dllを実行ファイルと同じディレクトリにコピーしておけば、F#で作ったプログラムを実行できます。
ですが注意が必要で、F# 2.0 のアセンブリには.NET Framework 2.0 用と .NET Framework 4.0 用の2種類があります。片方だけ利用している場合問題ないのですが、混在している場合は、コンパイルはできるのに実行できません。どちらかに統一してください。
F# コンパイラによるサポートもあります
FShap.Core.dllをコピーすればいいという話を先にしましたが、standaloneというコンパイラオプションを使えば、依存するアセンブリを成果物に含めることができます。このオプションをコンパイル時にしていすれば、F#がインストールされていない環境でもプログラムを実行できてしまいます。ナイスなオプションですね。
さらに、先ほど挙げた、.NET Framework 2.0用、4.0用のアセンブリが混在している場合はコンパイルが失敗します。コンパイルが成功したものは実行できる!
VS2012 での standalone オプションの指定方法
プロジェクトのプロパティを開き、ビルド > その他のフラグ に、--standalone と指定します。
standalone オプション利用時の注意点
standaloneオプションは出力形式がDLL、EXEのどちらでも利用できるのですが、EXEを出力するプロジェクトに指定するのが無難です。FShap.Core.dll等を取り込むため、standaloneオプションを指定して作成されたDLLと、FShap.Coreの両方を参照した場合、競合が起こりコンパイルができません。参照設定からFShap.Coreを除けば解決できるのですが、複数のプロジェクトを扱っていると面倒ですよね。
まとめ
この記事は僕が実際にハマった問題を元にしています。
standaloneオプションにより F# の無い環境でも実行できるようになりました。 妥協してF#を使わない理由がひとつ減ったと思います。