ほとんどの(?)プログラミング言語の関数が、任意の数の入力パラメーターをサポートするように設計されているが、戻り値は1つだけである理由はありますか?
ほとんどの言語では、その制限を「回避」することが可能です。アウトパラメーターを使用するか、ポインターを返すか、構造体/クラスを定義/返す。しかし、プログラミング言語が複数の戻り値をより「自然な」方法でサポートするように設計されていなかったのは奇妙に思われます。
これには説明がありますか?
一部の言語 Pythonなど は複数の戻り値をネイティブでサポートしますが、一部の言語 C#など はそれらのベースライブラリを介してそれらをサポートします。
しかし、一般に、それらをサポートする言語であっても、複数の戻り値はずさんなため、頻繁には使用されません。
戻り値の順序を間違えやすい
(password, username) = GetUsernameAndPassword()
(これと同じ理由で、多くの人は関数へのパラメータが多すぎるのを避けます。一部の人は、関数が同じ型の2つのパラメータを持つべきではないと言うまでさえ、それをとります!)
それらがかなり便利な場所は、ある関数からの複数の戻り値を別の関数への複数の入力パラメーターとして使用できる(Pythonなどの)言語の場合です。ただし、これがクラスを使用するよりも優れた設計であるユースケースはかなりスリムです。
関数は、計算を実行して結果を返す数学的な構造だからです。実際、いくつかのプログラミング言語の「内部」では、1つの入力と1つの出力にのみ焦点を当て、複数の入力は入力の薄いラッパーにすぎません。単一の値の出力が機能しない場合は、単一の凝集構造(またはタプル、またはMaybe
)が出力になります(ただし、「単一の」戻り値は多くの値で構成されます)。
プログラマーはout
パラメーターが、限られた一連のシナリオでのみ役立つ厄介な構成要素であることを発見したため、これは変更されていません。他の多くのものと同様に、ニーズ/需要がないため、サポートはありません。
数学では、 "well-defined" function は、特定の入力に対して出力が1つしかないものです(補足として、単一の入力関数しか持つことができませんが、意味的には複数の入力を使用して カレー )。
多値関数(たとえば、正の整数の平方根)の場合、コレクションまたは値のシーケンスを返すだけで十分です。
あなたが話している関数のタイプ(つまり、複数の値を返す関数、の異なるタイプ)については、あなたが思っているよりも少し異なって見えます:私は、out paramsの必要性/使用を、より良い設計またはより有用なデータ構造のための回避策として見ています。たとえば、outパラメータを使用する代わりに、*.TryParse(...)
メソッドがMaybe<T>
モナドを返したほうがよいでしょう。このコードをF#で考えてみましょう。
let s = "1"
match tryParse s with
| Some(i) -> // do whatever with i
| None -> // failed to parse
コンパイラ/ IDE /分析のサポートは、これらの構成に非常に適しています。これは、outパラメータの「必要性」の多くを解決します。完全に正直に言うと、これが解決策にならないような他の方法を手に負うことはできません。
他のシナリオ-私が思い出せないもの-単純なタプルで十分です。
関数が戻るときにアセンブリで使用されるパラダイムを見ると、すでに述べられていることに加えて、返されるオブジェクトへのポインターが特定のレジスターに残ります。それらが変数/複数のレジスターを使用した場合、呼び出し元の関数は、その関数がライブラリー内にある場合、戻り値を取得する場所を知りません。そのため、ライブラリへのリンクが困難になり、任意の数の戻り可能なポインタを設定するのではなく、それらを1つに設定しました。高レベルの言語には同じ言い訳はありません。
過去に複数の戻り値を使用した多くのユースケースは、現代の言語機能ではもはや必要ありません。エラーコードを返したいですか?例外をスローするか、Either<T, Throwable>
を返します。オプションの結果を返したいですか? Option<T>
を返します。いくつかのタイプのいずれかを返したいですか? Either<T1, T2>
またはタグ付き共用体を返します。
そして、本当に必要で複数の値を返す場合でも、現代の言語は通常、タプルまたはある種のデータ構造(リスト、配列、ディクショナリ)またはオブジェクト、および何らかの形式の構造化バインドまたはパターンマッチング。複数の値を1つの値にパッケージ化し、それを複数の値に再構成するのは簡単です。
しないが複数の値を返すことをサポートする言語の例をいくつか示します。新しい言語機能のコストを相殺するために、複数の戻り値のサポートを追加することで、それらを大幅に表現力豊かにする方法は、実際にはわかりません。
def foo; return 1, 2, 3 end
one, two, three = foo
one
# => 1
three
# => 3
def foo(): return 1, 2, 3
one, two, three = foo()
one
# >>> 1
three
# >>> 3
def foo = (1, 2, 3)
val (one, two, three) = foo
// => one: Int = 1
// => two: Int = 2
// => three: Int = 3
let foo = (1, 2, 3)
let (one, two, three) = foo
one
-- > 1
three
-- > 3
sub foo { 1, 2, 3 }
my ($one, $two, $three) = foo
$one
# > 1
$three
# > 3
単一の戻り値が非常に人気がある本当の理由は、非常に多くの言語で使用されている式です。 x + 1
のような式を使用できる任意の言語では、頭の中で式を断片に分割して評価し、各断片の値を決定するため、単一の戻り値の観点からすでに考えています。あなたはx
を見て、その値が3(たとえば)であることを決定し、1を見てからx + 1
を見て、それらをすべてまとめて全体の値が4であることを決定します。各構文式の一部には1つの値があり、他の数の値はありません。それは誰もが期待する表現の自然な意味です。関数が2つの値を返す場合でも、実際には2つの値の役割を果たしている1つの値を返します。2つの値を返す関数が、どういうわけか単一のコレクションにまとめられないという考えは奇妙です。
人々は、関数が複数の値を返すために必要となる代替のセマンティクスを扱いたくないのです。たとえば、- Forth のようなスタックベースの言語では、各関数がスタックのトップを変更し、入力をポップして出力をプッシュするだけなので、任意の数の戻り値を持つことができます。そのため、Forthには通常の言語のような表現がありません。
Perl は、通常、リストを返すと考えられている場合でも、関数が複数の値を返すように動作することがある別の言語です。ウェイリスト "interpolate" は、Perlで(1, foo(), 3)
のようなリストを提供します。これは、Perlを知らないほとんどの人が期待する3つの要素を持つ可能性がありますが、2つの要素、4つの要素のみを簡単に持つことができます。またはfoo()
に応じてさらに多くの要素。 Perlのリストはフラット化されているので、構文リストは常にリストのセマンティクスを持つわけではありません。それは単なる大きなリストの一部にすぎません。
関数に複数の値を返す別の方法は、任意の式が複数の値を持つことができ、各値が可能性を表す、代替の式セマンティクスを持つことです。もう一度x + 1
を取りますが、今回はx
に2つの値{3、4}があると想定すると、x + 1
の値は{4、5}になり、x + x
の値は{6、8}になります。または、1つの評価でx
に複数の値を使用できるかどうかに応じて、{6、7、8}のいずれかになります。 Prolog がクエリに複数の回答を与えるために使用するようなバックトラックを使用して実装される可能性があるそのような言語。
要するに、関数呼び出しは単一の構文単位であり、単一の構文単位は、私たち全員が知っており、愛している表現セマンティクスで単一の値を持っています。他のセマンティクスでは、Perl、Prolog、Forthなどの奇妙な方法で操作することになります。
この答え で示唆されているように、言語設計の伝統も影響しますが、ハードウェアサポートの問題です。
関数が戻ると、返されたオブジェクトへのポインタが特定のレジスタに残ります
Fortran、LISP、およびCOBOLの最初の3つの言語のうち、最初の言語は数学に基づいてモデル化されているため、単一の戻り値を使用していました。 2番目は、受け取ったのと同じ方法で、リストとして任意の数のパラメーターを返しました(単一のパラメーター(リストのアドレス)のみを渡して返したと主張することもできます)。 3番目は、ゼロまたは1つの値を返します。
これらの最初の言語は、その後に続く言語の設計に大きな影響を与えましたが、複数の値を返す唯一の言語であるLISPはそれほど人気がありませんでした。
Cが登場したとき、Cはそれ以前の言語に影響されていましたが、ハードウェアリソースの効率的な使用に大きな焦点を当て、C言語が行ったこととそれを実装したマシンコードとの密接な関連を維持しました。 「自動」変数と「レジスタ」変数などの最も古い機能の一部は、その設計哲学の結果です。
アセンブリ言語は、80年代まで広く普及し、ついに主流の開発から段階的に廃止され始めたことも指摘しておく必要があります。コンパイラーを作成して言語を作成した人々は、アセンブリに慣れ親しんでいて、ほとんどの場合、そこで最もうまく機能するものを守っていました。
この規範から逸脱したほとんどの言語はあまり人気がなかったため、言語デザイナー(もちろん、彼らが知っていることに触発されました)の決定に影響を与える強力な役割を果たすことはありませんでした。
それでは、アセンブリ言語を調べてみましょう。最初に、 6502 を見てみましょう。これは、Apple IIおよびVIC-20マイクロコンピュータで有名に使用されていた1975マイクロプロセッサです。これは、当時のメインフレームとミニコンピュータは、プログラミング言語の黎明期の20年前の最初のコンピュータと比較すると強力ですが、.
技術的な説明を見ると、5つのレジスタといくつかの1ビットフラグがあります。唯一の「完全な」レジスタは、プログラムカウンタ(PC)でした。このレジスタは、実行される次の命令を指します。その他のレジスタは、アキュムレータ(A)、2つの「インデックス」レジスタ(XおよびY)、およびスタックポインタ(SP)です。
サブルーチンを呼び出すと、PCはSPが指すメモリに配置され、SPをデクリメントします。サブルーチンからの戻りは逆の働きをします。スタック上で他の値をプッシュしたりプルしたりできますが、SPに対してメモリを参照することは難しいため、 リエントラントサブルーチン の記述は困難でした。当たり前のことですが、いつでもサブルーチンを呼び出すことは、このアーキテクチャではそれほど一般的ではありませんでした。多くの場合、個別の「スタック」が作成され、パラメーターとサブルーチンの戻りアドレスが別々に保たれます。
6502に影響を与えたプロセッサ 68 を見ると、SPと同じ幅の追加のレジスタであるインデックスレジスタ(IX)があり、SPから値を受け取ることができます。
マシンでは、再入可能サブルーチンの呼び出しは、スタック上のパラメーターのプッシュ、PCのプッシュ、PCの新しいアドレスへの変更で構成され、その後、サブルーチンはそのローカル変数をスタックにプッシュします。ローカル変数とパラメーターの数は既知であるため、それらのアドレス指定はスタックを基準にして行うことができます。たとえば、2つのパラメータを受け取り、2つのローカル変数を持つ関数は、次のようになります。
SP + 8: param 2
SP + 6: param 1
SP + 4: return address
SP + 2: local 2
SP + 0: local 1
すべての一時スペースがスタック上にあるため、何度でも呼び出すことができます。
TRS-80およびCP/Mベースのマイクロコンピューターのホストで使用される 808 は、スタックにSPをプッシュし、次に間接レジスタHLにポップします。
これは物事を実装するための非常に一般的な方法であり、より最近のプロセッサでさらに多くのサポートが得られます。ベースポインタを使用すると、簡単に戻る前にすべてのローカル変数をダンプできます。
問題は、どのようにして何を返すかです?初期のプロセッサレジスタはそれほど多くなく、アドレス指定するメモリの一部を見つけるためにそれらのいくつかを使用する必要がしばしばありました。スタック上に物を戻すのは複雑です。すべてをポップし、PCを保存し、戻されたパラメーター(その間に保存されますか?)をプッシュしてから、もう一度PCをプッシュして戻る必要があります。
したがって、通常行われたことは戻り値用に1つのレジスタを予約することでした。呼び出しコードは、戻り値が特定のレジスターにあることを知っていました。これは、保存または使用できるようになるまで保持する必要があります。
複数の戻り値を許可する言語を見てみましょう:Forth。 Forthが行うことは、個別の戻りスタック(RP)とデータスタック(SP)を保持することです。これにより、関数はすべてのパラメーターをポップし、戻り値をスタックに残しました。リターンスタックは独立しているため、邪魔になりませんでした。
コンピューターでの最初の6か月の経験でアセンブリー言語とフォースを学んだ人として、複数の戻り値は私には完全に正常に見えます。整数の除算とを除算するForthの/mod
などの演算子は、明らかなようです。一方で、初期の経験がCマインドだった人がその概念を奇妙に感じる方法を簡単に見ることができます。
数学に関しては、まあ、数学のクラスの関数にたどり着く前にコンピュータをプログラミングしていた。 CSとプログラミング言語のセクション全体がありますが、数学の影響を受けますが、ここでも影響を受けないセクション全体があります。
したがって、数学が初期の言語設計に影響を与えた要素、ハードウェアの制約が簡単に実装できるものを決定した要素、および一般的な言語がハードウェアの進化に影響を与えた要素の合流点があります(LISPマシンとForthマシンのプロセッサは、このプロセスのロードキルでした)。
私が知っている関数型言語は、タプルを使用して複数の値を簡単に返すことができます(動的に型付けされた言語では、リストを使用することもできます)。タプルは他の言語でもサポートされています。
f :: Int -> (Int, Int)
f x = (x - 1, x + 1)
// Even C++ have tuples - see Boost.Graph for use
std::pair<int, int> f(int x) {
return std::make_pair(x - 1, x + 1);
}
上記の例では、f
は2つの整数を返す関数です。
同様に、ML、Haskell、F#などもデータ構造を返すことができます(ほとんどの言語ではポインターが低レベルすぎます)。私はそのような制限がある現代のGP言語について聞いたことがありません:
data MyValue = MyValue Int Int
g :: Int -> MyValue
g x = MyValue (x - 1, x + 1)
最後に、out
パラメータは、関数型言語でもIORef
によってエミュレートできます。ほとんどの言語でout変数がネイティブでサポートされていない理由はいくつかあります。
不明確なセマンティクス:次の関数は0または1を出力しますか?私は0を出力する言語と1を出力する言語を知っています。両方に利点があります(パフォーマンスの点でも、プログラマーのメンタルモデルと一致する点でも)。
int x;
int f(out int y) {
x = 0;
y = 1;
printf("%d\n", x);
}
f(out x);
非ローカライズされた効果:上記の例のように、長いチェーンがあり、最も内側の関数がグローバルな状態に影響を与えることがわかります。一般に、関数の要件が何であるか、および変更が合法であるかどうかを推論することは困難になります。ほとんどの最新のパラダイムは、効果をローカライズ(OOPでカプセル化)するか、副作用(関数型プログラミング)を排除しようとするため、これらのパラダイムと競合します。
冗長になる:タプルがある場合、out
パラメーターの機能の99%と慣用的な使用の100%があります。ミックスにポインターを追加すると、残りの1%がカバーされます。
タプル、クラス、またはout
パラメータを使用して複数の値を返すことができない1つの言語の名前付けに問題があります(ほとんどの場合、これらのメソッドの2つ以上が許可されます)。
式のせいだと思います。たとえば、(a + b[i]) * c
などです。
式は「単一の」値で構成されます。したがって、上記の4つの変数の代わりに、特異値を返す関数を式で直接使用できます。マルチ出力関数は、少なくともsomewhat式では不格好です。
私はこれがtheの特異な戻り値について特別であると個人的に感じています。これを回避するには、式で使用する複数の戻り値を指定する構文を追加しますが、簡潔で誰もが使い慣れている古き良き数学表記よりも不格好です。
構文は少し複雑になりますが、実装レベルで許可しないという正当な理由はありません。他のいくつかの応答とは対照的に、複数の値を返し、可能な場合は、より明確で効率的なコードを導きます。 X and a Y、または「成功した」ブール値andの有用な値を返すことを希望した頻度を数えることはできません。
関数がサポートされているほとんどの言語では、そのタイプの変数を使用できる場所であればどこでも関数呼び出しを使用できます。
x = n + sqrt(y);
関数が複数の値を返す場合、これは機能しません。 pythonなどの動的に型付けされた言語ではこれを行うことができますが、ほとんどの場合、途中でタプルを使って行うのが賢明なことをうまく実行できない限り、ランタイムエラーがスローされます方程式の。
Harveyの答えを基にしたいだけです。私はもともとニューステックサイト(arstechnica)でこの質問を見つけましたが、この質問の核心に本当に答えていて、他のすべての回答(Harveyのものを除く)に欠けていると思う驚くべき説明を見つけました。
関数からの単一の戻りの起源は、マシンコードにあります。マシンコードレベルでは、関数はA(アキュムレータ)レジスタの値を返すことができます。その他の戻り値はスタック上にあります。
2つの戻り値をサポートする言語は、1つを返すマシンコードとしてコンパイルし、2番目をスタックに配置します。つまり、2番目の戻り値はいずれにしても、outパラメータとして返されます。
それは、なぜ割り当てが一度に1つの変数であるのかを尋ねるようなものです。たとえば、a、b = 1、2を許可する言語を使用できます。しかし、最終的には、マシンコードレベルでa = 1に続いてb = 2になります。
コードがコンパイルされて実行されているときに実際に何が起こるかについて、プログラミング言語の構成要素にいくつかの類似点を持たせることには、いくつかの理論的根拠があります。