ネストされたオプション、期待される、および構成

Andrzej は CTAD とネストされたオプションの問題について書き、Barry は比較とネストされたオプションの問題について書きました。

両方の問題の共通点は何ですか?

ネストされたオプション。

それでは、それらについて話しましょう:実際にはどういう意味ですか?

07 :18 ないかもしれない

オブジェクトを返せるかどうかわからない関数を扱っているとします。With 23 モデル化しやすい:

/// Does a database lookup, returns `std::nullopt` if it wasn't found.
template <typename T>
std::optional<T> lookup(const database& db, std::string name);

/// Calls the function if the condition is `true` and returns the result,
/// `std::nullopt` if the condition was false.
template <typename T>
std::optional<T> call_if(bool condition, std::function<T()> func);

3040 のいずれか」を意味します その意味では 59 のようなものです。 .これは、「62 のいずれか」も意味します。 まだ 72 より便利なインターフェイスを備えているため、推奨されます。

ただし、どちらも単に「または何もない」を意味することに注意してください。「または見つからない」または「または関数が呼び出されなかった」ではありません。 86 固有のセマンティックな意味はありません。意味はコンテキストによって提供されます:

auto value = lookup<my_type>(db, "foo");
if (!value)
    // optional is empty, this means the value wasn't there

…

auto result = call_if(condition, some_function);
if (!result)
    // optional is empty, this means the condition was false

ここで、空のオプションは、そのオプションのソースに応じて何かが異なることを意味します。それだけですべて 90 は同じですが、文脈によって意味が異なります:

template <typename T>
void process(std::optional<T> value)
{
    if (!value)
        // we don't know *why* the `T` isn't there, it just isn't
}

105 :116 またはエラー

追加情報を提供する場合 理由 125 提案された 137 を使用できます .これは「148 のいずれか」を意味します またはその存在を妨げたエラー 151

標準的な例は次のようになります:

/// Opens the file or returns an error code if it was unable to do so.
std::expected<file, std::error_code> open_file(const fs::path& p);

関数がファイルを返すことができなかった場合、165 を返します。 その代わりに 175 187 のようなものです — インターフェースが改善され、セマンティクスがより明確になっただけです。199 208 を意味します または 214227 230 を与える 特別な意味。

しかし、244 の場合、興味深いことが起こります。 単一の状態を持つ空の型です:

struct value_not_found {};

template <typename T>
std::expected<T, value_not_found> lookup(const database& db, std::string name);

この 258 実装も 267 を返します 見つからなかった場合は何もありません.しかし、「何もない」には、型にエンコードされた明確な意味があります — 275 .

これは 281 とは異なります :その場合、オプションのコンテキスト/起源が与えられた場合にのみ意味が存在します。現在、意味は型自体にエンコードされています:

template <typename T>
void process(std::expected<T, value_not_found> value)
{
    if (!value)
        // ah, the `T` wasn't found in the database
}

後で説明するように、これは重要な違いです。

要約:293303312

要約すると:

  • 323 より良い 330 です
  • 349 より良い 351 です
  • 366379 どちらも「空」を意味する一般的な型であり、特別な意味は文脈によってのみ染み込む
  • 389 などの他の空の型 コンテキストなしで、それだけで意味を持って専門化されている
  • 392 および 403 どちらも同じ意味です:413 あるかないか — ない場合は意味がない
  • 421 436 よりも意味的な意味があります :449 エラーに詳しい情報を提供します

ここで重要な仮定をしていることに注意してください:458465 471 を使用します。 理由 なぜ 482 を持っていませんでした 496 を使用します どちらのタイプも、異なる API では問題ありません。

仮定をもう一度繰り返します。これに同意しない場合は、投稿の残りの部分に同意しないからです。

501 および 512 どちらも同じ「525」をモデル化しています そこにないかもしれません.537 追加情報を保存するだけです理由 ありません。

ある 541 を使用するその他の状況 しかし、多かれ少なかれそれらは問題があると考えています。フォローアップの投稿でさらに詳しく説明します。今のところ、私の仮定が成り立つ状況を考えてみてください.

ネストはオプションであり、期待される

