locked
相互参照の解決方法 RRS feed

  • 質問

  • Visual Studio 2012 / F# 3.0を使用しています。

    例えばモジュールa、b、cが相互に参照しているときのコンパイル方法はどのようにすればよいのでしょうか?

    現在、それぞれのシグネチャファイルを用意し、a.fsi、b.fsi、c.fsi、a.fs、b.fs、c.fsの順に並べてコンパイルしています。しかしモジュールa、bに対して

    エラー FS0039: 名前空間またはモジュール 'B' が定義されていません
    エラー FS0039: 名前空間またはモジュール 'C' が定義されていません

    というエラーが出てしまいます。もちろんモジュールcを先にすればそれらのエラーは解決しますが、今度はモジュールcがエラーになります。この未定義エラー以外にはエラーが出ていないため手詰まりの状況です。

    本来、定義前に参照するためにシグネチャファイルが存在していたのだと思うのですが全く役に立っていません。

    2013年2月27日 23:04

回答

  • F# 3.0でモジュールの相互参照はサポートされていないと思います。

    代替案なのですが、cモジュールの値を引数で渡したりクラスを使ったりする方法はどうでしょうか。

    引数渡し

    type greeting = {
      hello : unit -> string
      bye: unit -> string }
    
    module a =
      let greet (greeting: greeting) = greeting.hello(), greeting.bye() 
    
    module b =
      let greet greeting = a.greet greeting
    
    module c =
      let hello() = "hello in c module"
      let bye() = "bye in c module"
      let greet() = b.greet {hello = hello; bye = bye}
    
    printfn "%A" (c.greet()) // ("hello in c module", "bye in c module")

    クラスを使った相互参照

    type a =
      static member greet() = c.hello(), c.bye()
    and b =
      static member greet() = a.greet()
    and c =
      static member hello() = "hello in c class"
      static member bye() = "bye in c class"
      static member greet() = b.greet()
    
    printfn "%A" (c.greet()) // ("hello in c class", "bye in c class")


    • 回答としてマーク 佐祐理 2013年2月28日 13:24
    2013年2月28日 0:37
  • サポートされていないというのは明確にどこか記述されているのでしょうか? もしあれば教えていただきたいです。

    公式のドキュメントなどには明確な記述はないと思います。

    公式なものではないですが、F#コミュニティの中心人物の一人であるTomas Petricekさんの回答は信頼できると思います(モジュールを相互参照させるちょっとトリッキーな?方法を紹介していますね)。

    エラー FS0039: フィールド、コンストラクター、またはメンバー 'b' が定義されていません
    が出ることからシグネチャファイルに型の定義が求められているわけで、矛盾を感じます。

    定義が求められているというわけではないと思います。b.fsで let a = {a.x = 1} としても同様のエラーが出ます(bの代わりに存在するはずのないxを参照)。実装ファイルに記述されたbの定義をシグネチャファイルが「隠している(hiding)」いるため見つからないと考えるのがいいのではと思います。Expert F# 3.0という書籍(Chapter 7)にはそのような表現がでてきます。


    • 編集済み toshihiro.nakamura 2013年2月28日 11:13 リンクを張りました
    • 回答としてマーク 佐祐理 2013年2月28日 13:24
    2013年2月28日 11:12

