1. ホーム
  2. programming-languages

[解決済み】戻り値型による関数のオーバーロード?

2022-04-28 19:19:19

質問

なぜ、より主流の静的型付け言語では、戻り値の型による関数/メソッドのオーバーロードをサポートしないのでしょうか? 思い当たらないのですが。 パラメータの型によるオーバーロードをサポートするのと同じぐらい便利で合理的だと思うのですが。 どうしてそんなに人気がないんだろう?

どうすれば解決するの?

他の人が言っているのとは逆に、戻り値の型によるオーバーロードは 可能であり いくつかの現代的な言語で行われています。 通常の反論は、次のようなコードで

int func();
string func();
int main() { func(); }

を選択した場合、どの func() が呼び出されます。 これはいくつかの方法で解決することができます。

  1. このような状況でどの関数が呼び出されるかを予測できる方法を用意する。
  2. このような状況が発生すると、必ずコンパイルエラーになります。 ただし、プログラマが曖昧さを解消できるような構文を持つこと、例えば int main() { (string)func(); } .
  3. 副作用を持たないこと。 副作用がなく、関数の戻り値を使うことがなければ、コンパイラはそもそもその関数を呼び出さないようにすることができるのです。

私が普段から使っている2つの言語( ab )は、戻り値の型によるオーバーロードを使用します。 Perl ハスケル . では、それらが何をするものなのかを説明しましょう。

Perl という基本的な区別があります。 スカラー リスト コンテキストを使用します(他にもありますが、ここでは2つということにしておきます)。 Perlのすべての組み込み関数は、その関数に依存して異なることを行うことができます。 コンテキスト で呼び出されます。 例えば join 演算子はリストコンテキストを(結合されるものに対して)強制するのに対して scalar 演算子はスカラーコンテキストを強制しますので、比較してみてください。

print join " ", localtime(); # printed "58 11 2 14 0 109 3 13 0" for me right now
print scalar localtime(); # printed "Wed Jan 14 02:12:44 2009" for me right now.

Perlのすべての演算子は、スカラーコンテキストで何かを行い、リストコンテキストで何かを行うが、図示されているように、それらは異なる場合がある。 (のようなランダムな演算子だけではありません。 localtime . もし、配列 @a をリストコンテキストで使用すると配列を返し、スカラコンテキストで使用すると要素数を返します。 つまり、たとえば print @a は要素を表示しますが print 0+@a はサイズを表示します)。 さらに、どの演算子も コンテキストを追加します。 + はスカラーコンテキストを強制する。 の各エントリは man perlfunc はこれを文書化したものである。 たとえば、次のようなエントリーの一部です。 glob EXPR :

リストコンテキストでは、(おそらく) のファイル名展開のリスト(空)。 の値は EXPR のような標準的な Unixシェル /bin/csh がそうです。での スカラーコンテキストでは、グロブは このようなファイル名の拡張を行い リストを使い切るとundefとなる。

さて、リストとスカラー・コンテキストの関係はどうなっているのだろうか。 さて。 man perlfunc とは

次の重要なルールを覚えておいてください。 を関係づけるルールはない。 における式の動作は、リスト コンテキストでの動作とスカラー のコンテキスト、またはその逆を実行します。 それは はまったく別のものです。 それぞれの 演算子や関数は どのような値が最適なのか スカラーで返すのが適切 コンテキストで使用されます。 ある演算子は の長さである。 はリストコンテキストで返されます。 いくつかの 演算子は、その最初の値を返します。 リスト いくつかの演算子は リスト内の最後の値。 いくつかの 演算子は、成功した回数を返します。 演算を行うことができます。 一般に、これらは 一貫性を求めるのでなければ

ということで、1つの関数を持っていて、最後に簡単な変換を行うという単純なものではありません。 実際、私が選んだのは localtime の例は、そのような理由からです。

このような動作をするのは、ビルトインだけではありません。 どんなユーザーでも、このような関数を wantarray で、リスト、スカラー、ボイドのコンテキストを区別することができます。 つまり、例えばvoidコンテキストで呼び出された場合は何もしないことにすることができる。

さて、これでは文句が出るかもしれません。 真の なぜなら、関数はひとつしかなく、その関数は呼び出されたコンテキストを知らされ、その情報に基づいて動作するからです。 しかし、これは明らかに同等です(Perlが通常のオーバーロードを文字通りに許可せず、関数がその引数を調べることができるのと類似しています)。 さらに、これはこの回答の冒頭で述べた曖昧な状況をうまく解決しています。 Perlはどのメソッドを呼び出すべきかわからないと文句を言うのではなく、ただ呼び出せばいいのです。 Perlがしなければならないのは、関数がどのようなコンテキストで呼び出されたかを把握することであり、それは常に可能なのです。

sub func {
    if( not defined wantarray ) {
        print "void\n";
    } elsif( wantarray ) {
        print "list\n";
    } else {
        print "scalar\n";
    }
}

func(); # prints "void"
() = func(); # prints "list"
0+func(); # prints "scalar"

(注:関数のことをPerlの演算子と言うことがあります。 これはこの議論には重要ではありません)。

ハスケル は、もう一方のアプローチ、つまり副作用を持たないというアプローチをとっています。 また、強力な型システムを持っているので、次のようなコードを書くことができます。

main = do n <- readLn
          print (sqrt n) -- note that this is aligned below the n, if you care to run this

このコードは、標準入力から浮動小数点数を読み取り、その平方根を表示します。 しかし、これのどこが驚くべきことなのだろうか? それは readLnreadLn :: Read a => IO a . この意味は Read (のインスタンスであるすべての型(正式には Read 型クラス)。 readLn が読めます。 どうしてHaskellは 私が浮動小数点数を読みたがっていると分かったのでしょう? それは sqrtsqrt :: Floating a => a -> a ということになります。 sqrt は浮動小数点数しか入力として受け付けないので、Haskellは私が欲しいものを推論してくれました。

Haskellが私の欲しいものを推論できないときはどうなるのでしょうか? いくつかの可能性があります。 戻り値をまったく使わない場合、Haskellはそもそも関数を呼び出さないだけです。 しかし、もし私が する を使用すると、Haskell は型を推測できないと文句を言います。

main = do n <- readLn
          print n
-- this program results in a compile-time error "Unresolved top-level overloading"

欲しい型を指定することで、曖昧さを解消できるんだ。

main = do n <- readLn
          print (n::Int)
-- this compiles (and does what I want)

とにかく、この議論が意味するのは、戻り値によるオーバーロードは可能であり、実行されているということで、質問の一部には答えがあります。

もうひとつは、なぜもっと多くの言語がそうしないのかということだ。 これについては、他の人に答えてもらうことにしよう。 しかし、いくつかコメントを。おそらく、引数の型によるオーバーロードよりも、混乱の機会が本当に大きいというのが主な理由でしょう。 また、個々の言語の根拠を見ることもできます。

Ada : "最も単純なオーバーロード解決ルールは、オーバーロードされた参照を解決するために、できるだけ広いコンテキストからのすべての情報を使用することであると思われるかもしれません。このルールは単純かもしれませんが、役に立ちません。このルールでは、読み手は恣意的に大きなテキストをスキャンし、恣意的に複雑な推論(上記(g)のような)を行う必要があるからです。私たちは、人間の読者やコンパイラが実行しなければならないタスクを明示し、このタスクを人間の読者にとって可能な限り自然にするものが、より良いルールであると信じています"。

C++(Bjarne Stroustrup著「The C++ Programming Language」の7.4.1項):"戻り値はオーバーロードの解決に考慮されません。 その理由は、個々の演算子や関数呼び出しの解決を、コンテキストに依存しないようにするためです。 考えてみてください。

float sqrt(float);
double sqrt(double);

void f(double da, float fla)
{
    float fl = sqrt(da);     // call sqrt(double)
    double d = sqrt(da); // call sqrt(double)
    fl = sqrt(fla);            // call sqrt(float)
    d = sqrt(fla);             // call sqrt(float)
}

もし戻り値の型を考慮した場合、もはや sqrt() を分離して、どの関数が呼び出されたかを判断することができます(比較のため、Haskellでは 暗黙の の変換を行います)。

Java ( Java言語仕様書9.4.1 ): "継承されたメソッドの1つは、他のすべての継承されたメソッドに対して戻り値の型が代入可能でなければなりません。 根拠はGoslingが"the Java Programming Language"で述べているはずです。 もしかしたら、どなたかコピーを持っているかもしれませんね。 要するに「最小驚嘆の原則」なんでしょうけど(笑)。 しかし、Javaの楽しい事実:JVMは 戻り値でオーバーロード! これは、例えば Scala にアクセスすることができます。 を経由して直接Java も、内部を弄ることで実現できます。

PS. 最後になりますが、実はC++ではトリックを使えば戻り値でオーバーロードすることが可能です。 目撃してください。

struct func {
    operator string() { return "1";}
    operator int() { return 2; }
};

int main( ) {
    int x    = func(); // calls int version
    string y = func(); // calls string version
    double d = func(); // calls int version
    cout << func() << endl; // calls int version
    func(); // calls neither
}