2 つの API をもう一度考えてみましょう:

/// Does a database lookup, returns `std::nullopt` if it wasn't found.
template <typename T>
std::optional<T> lookup(const database& db, std::string name);

/// Calls the function if the condition is `true` and returns the result,
/// `std::nullopt` if the condition was false.
template <typename T>
std::optional<T> call_if(bool condition, std::function<T()> func);

これらの API には 2 つの興味深い状況があります。

1 つ目は、553 の可能性がある値のデータベース ルックアップを実行する場合に発生します。

auto result = lookup<std::optional<my_type>>(db, name);
if (!result)
    // not found in database
else if (!result.value())
    // found in database, but `null`
else
{
    // found and not null
    auto value = result.value().value();
}

最終的に 561 になります .外側のオプションが空の場合、値がデータベースに保存されていないことを意味します.内側のオプションが空の場合、値はデータベースに保存されていましたが、578 だったことを意味します .両方が空でない場合、値は格納され、583 ではありません .

2 番目の状況は、2 つの機能を単純に組み合わせた場合に発生します。

auto lambda = [&] { return lookup<my_type>(db, name); };
auto result = call_if(condition, lambda);
if (!result)
    // condition was false
else if (!result.value())
    // condition was true, but the lookup failed
else
{
    // condition was true and the lookup succeeded
    auto actual_value = result.value().value();
}

ここでも、入れ子になったオプションがあります。また、どのオプションが空であるかによって、意味が異なります。

でも 598 だけ それ自体ではその情報を持っていません!空のオプションは何も意味しません。空のオプションを含むオプションも同様です.

void process(std::optional<std::optional<my_type>> result)
{
    if (!result)
        // ah, the result was not found in the database
        // or the condition was false
        // or the value was null?
    else if (!result.value())
        // was found, but `null`
        // or the condition was true but not found?
    else
        …
}

コンテキスト、さらに順序 操作の意味を与えます。

607 で 一方、API の場合、情報は明確です:

void process(std::expected<std::expected<my_type, value_not_found>, func_not_called> result)
{
    if (!result)
        // function wasn't called
    else if (!result.value())
        // value not found
}

613 と言っているわけではないことに注意してください。 API の方が優れている :629 あるのは厄介です 633 を返す 、 640 明らかにその関数のより良い選択です.そして、私は 651 も主張したいと思います. 666 を使用する必要があります 値が存在しない理由が複数ある場合を除きます。

671 であることを示しているだけです。 保存 683 中の空の状態に関する情報

フラット化オプションおよび期待

上記の両方の状況が理想的ではないことに全員が同意できることを願っています。 または 705

値を処理したい場合は、おそらく次のようにします:

auto result = lookup<std::optional<my_type>>(db, name);
if (!result)
    process(std::nullopt);
else if (!result.value())
    process(std::nullopt);
else
    process(result.value().value());

void process(const std::optional<my_type>& result)
{
    if (!result)
        // wasn't there — for whatever reason
    else
        // it was there, go further
}

つまり、715 の 2 つの異なる空の状態を組み合わせます。 平らにする 729 733 に .

748 の平坦化 情報を失う:2 つの異なる空の状態を 1 つに押しつぶしていますが、追加のコンテキストがなければ、2 つの空の状態はとにかく同じです — 757 複数の場所から呼び出された場合、それらを区別できません。気にするのは、実際に値があるかどうかだけです。

理由が気になる場合は、766 API の方が良いかもしれません。

auto result = lookup<std::optional<my_type>>(db, name);
if (!result)
    process(name_not_found);
else if (!result.value())
    process(value_null);
else
    process(result.value().value());

ここで、個別のエラー情報を 778 に渡します これは実際に使用可能な情報です。ある意味では、それは平坦化でもあります。しかし、情報を保存する平坦化です。そのような保存平坦化には、コンテキスト、つまり 781 の意味が必要です。 であるため、一般的な方法では実行できません。

794の組み合わせで ベースの API では、ネストされた 808 になることもあります .どのように平らにしますか?