すべての返信

  • F# 3.0でモジュールの相互参照はサポートされていないと思います。

    代替案なのですが、cモジュールの値を引数で渡したりクラスを使ったりする方法はどうでしょうか。

    引数渡し

    type greeting = {
      hello : unit -> string
      bye: unit -> string }
    
    module a =
      let greet (greeting: greeting) = greeting.hello(), greeting.bye() 
    
    module b =
      let greet greeting = a.greet greeting
    
    module c =
      let hello() = "hello in c module"
      let bye() = "bye in c module"
      let greet() = b.greet {hello = hello; bye = bye}
    
    printfn "%A" (c.greet()) // ("hello in c module", "bye in c module")

    クラスを使った相互参照

    type a =
      static member greet() = c.hello(), c.bye()
    and b =
      static member greet() = a.greet()
    and c =
      static member hello() = "hello in c class"
      static member bye() = "bye in c class"
      static member greet() = b.greet()
    
    printfn "%A" (c.greet()) // ("hello in c class", "bye in c class")


    • 回答としてマーク 佐祐理 2013年2月28日 13:24
    2013年2月28日 0:37
  • 回答ありがとうございます。

    サポートされていないというのは明確にどこか記述されているのでしょうか? もしあれば教えていただきたいです。

    改めてシグネチャについて読み返したところ、シグネチャファイルはpublic / privateなどのアクセス制御をするものとしか説明がされておらず、C言語におけるヘッダーファイルの意味合いは含まれていなさそうにも読めました。

    とはいうものの、

    // a.fsi
    module A
    type a
    
    // a.fs
    module A
    type a = { b : int }
    
    // b.fs
    open A
    let a = { a.b = 1 }
    

    とやると、
    エラー FS0039: フィールド、コンストラクター、またはメンバー 'b' が定義されていません
    が出ることからシグネチャファイルに型の定義が求められているわけで、矛盾を感じます。
    (モジュールの相互参照ができないことから、モジュールについては実装ファイルを参照していると想像されるが、モジュール内に定義される型については上記例からシグネチャファイルを参照している…?)

    2013年2月28日 5:23
  • サポートされていないというのは明確にどこか記述されているのでしょうか? もしあれば教えていただきたいです。

    公式のドキュメントなどには明確な記述はないと思います。

    公式なものではないですが、F#コミュニティの中心人物の一人であるTomas Petricekさんの回答は信頼できると思います(モジュールを相互参照させるちょっとトリッキーな?方法を紹介していますね)。

    エラー FS0039: フィールド、コンストラクター、またはメンバー 'b' が定義されていません
    が出ることからシグネチャファイルに型の定義が求められているわけで、矛盾を感じます。

    定義が求められているというわけではないと思います。b.fsで let a = {a.x = 1} としても同様のエラーが出ます(bの代わりに存在するはずのないxを参照)。実装ファイルに記述されたbの定義をシグネチャファイルが「隠している(hiding)」いるため見つからないと考えるのがいいのではと思います。Expert F# 3.0という書籍(Chapter 7)にはそのような表現がでてきます。


    • 編集済み toshihiro.nakamura 2013年2月28日 11:13 リンクを張りました
    • 回答としてマーク 佐祐理 2013年2月28日 13:24
    2013年2月28日 11:12
  • ありがとうございます。確かにできないと言われていますね。

    また、シグネチャファイルでは、型がpublic宣言されると同時に、定義なし=すべてのメンバーがprivateとして定義される、という動作で理解しました。

    結局、シグネチャファイルはアクセス制御のみの機能で、それに派生してシグネチャファイルに記載しなかったものは隠ぺいされる、と。つまり、シグネチャファイルはメンバーの存在は表さず、メンバーの不在のみを表す存在ということでしょうか。
    腑に落ちないですが納得します。

    さて本題ですが、提案いただいた引数渡しやクラス、またインターフェースによる契約など方法はいろいろなのですが、相互に絡み過ぎてて私にはいずれの方法の適用も厳しく半ばあきらめることにしました。

    2013年2月28日 13:24
  • つまり、シグネチャファイルはメンバーの存在は表さず、メンバーの不在のみを表す存在ということでしょうか。

    この表現は私にはしっくりしませんでした。

    私は、モジュールのクライアントの観点を入れて、次のように理解しています。

    • シグネチャファイルは、クライアントから実装の詳細を隠蔽するもの、つまりクライアントにpublicなAPIを示すものである

    シグネチャファイルの利用はオプションですので、私はあまり利用していません。記述が冗長になって面倒だからです。しかし、アクセス制御はしたいので、モジュール、型、関数に対してアクセス修飾子を直接つける方法を採っています。アクセス修飾子はデフォルトでpublicなので、クライアントに隠したいものに対してinternalやprivateをつけています。

    2013年3月1日 0:21
  • コンパイル処理のかなり後半で、シグネチャファイルと実装ファイルの一致処理が行われ、シグネチャのみ存在するメンバーや関数パラメーターの一致しないものについてエラーが出ます。

    これ自体は本来あるべき動作ですが、この一致処理を前提とするならば、コンパイルの前半で今回問題となっているような参照の解決にシグネチャを使えばいいわけですが、実際にはシグネチャは使われていないわけですよね。
    それをもって「メンバーの存在は表さない」と表現しました。

    2013年3月1日 0:59
  • F# では前方参照ができません(同一クラス内の member 間ではできたりしますが)。また、F# のシグネチャファイル(*.fsi)は、C 言語のヘッダー ファイルにおける前方宣言としての役割を果たしません。

    シグネチャ ファイルは、実装ファイル(*.fs)に書かれたコードの型シグネチャを明示する役割と、ファイル外部に公開する要素を指定・制限する役割があります。

    というところで本題。

    nakamura さんが示されている方法もカッコイイのですが、どちらかというと pluggable な方法ですので、もっとかんたんに、相互に参照しあう部分・共通して参照される部分を抜き出して別のモジュールにまずは定義しておいて、それを module A、B、C から使うというのはどうでしょうか。

    module ABC =
       let a () = ...
       let b () = ...
       let c () = ...
    
    // 混乱しないなら、ここらで open ABC してもよさそう
    
    module A =
       let a () = ABC.a ()
    
    module B =
       let b () = ABC.b ()
    
    module C =
       let c () = ABC.c ()

    実際の実装用の module ABC を用意しておいて、module A、B、C を API 公開用とするイメージです。で、これらを1つの実装ファイルに定義し、シグネチャ ファイルには module ABC を記載しないことによって、ABC はファイル内スコープのモジュールとするのがよいかなと。

    ただ、この方法の弱点は mutable な値を扱いづらいところです。ABC に定義した mutable な値を A で公開するというようなことはできず、やるとすれば getter setter な関数を別途用意してやるとかになりそうな感じがします。

    • 編集済み いげ太 2013年3月1日 12:01
    2013年3月1日 3:30
  • なるほど、シグネチャファイルがコンパイラにどのように使われるのかという観点での表現だったのですね。

    シグネチャファイルが参照の解決に使われないというのは、その通りだと思います。

    書籍 Expert F# 3.0には次のような記述があります。

    The conformance between the signature type and the corresponding implementation is checked after an implementation has been fully processed

    このような実装になっている背景も合わせて載せてくれているとよかったのですけど、残念ながら見当たりませんでした。

    2013年3月1日 3:30
  • シグネチャ ファイルは、実装ファイル(*.fs)に書かれたコードの型シグネチャを明示する役割と、ファイル外部に公開する要素を指定・制限する役割があります。

    何度となく説明しているつもりですが、前半の役割はないと考えています。シグネチャファイルに関数パラメーターを明示しても、それは型推論には使われないようです。コンパイルの後半で、型推論結果とそれによって確定したパラメーターの型、それとシグネチャに書かれた型との比較が行われる、いわば答え合わせでしかありません。例えば、シグネチャには byte array と書いておき、実装ファイル側では型推論の結果 'a array となった場合、シグネチャが明示されるのではなく、コンパイルエラーになるだけです。

    本題について、アドバイスありがとうございます。
    実のところ、この3つのモジュールはそれぞれ公開メンバーを持つライブラリなので、ABCに定義を移すということができません。元々はOCamlで記述されていて、Obj.magicを使用して強引なキャストをしているコードなのです。
    ABCに定義を移すとなると、F# / .NETに於いては、すべての入出力に対して対応する型に1つ1つ正確に積み替える必要があると思います。かなり不毛なコードのが量産されてしまうため、このコードについてはあきらめることにしました。

    2013年3月1日 23:05
  • いわば答え合わせでしかありません。

    はい、その通りです。

    実のところ、この3つのモジュールはそれぞれ公開メンバーを持つライブラリなので、ABCに定義を移すということができません。元々はOCamlで記述されていて、Obj.magicを使用して強引なキャストをしているコードなのです。

    Obj.magic ってそんなことができるんですね。まさにマジック!

    定義を移すことはできないとのことなので、別の案として、静的クラス(というか、静的メンバーしか持たないクラス)をモジュール代わりに使ってみるのはいかがでしょう?

    type A =
        static member a with get() = "A"
        static member foo() = A.a + B.b + C.c
    and  B =
        static member b with get() = "B"
        static member bar() = B.b + C.c + A.a
    and  C =
        static member c with get() = "C"
        static member baz() = C.c + A.a + B.b

    こんな感じで type A = ... and B = ... and C ... と定義すれば、それぞれのメンバーが相互に参照可能です。

    # あきらめたと書かれてるところあれなんですが、いちおうこんな案もありますよということで。


    • 編集済み いげ太 2013年3月2日 3:01
    2013年3月2日 2:57
  • こんにちは。
    実のところ、この3つのモジュールはそれぞれ公開メンバーを持つライブラリなので、ABCに定義を移すということができません。元々はOCamlで記述されていて、Obj.magicを使用して強引なキャストをしているコードなのです。

    この部分が少し気になったのでコメントさせてください。
    Obj.magicの部分についてですが、

    • 「F#にObj.magicがないから(ある箇所を)移植できない」という意味
    • 「元のOCamlのソースはObj.magicで相互参照を解決している」という意味

    のどちらでしょう・・・?
    前方参照できないのはOCamlも同じだと思うので、元のソースはなぜ解決できていたのか疑問に思いました。
    (あと、「公開メンバーを持つライブラリ」というのが、どのようなものなのか想像が付きませんでした)


    F#でも暗黒面の力を使うとObj.magicと似たようなことができたりします。

    #nowarn "42";;
    module Obj =
        let inline magic (x:'a) : 'b = (# "" x : 'b #)
    
    
    type A(x:int) =
        member this.Method () = printfn "A : %d" x
    
    type B(x:int) =
        member this.Method () = printfn "B : %d" x
    
        
    [<EntryPoint>]
    let main _ =
        let x : int = Obj.magic 1.0
        printfn "%d" x
        
        let a = A(10)
        let b : B = Obj.magic a
        b.Method() // B : 10
    
        0

    本来コンパイラ内部で使われる機能であるため、この仕組みは仕様が明確にされていません。
    ですので、OCamlのObj.magicと全く同じ振る舞いをする保証はありません。

    2013年3月2日 15:20
  • 前方参照できないのはOCamlも同じだと思うので、元のソースはなぜ解決できていたのか疑問に思いました。
    (あと、「公開メンバーを持つライブラリ」というのが、どのようなものなのか想像が付きませんでした)

    OCamlについては実はよくわかっていません。しかし普通に使われているライブラリなのでコンパイルは通っているのでしょう。つまり前方参照できるのだと思います。(コンパイル時にオーバーロード解決の必要なC++言語ではなく)C言語のようなリンク時にメンバー確認する方法であれば実現可能ですし。

    その上で、件のOCamlコードでは相互にモジュール関数を呼び出しあっていて、なおかつ、双方に定義された同じメンバーを持つレコード型同士をObj.magicでキャストしています。

    F#の暗黒面は面白そうですが、何が何だか…キャストっぽい動作をしてくれるということはわかりました。

    2013年3月4日 22:40
  • このページの「Reference to undefined global mod」(ずっと下のほうです)にあるように、
    やはり普通のやり方ではできないように思えます。
    コンパイルオプション等があったりするのかな・・・。

    もし差し支えなければ、なんというライブラリか教えて頂けますか・・・?
    ちょっとソースコードを見てみたいなと思いまして。

    2013年3月9日 16:19
  • 詳しい方に見てもらうのが一番ですね。お手数おかけします。具体的にはXML-LIGHTというライブラリのXml、Dtd、XmlParserの3モジュールです。

    なおライブラリの利用をあきらめたというのは、別段このライブラリを使わなくともLinq to XMLを使えば同等以上のXML解析・構築が可能だからです。

    2013年3月10日 22:48
  • ソースを見てみましたが、やはり奇妙なプログラムだったので、
    同梱されているMakefileを確認してみました。OCamakeを使っているみたいですね。
    何やら複雑なコンパイルの仕方をしているようです。

    この特殊なコンパイルについて調べていたところ、Stack Overflowにずばりな投稿を発見してしまいました。
    http://stackoverflow.com/questions/5900813/ocaml-compile-multiple-files-circular-dependencies

    ですので、 佐祐理さんが混乱されたもの無理はないと思います・・・。

    F#にもFAKEと呼ばれるツールがありますが、同じようにコンパイルできるかどうかは分かりません。。。


    なおライブラリの利用をあきらめたというのは、別段このライブラリを使わなくともLinq to XMLを使えば同等以上のXML解析・構築が可能だからです。

    そのほうがいいと思います!笑
    機能的にも.NETのほうが充実しているでしょうしね。

    ちなみに、F#でこんなXML DSLを書いている方もいます。
    http://fssnip.net/U

    2013年3月11日 13:34