より主流の静的型付け言語が、戻り値の型による関数/メソッドのオーバーロードをサポートしないのはなぜですか? 私は何も考えられません。パラメーターの型によるオーバーロードのサポートと同じくらい有用で合理的です。あまり人気がないのはどうしてですか?
14 に答える
他の人が言っていることに反して、戻り型によるオーバーロードは可能であり、いくつかの現代言語によって行われます。通常の反対意見は、次のようなコードの場合です。
int func();
string func();
int main() { func(); }
func()
どちらが呼び出されているかわかりません。これはいくつかの方法で解決できます。
- そのような状況でどの関数が呼び出されるかを決定するための予測可能な方法があります。
- このような状況が発生するたびに、コンパイル時エラーになります。ただし、プログラマーが曖昧さを解消できる構文を用意して
int main() { (string)func(); }
ください。 - 副作用はありません。副作用がなく、関数の戻り値を使用しない場合、コンパイラーは最初から関数を呼び出さないようにすることができます。
私が定期的に( ab )使用している2つの言語、PerlとHaskellはリターンタイプによるオーバーロードです。彼らが何をしているのか説明させてください。
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
。スカラーコンテキストでは、globはそのようなファイル名展開を繰り返し、リストが使い果たされるとundefを返します。
さて、リストとスカラーコンテキストの関係は何ですか?まあ、man perlfunc
言う
次の重要なルールを覚えておいてください。リストコンテキストでの式の動作をスカラーコンテキストでの動作に関連付けるルールはありません。その逆も同様です。それは2つのまったく異なることをするかもしれません。各演算子と関数は、スカラーコンテキストで返すのが最も適切な値の種類を決定します。一部の演算子は、リストコンテキストで返されるはずのリストの長さを返します。一部の演算子は、リストの最初の値を返します。一部の演算子は、リストの最後の値を返します。一部の演算子は、成功した操作のカウントを返します。一般に、一貫性が必要でない限り、それらは必要なことを実行します。
したがって、単一の関数を持つという単純な問題ではなく、最後に単純な変換を行います。localtime
実際、私はその理由で例を選びました。
この動作をするのはビルトインだけではありません。すべてのユーザーは、を使用してこのような関数を定義wantarray
できます。これにより、リスト、スカラー、およびvoidコンテキストを区別できます。したがって、たとえば、無効なコンテキストで呼び出されている場合は、何もしないことを決定できます。
さて、これは戻り値による真のオーバーロードではないという不満を言うかもしれません。これは、呼び出されたコンテキストに通知され、その情報に基づいて動作する関数が1つしかないためです。ただし、これは明らかに同等です(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演算子と言うことがあります。これは、この説明にとって重要ではありません。)
Haskellは別のアプローチを採用しています。つまり、副作用がないようにすることです。また、強い型のシステムがあるため、次のようなコードを記述できます。
main = do n <- readLn
print (sqrt n) -- note that this is aligned below the n, if you care to run this
このコードは、標準入力から浮動小数点数を読み取り、その平方根を出力します。しかし、これについて驚くべきことは何ですか?さて、のタイプはreadLn
ですreadLn :: Read a => IO a
。これが意味するのは、Read
(正式には、Read
型クラスのインスタンスであるすべての型)である可能性のあるすべての型について、readLn
それを読み取ることができるということです。Haskellは、私が浮動小数点数を読みたいと思ったことをどうやって知りましたか?さて、のタイプsqrt
はsqrt :: 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)
とにかく、この議論全体が意味するのは、戻り値によるオーバーロードが可能であり、実行されるということです。これは、あなたの質問の一部に答えます。
あなたの質問の他の部分は、なぜもっと多くの言語がそれをしないのかということです。他の人に答えさせます。ただし、いくつかのコメント:主な理由は、おそらく、引数タイプによるオーバーロードよりも、混乱の可能性が本当に大きいためです。また、個々の言語からの理論的根拠を見ることができます。
エイダ:「最も単純な過負荷解決ルールは、すべて(可能な限り広いコンテキストからのすべての情報)を使用して過負荷参照を解決することであるように見えるかもしれません。このルールは単純かもしれませんが、役に立ちません。人間の読者が必要です。任意に大きなテキストをスキャンし、任意に複雑な推論を行う(上記の(g)など)。より良いルールは、人間の読者またはコンパイラが実行する必要のあるタスクを明示的にし、このタスクを作成するルールであると考えています。人間の読者にとって可能な限り自然なことです。」
C ++(BjarneStroustrupの「C++プログラミング言語」のサブセクション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つは、他のすべての継承されたメソッドに対してreturn-type-substitutableである必要があります。そうでない場合、コンパイル時エラーが発生します。」(はい、これが論理的根拠を与えないことを私は知っています。論理的根拠は「Javaプログラミング言語」のGoslingによって与えられると確信しています。誰かがコピーを持っているかもしれませんか?それは本質的に「驚き最小の原則」であるに違いありません。 )しかし、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
}
関数がリターンタイプによってオーバーロードされ、これら2つのオーバーロードが発生した場合
int func();
string func();
このような呼び出しを見たときに、コンパイラがこれら2つの関数のどちらを呼び出すかを判断する方法はありません。
void main()
{
func();
}
このため、言語設計者は戻り値のオーバーロードを許可しないことがよくあります。
ただし、一部の言語(MSILなど)では、戻り型によるオーバーロードが許可されています。もちろん、これらも上記の問題に直面していますが、回避策があり、ドキュメントを参照する必要があります。
そのような言語では、次のことをどのように解決しますか。
f(g(x))
オーバーロードがあり、オーバーロードがあり、オーバーロードがあった場合f
は? ある種の曖昧さ回避ツールが必要になります。void f(int)
void f(string)
g
int g(int)
string g(int)
これが必要な状況では、関数に新しい名前を選択することでより適切に機能すると思います。
別の非常によく似た質問(だまされていますか?)からC++ 固有の回答を盗むには:
関数の戻り値の型がオーバーロードの解決に関与しないのは、Stroustrup (他の C++ アーキテクトからの入力があると思います) がオーバーロードの解決を「コンテキストに依存しない」ことを望んでいたからです。「C++ プログラミング言語、第 3 版」の 7.4.1 - 「オーバーロードと戻り値の型」を参照してください。
その理由は、個々の演算子または関数呼び出しの解決をコンテキストに依存しないようにするためです。
彼らは、結果がどのように使用されたかではなく、オーバーロードがどのように呼び出されたかのみに基づくことを望んでいました (使用された場合)。実際、多くの関数は結果を使用せずに呼び出されるか、結果がより大きな式の一部として使用されます。彼らがこれを決定したとき、私が確信している1つの要因は、戻り値の型が解決の一部である場合、複雑なルールで解決する必要があるか、コンパイラにスローさせる必要があるオーバーロードされた関数への多くの呼び出しがあるということでした.呼び出しがあいまいだったというエラー。
そして、ご承知のとおり、C++ のオーバーロードの解決は現状のままでは十分に複雑です...
haskell では、関数のオーバーロードがなくても可能です。Haskell は型クラスを使用します。プログラムでは、次のことがわかります。
class Example a where
example :: Integer -> a
instance Example Integer where -- example is now implemented for Integer
example :: Integer -> Integer
example i = i * 10
関数のオーバーロード自体はあまり一般的ではありません。私が見たほとんどの言語は C++ で、おそらく Java や C# です。すべての動的言語では、次の省略形です。
define example:i
↑i type route:
Integer = [↑i & 0xff]
String = [↑i upper]
def example(i):
if isinstance(i, int):
return i & 0xff
elif isinstance(i, str):
return i.upper()
したがって、それにはあまり意味がありません。ほとんどの人は、言語を使用する場所ごとに 1 行を削除するのに言語が役立つかどうかには関心がありません。
パターン マッチングは、関数のオーバーロードにいくぶん似ています。ごく少数のプログラムでしか役に立たず、ほとんどの言語で実装するのが難しいため、一般的ではありません。
言語に実装するための、より実装しやすい機能が他にも無限にあることがわかります。
- 動的型付け
- リスト、辞書、Unicode 文字列の内部サポート
- 最適化 (JIT、型推論、コンパイル)
- 統合された導入ツール
- ライブラリ サポート
- 地域支援・集いの場
- 豊富な標準ライブラリ
- 良い構文
- 評価印刷ループの読み取り
- リフレクティブ プログラミングのサポート
良い答えです!特に A.Rex の回答は非常に詳細で有益です。彼が指摘するように、C++はコンパイル時にユーザー指定の型変換演算子を考慮しますlhs = func();
(func は実際には構造体の名前です)。私の回避策は少し異なります-より良いというわけではなく、単に異なるだけです(ただし、基本的な考え方は同じです)。
書きたかったのに…
template <typename T> inline T func() { abort(); return T(); }
template <> inline int func()
{ <<special code for int>> }
template <> inline double func()
{ <<special code for double>> }
.. etc, then ..
int x = func(); // ambiguous!
int x = func<int>(); // *also* ambiguous!? you're just being difficult, g++!
パラメータ化された構造体 (T = 戻り値の型) を使用するソリューションになりました。
template <typename T>
struct func
{
operator T()
{ abort(); return T(); }
};
// explicit specializations for supported types
// (any code that includes this header can add more!)
template <> inline
func<int>::operator int()
{ <<special code for int>> }
template <> inline
func<double>::operator double()
{ <<special code for double>> }
.. etc, then ..
int x = func<int>(); // this is OK!
double d = func<double>(); // also OK :)
このソリューションの利点は、これらのテンプレート定義を含むすべてのコードで、より多くの型に特殊化を追加できることです。また、必要に応じて構造体の部分的な特殊化を行うこともできます。たとえば、ポインター型の特別な処理が必要な場合:
template <typename T>
struct func<T*>
{
operator T*()
{ <<special handling for T*>> }
};
マイナスとして、私の解決策では書くことができませんint x = func();
。あなたは書く必要がありますint x = func<int>();
。コンパイラに型変換演算子を調べて調べるように依頼するのではなく、戻り値の型が何であるかを明示的に言う必要があります。「私の」ソリューションと A.Rex のソリューションはどちらも、この C++ ジレンマに取り組む方法のパレート最適な最前線に属していると言えます :)
この過負荷機能は、少し違った見方をすれば、管理するのは難しくありません。次のことを考慮してください。
public Integer | String f(int choice){
if(choice==1){
return new string();
}else{
return new Integer();
}}
言語がオーバーロードを返す場合、パラメーターのオーバーロードは許可されますが、重複は許可されません。これにより、次の問題が解決されます。
main (){
f(x)
}
選択できる f(int の選択) は 1 つしかないためです。
.NET では、一般的な結果から目的の出力を示すために 1 つのパラメーターを使用してから、期待どおりの結果を得るために変換を行うことがあります。
C#
public enum FooReturnType{
IntType,
StringType,
WeaType
}
class Wea {
public override string ToString()
{
return "Wea class";
}
}
public static object Foo(FooReturnType type){
object result = null;
if (type == FooReturnType.IntType)
{
/*Int related actions*/
result = 1;
}
else if (type == FooReturnType.StringType)
{
/*String related actions*/
result = "Some important text";
}
else if (type == FooReturnType.WeaType)
{
/*Wea related actions*/
result = new Wea();
}
return result;
}
static void Main(string[] args)
{
Console.WriteLine("Expecting Int from Foo: " + Foo(FooReturnType.IntType));
Console.WriteLine("Expecting String from Foo: " + Foo(FooReturnType.StringType));
Console.WriteLine("Expecting Wea from Foo: " + Foo(FooReturnType.WeaType));
Console.Read();
}
たぶん、この例も役立つかもしれません:
C++
#include <iostream>
enum class FooReturnType{ //Only C++11
IntType,
StringType,
WeaType
}_FooReturnType;
class Wea{
public:
const char* ToString(){
return "Wea class";
}
};
void* Foo(FooReturnType type){
void* result = 0;
if (type == FooReturnType::IntType) //Only C++11
{
/*Int related actions*/
result = (void*)1;
}
else if (type == FooReturnType::StringType) //Only C++11
{
/*String related actions*/
result = (void*)"Some important text";
}
else if (type == FooReturnType::WeaType) //Only C++11
{
/*Wea related actions*/
result = (void*)new Wea();
}
return result;
}
int main(int argc, char* argv[])
{
int intReturn = (int)Foo(FooReturnType::IntType);
const char* stringReturn = (const char*)Foo(FooReturnType::StringType);
Wea *someWea = static_cast<Wea*>(Foo(FooReturnType::WeaType));
std::cout << "Expecting Int from Foo: " << intReturn << std::endl;
std::cout << "Expecting String from Foo: " << stringReturn << std::endl;
std::cout << "Expecting Wea from Foo: " << someWea->ToString() << std::endl;
delete someWea; // Don't leak oil!
return 0;
}
記録のために、Octaveは、戻り要素がスカラーか配列かによって、異なる結果を許容します。
x = min ([1, 3, 0, 2, 0])
⇒ x = 0
[x, ix] = min ([1, 3, 0, 2, 0])
⇒ x = 0
ix = 3 (item index)
特異値分解も参照してください。
これは、最新の C++ 定義のギャップだと思います...なぜですか?
int func();
double func();
// example 1. → defined
int i = func();
// example 2. → defined
double d = func();
// example 3. → NOT defined. error
void main()
{
func();
}
C++ コンパイラが例 "3" でエラーをスローできず、例 "1+2" のコードを受け入れることができないのはなぜですか??
ほとんどの静的言語は、問題を解決するジェネリックもサポートするようになりました。前に述べたように、パラメーター diff がなければ、どれを呼び出すべきかを知る方法はありません。したがって、これを行いたい場合は、ジェネリックを使用して、1 日と呼んでください。