816 または失敗しました。失敗したときは、828 が原因で失敗しました。 または 832 のため .つまり:841 857 にフラット化 .この平坦化により保存 すべての情報。

867 の場合に注意してください と 874 空の型、887 エラーコード 898 に似ています

完全を期すために 900 を混ぜるとどうなりますか と 918 ?

924 を覚えていれば 936 です 、フラット化ルールは自然に従います:946 956 です 967 です .そして 971 982 です 994 です .

考えてみれば、これは理にかなっています。どちらの場合も、次の 3 つの状態があります:10001012 による失敗 または、一般的な理由による失敗。

一般的な障害が別の順序で発生するため、情報が失われていると主張するかもしれませんが、とにかくそれは実際には有用な情報ではありません.それは単なる「一般的な障害」です.

1022 1035 のため、平坦化ルールは整形式です 1046 です 1056 です 1065 です 1079 です .オプションのフラット化ルールは単純に従うだけです!

要約すると:

  • 1082 1091 まで平坦化 、すべての情報を保持
  • 1108 1114 まで平坦化 、一部の情報が失われましたが、その情報はそもそも存在しませんでした
  • 1125 を扱うことから、その他の平坦化規則が続きます。 1139 として

ネストされたオプションまたは期待値は必要ありません

ネストされたオプショナルと予想されるものを扱うのは厄介です。複数のレイヤーをチェックする必要があります。 1144 と記述してください 等々。したがって、実際のコードではそれらを回避します。それらを取得したらすぐに、おそらく手動でフラット化します。

繰り返しになりますが、ネストされたオプションをフラット化しても、使用可能が失われることはありません 空の状態は、コンテキストからのみ意味を取得します。コンテキストが存在しない場合、それらは同等です。

したがって、ユーザー向けの高レベル API を作成している場合は、ネストされたオプションまたは期待値を意図的に返すことは決してありません!

「意図的に」と言ったことに注意してください:

template <typename T>
std::optional<T> lookup(const database& db, std::string name);

見ただけでは、この API はネストされたオプションを返しません。しかし、これまで見てきたように、1155 それ自体はオプションです。

しかし、この API は何も悪いことをしていません。その意図と目的のために、1168 不透明なジェネリック型です。正確な詳細にはあまり関係ありません。その API を使用するすべてのジェネリック コードは、実際には入れ子になったオプションであることを認識することはなく、1177 を処理するだけです。 どこで 1184

1196 を明示的に渡した最終ユーザーのみ しかし、API 自体は「意図的に」作成されたのではなく、いわば「偶然」に作成されたものです。

1201 と書いたら 1211 と書くだけなら どこで 1227 かも 1238 であること でも気にしないで、大丈夫です。

自動フラット化?

ネストされたオプションを取得したらすぐにフラット化する場合、それを自動的に行わないのはなぜですか? 1241 にしないのはなぜですか? と 1256 同じタイプですか?

結果をあまり考えず、正当化を裏付けるこの 2800 語のエッセイもなしに、Twitter で提案したので、それを行うのは有害で奇妙に思えました。

もちろん 12601276 異なるものです:1つは 1280 です そこにないかもしれませんが、もう 1 つは 1294 です そこにないかもしれません.しかし、私があなたを納得させたかもしれないように、文脈なしでは、この区別は実際には使用できません.どちらも 1309 をモデル化するだけです. そこにないかもしれません。

だから私は 欲しがる のが正当だと思う しかし、残念ながらそれはまだ非現実的です。

次のテストがすべての 1314 に対して保持されると予想されます :

T some_value = …;

std::optional<T> opt1;
assert(!opt1.has_value());

std::optional<T> opt2(some_value);
assert(opt2.has_value());
assert(opt2.value() == some_value);

1326 の場合 1335 です 1348 で自動的に平らになります 1355 は返されません オブジェクトを返すと、1360 が返されます !これにより、一般的なコードで問題が発生する可能性があることは想像に難くありません。

すべてを自動的にフラット化

オプションの作成

ブログ投稿のこの時点で、モナドを紹介する必要があります。この目的のために、モナドは 1376 のコンテナーです。 、 1389 、次の操作で:

  • 平坦化 1390 1409
  • 1411 を適用する 14241437 を生成 、1449 と呼ばれる
  • 1459 を適用する 14611475 を生成 、1484 と呼ばれる または 1490

これは、1500 に対して実装する方法です :

template <typename T>
std::vector<T> flatten(const std::vector<std::vector<T>>& vec)
{
    std::vector<T> result;
    for (auto& outer : vec)
        for (auto& inner : outer)
            result.push_back(inner);
    return result;
}

template <typename T, typename U>
std::vector<U> map(const std::vector<T>& vec, const std::function<U(T)>& func)
{
    std::vector<U> result;
    // just std::transform, really
    for (auto& value : vec)
        result.push_back(func(value));
    return result;
}

template <typename T, typename U>
std::vector<U> and_then(const std::vector<T>& vec, const std::function<std::vector<U>(T)>& func)
{
    std::vector<U> result;
    for (auto& value : vec)
        for (auto& transformed : func(value))
            result.push_back(transformed);
    return result;
}

1517 の実装 または 1528 1532 については注意してください。 2 つの実装があります。1 つは値に関するもので、もう 1 つはエラーに関するものです。そして、私が説明したフラット化は、ここで期待されるフラット化と実際には一致しません (しゃれは意図されていません)。

15431551 あるケースでは、関数はすべての要素を個別に変換し、単一の要素を生成します。別のケースでは、関数はすべての要素を再びコンテナーに変換します。

1567 を実装することもできます 1576 を呼び出して そして 1586

そして明らかに 1596 の場合 1607 との間には大きな違いがあります と 1614 .

ただし、1625 の場合 ?

私は主張しましたが、そうではありません.それでも、どちらを行うかを考える必要があります:

std::optional<int> opt = …;
opt = map(opt, [](int i) { return 2 * i; } );
opt = and_then(opt, [](int i) { return i ? std::make_optional(4 / i) : std::nullopt; } ); 

最初のラムダは 1637 を返します 、つまり 1640 を使用します .2 番目は 1655 を返します 、つまり 1665 を使用します .誤って 1678 を使用した場合 あなたは 1686 を持っています .

その違いについて考えるのは面倒です:C++ ですでにオプションを作成するのはかなり厄介です。そのような違いは問題にならないはずです。

単一の関数は、何を投げても正しいことを行う必要があります。

はい、これは数学的に不純であり、実際には 1697 のモナドを実装していません .しかし、C++ は圏論ではありません。実用的であることは問題ありません。いずれにせよ、「モナド」を取るテンプレートは実際にはありません。それらは数学的に類似していますが、実際の使用法とパフォーマンスの違いはあまりにも異なります.

一般にモナドが自動的に平坦化すべきだと言っているわけではない .Just 1705 .

同様に、expected を返す複数の関数を構成する場合も同様に平坦化する必要があります。ネストされた 1710 は必要ありません。 、単一の 1726 が必要です すべてのエラーを結合します。

コンポジションでのこの自動平坦化には前例があることに注意してください:Rust の予想、1731 1741 を返す関数を作成している場合 1758 を返す関数で 、それらは自動的に変換されます。

結論

1765 の空の状態 固有の意味はありません.単に「空」を意味します.「見つからない」などの意味を与えるのは起源だけです.

そのため 1779 1787 のみを意味します または空または本当に空。 1796 と同じ追加のコンテキストなし ネストされたオプションをフラット化すると、情報は失われますが、使用可能な情報は失われます。

空の状態に特別な意味を持たせたい場合は 1808 を使用してください どこで 1810 は特別な意味です。ネストされた期待値を平坦化すると、すべての情報が保持されます。

ネストされたオプションまたは期待値を操作するのは厄介なので、フラット化する必要があります。汎用コードでブレークするたびに自動的にフラット化しますが、コンポジションでフラット化することは数学的に少し不純ですが、機能します。

その情報を使用して、Barry のブログ投稿で概説されている比較の問題にも答えることができます。What should 1827 戻る?

1837 として それ自体には特別な意味はありません。すべてのインスタンスは等しいです。入れ子になったオプションの数は関係ありません。