Jetpack Compose 徹底探索シリーズ 1: コンポーザブル機能

コンポーズ可能な関数の意味

単純な構文だけに注目すると、標準の Kotlin 関数は次のようにアノテーションを付けることで、コンポーズ可能な関数にすることができます@Composable

ここに画像の説明を挿入

これを行うことで、基本的に、関数がコンポーザブル ツリーに登録するために一部のデータをノードに変換するつもりであることをコンパイラに伝えます。つまり、コンポーズ可能な関数を と考えると@Composable (Input) -> Unit、入力はデータですが、出力は多くの人が考える関数の戻り値ではなく、ツリーに要素を挿入する登録アクションです。これは関数実行の副作用と考えることができます。

ここでのいわゆる「登録アクション」は、通常、Compose では「発行」と呼ばれます。放出アクションは、コンポーズ可能な関数が実行されるときに実行されます。これは、コンポジション中に発生します。

ここに画像の説明を挿入
コンポーズ可能な関数は、ツリーのメモリ内表現状態を構築または更新することのみを目的として実行されます。これにより、コンポーズ可能な関数は読み取られたデータが変更されるたびに再実行されるため、それが表すツリー構造を常に最新の状態に保つことができます。ツリーの状態を最新の状態に保つために、(上記のように) 新しいノードを挿入する操作を発行できますが、同様にノードを削除、置換、または移動することもできます。コンポーズ可能な関数は、ツリーから状態を読み取ったり、ツリーに状態を書き込んだりすることもできます。

コンポーザブル関数のプロパティ

関数に注釈を付けると、Composable他の関連した影響が生じます。@Composable注釈は、適用される関数または式の型を効果的に変更し、他の型と同様に、それにいくつかの制約やプロパティを課します。これらのプロパティは、Compose ライブラリの関連機能のロックを解除するため、Jetpack Compose と非常に密接に関連しています。

Compose runtime関数は上記のプロパティに従うことが期待されているComposableため、特定の動作を想定し、並列合成、優先順位に基づく任意の合成順序、スマートな再編成や位置メモリなどのさまざまな実行時の最適化を利用できます。

一般に、ランタイムの最適化は、ランタイムが実行する必要があるコードについてある程度の確信を持っている場合にのみ可能であり、ランタイムはそこから特定の条件と動作を想定できます。これにより、実行の機会、つまり、別の実行戦略または評価手法に従うために、前述の決定論を使用してこのコードを「消費」する機会が解放されます。

これらの確実性の例としては、コード内のさまざまな要素間の関係が挙げられます。これらは相互に依存していますか? プログラムに影響を与えずに、それらを並列または異なる順序で実行できますか? 各アトミック論理フラグメントを完全に分離された単位として解釈できますか?

コールコンテキスト

コンポーズ可能な関数のほとんどのプロパティは、Compose コンパイラによって有効になります。これはKotlin コンパイラー プラグインであるため、通常のコンパイラー フェーズで実行され、Kotlin コンパイラーが可能なすべての情報にアクセスできます。これにより、追加情報を追加するために、すべての構成可能な関数からIR (中間表現)をインターセプトして変換することができます。

その中で、Compose コンパイラーが各関数にComposable追加することの 1 つはComposer、パラメーター リストの最後にパラメーターを追加することです。このパラメーターは暗黙的です。つまり、開発者はコードを作成するときにそれを認識しません。そのインスタンスは実行時に挿入され、すべての子呼び出しに転送されるComposableため、ツリーのすべてのレベルからアクセスできます。

ここに画像の説明を挿入

次のコードがあるとします。

ここに画像の説明を挿入

次に、Compose コンパイラはそれを次のように変換します。

ここに画像の説明を挿入

Composer本文内のすべての呼び出しに転送されていることがわかりますComposableこれに基づいて、Compose コンパイラは、コンポーズ可能な関数に厳密なルールを課します。これらの関数は、他のコンポーズ可能な関数からのみ呼び出すことができますこれには実際には呼び出しコンテキストが必要であるため、ツリーがコンポーズ可能な関数のみで構成され、それらの関数をComposer下に転送できるようになります。

Composer私たちが開発者として作成するコンポーザブル コードとCompose runtimeコンポーズ可能な関数はこれを使用してツリー上で変更操作を発行し、それによってCompose runtimeメモリ内の表現状態を構築または更新するためにツリーの形状を通知します。

冪等

コンポーズ可能な関数は、生成するノード ツリーに関して等です。つまり、同じ入力パラメータを使用してコンポーザブル関数を複数回再実行すると、同じツリーが生成されるはずですJetpack Compose ランタイムは、再構成などについてこの前提に依存します。

Jetpack Compose では、再構成とは、入力が変更されたときにコンポーズ可能な関数を再実行する行為であり、更新された情報を出力してツリーを更新できます。Compose runtimeコンポーザブル関数はいつでも、さまざまな理由で再結合できる必要があります。

再編成プロセスはツリー全体を調べて、どのノードを再グループ化する必要があるかを (繰り返し) チェックします。入力が変更されたノードのみが再グループ化され、残りはスキップされます。ノードのスキップは、それを表すコンポーザブル関数がidempotentである場合にのみ可能です。これは、ランタイムが同じ入力が与えられた場合に同じ結果が生成されると想定できるためです。これらの結果はすでにメモリ内にあるため、Compose はそれを再実行する必要はありません。

制御できない副作用を取り除く

副作用として、予期しないことを行うために、それを呼び出した関数の制御を逃れることがあります。ローカル キャッシュからのデータの読み取り、ネットワーク リクエストの実行、またはグローバル変数の設定はすべて副作用とみなされる可能性があります。これらは、関数の呼び出しを、その動作に影響を与える可能性のある外部要因 (他のスレッドから書き込まれる可能性のある外部状態、例外をスローする可能性のあるサードパーティ API など) に依存させます。言い換えれば、この時点では、関数は結果を生成するために入力だけに依存していません。

副作用により、関数の入力ソースがあいまいまたは非決定的になりますこれは Compose にとって好ましくありません。ランタイムは、コンポーズ可能な関数が予測可能であること (つまり、決定性があること) を期待しており、安全に複数回再実行できるからです。

コンポーズ可能な関数が副作用を実行する場合、実行するたびに異なるプログラム状態が生成され、非冪等になる可能性があります。

次のように、コンポーザブル関数の本体から直接ネットワーク リクエストを行うとします。

ここに画像の説明を挿入
Compose runtimeこの機能は短期間に何度も再実行され、ネットワーク要求が複数回発生して制御不能になる可能性があるため、これは非常に危険です。これらの実行は調整なしに別のスレッドで発生する可能性があるため、現実はそれより悪いです。

Compose runtimeコンポーズ可能な関数の実行戦略を選択する権利は留保されます。複数のコアを利用してパフォーマンスを向上させるために、別のスレッドへの再編成をスケジュールしたり、ニーズや優先順位に応じて任意の順序でコンポーザブル関数を実行したりできます (たとえば、画面に表示されない組み合わせには、より低い優先順位を割り当てることができます)。

もう 1 つの一般的な副作用警告は、あるコンポーザブル関数が別のコンポーザブル関数の結果に依存し、順序関係が強制される可能性があることです。これは何としてでも避けたいと思っています。例えば:

ここに画像の説明を挿入
このコード スニペットでは、HeaderProfileDetailEventList任意の順序で (並列でも) 実行できます。書き込まれることが予想される外部変数からの読み取りなど
、特定の実行順序を想定したロジックを作成すべきではありません。ProfileDetailHeader

一般に、副作用はコンポーズ可能な関数では理想的ではありません。すべてのコンポーザブル関数をステートレスにして、すべての入力をパラメーターとして受け取り、結果を生成するためにのみ使用できるようにする必要があります。これにより、コンポーザブルはよりシンプルで信頼性が高く、再利用性が高くなります。ただし、ネットワーク リクエストの実行、データベースへの情報の保存、メモリ内キャッシュの使用などを必要とするステートフル プログラムを作成する場合には、副作用が必要になります。したがって、ある時点でそれらを実行する必要があります (通常はコンポーザブル ツリーのルートで)。このため、Jetpack Compose は、制御された環境でコンポーザブル関数から副作用操作を安全に呼び出すためのメカニズムであるSide Effect APIを提供します。

副作用 API は、副作用操作にコンポーザブルのライフサイクルを認識させるため、コンポーザブルにバインド/駆動できるようになります。これらにより、コンポーザブルがツリーからアンロードされるときに副作用操作を自動的にキャンセルしたり、副作用の入力が変更されたときに再トリガーしたり、複数の再コンポジションにわたって同じ副作用 (1 回だけ呼び出される) を維持したりすることもできます。これらにより、制御なしでコンポーザブル本体から直接副作用操作を呼び出すことを回避できます。副作用ハンドラーについては後の章で詳しく説明します。

再起動可能

コンポーズ可能な関数は再構成できることを何度か述べましたが、コール スタックの一部として 1 回だけ呼び出されるわけではないという意味で、コンポーズ可能な関数は従来の標準関数とは異なります。

通常の呼び出しスタックは次のようになります。各関数は 1 回呼び出され、1 つ以上の他の関数を呼び出すことができます。

ここに画像の説明を挿入
一方、コンポーズ可能な関数は複数回再起動 (再実行、再構成) できるため、ランタイムはそれらへの参照を保持しますコンポーザブル呼び出しツリーは次のようになります。

ここに画像の説明を挿入

ここで、Composable 4、 および はComposable 5入力が変更された後に再実行されます。

Compose は、メモリ内の表現を常に最新の状態に保つために、ツリー内のどのノードを再起動するかを選択します。コンポーズ可能な関数は、反応的であり、観察された状態の変化に基づいて再実行できるように設計されています。

Compose コンパイラは、何らかの状態を読み取るすべての Composable 関数を検索し、再起動する時期が来たことをランタイムに伝えるために必要なコードを生成します。状態を読み取らないコンポーズ可能な関数は再起動する必要がないため、ランタイムにその方法を指示する必要はありません。

高速実行

コンポーザブル関数とコンポーザブル関数ツリーは、メモリ内に保持され、後の段階で解釈/実体化されるプログラムの記述を構築するための、高速で宣言的かつ軽量な方法と考えることができます。

コンポーズ可能な関数はUI を構築して返しませんメモリ内の構造を構築または更新するためにデータを送信するだけです。これにより、実行が非常に高速になり、ランタイムが恐れることなく複数回実行できるようになります。場合によっては、アニメーションのすべてのフレームのように、非常に頻繁に発生します。

開発者はコードを記述するときにこの点を認識し、可能な限りこの期待に応えるように努める必要があります。高い時間コストを引き起こす可能性のある計算操作はすべてコルーチンで実行する必要があり、常にライフサイクルを意識した副作用 API でラップする必要があります。

位置記憶

場所の記憶は機能の記憶の一形態です。関数のメモ化とは、同じ入力に対して呼び出されるたびに関数を再計算する必要がないように、入力に基づいて結果をキャッシュする関数の機能です。前述したように、これは純粋な関数(決定論的) でのみ可能です。同じ入力に対して常に同じ結果を返すことが確実であるため、値をキャッシュして再利用できます。

関数のメモ化は、関数プログラミング パラダイムのよく知られた手法であり、プログラムは純粋な関数の構成として定義されます。

関数メモリでは、関数呼び出しは、名前パラメータ値の組み合わせによって識別できます。これらの要素を使用して、後の呼び出しでキャッシュされた結果を保存/インデックス付け/読み取りするための一意のキーを作成できますしかし、Compose では、追加の要素が考慮されます。つまり、コンポーズ可能な関数は、ソース コード内での位置に関する不変の知識を持っています。同じ関数が同じパラメータ値を使用して異なる場所で呼び出された場合、ランタイムは異なる値を生成しますid(親関数内で一意)。

ここに画像の説明を挿入
インメモリ ツリーには、それぞれ異なる ID を持つ 3 つの異なるインスタンスが保存されます。

ここに画像の説明を挿入

コンポーザブルの ID は再構成中に保持されるため、ランタイムはこの ID を使用してコンポーザブルが以前に呼び出されたかどうかを判断し、可能であればスキップできます。

場合によっては、Compose runtimeユーザーにとって一意の識別子の割り当てが難しい場合があります。簡単な例は、ループからコンポーザブルのリストを生成することです。

ここに画像の説明を挿入

この場合、毎回同じ場所から呼び出されますTalk(talk)が、それぞれがTalkリスト上の異なる項目を表すため、ツリー上の異なるノードを表します。この場合、呼び出しの順序Compose runtimeに基づいて一意の呼び出しを生成し、それでもそれらを区別できるようにします。id

新しい要素をリストの最後に追加する場合、残りの呼び出しは以前と同じ場所に留まるため、このコードは引き続き正常に機能します。しかし、要素を上部または中間のどこかに追加した場合はどうなるでしょうか? 入力が変更されなかった場合でも、位置が変更されると、Compose runtimeその位置の下にすべてが再グループ化されます。Talkこれらの呼び出しはスキップされるべきであるため、これは非常に非効率的です (特に長いリストの場合)。

この問題を解決するために、Compose はkey設定用の Composable を提供するため、明示的なものを Composable 呼び出しに手動で割り当てることができますkey

ここに画像の説明を挿入
talk.idこの例では、 each として (おそらく一意の)を使用します。これによりTalkkeyランタイムは、位置に関係なく、リスト内のすべての項目の ID を保持できるようになります。

メモ化により、ランタイムは設計によりコンポーズ可能な関数を記憶できるようになります。Compose コンパイラによって再起動可能であると推測されるコンポーズ可能な関数はすべてスキップ可能である必要があるため、自動的に記憶されます。Compose はこのメカニズムに基づいて構築されています。

開発者は、コンポーザブル関数の範囲よりも詳細な方法でこのメモリ構造を使用する必要がある場合があります。コンポーザブル関数で発生する大量の計算の結果をキャッシュしたいとします。Compose ランタイムは、rememberこのための関数を提供します。

ここに画像の説明を挿入
rememberここでは、キャッシュされた操作の結果を使用して、画像のフィルターを事前計算します。インデックス キャッシュ値のキーは、ソース コード内の呼び出し位置と関数入力 (この場合はファイル パス) に基づきます。remember関数は、ツリーの状態を保持するメモリ構造の読み取りおよび書き込み方法を認識する、単なるコンポーズ可能な関数です。この「位置の記憶」メカニズムは開発者にのみ公開されます

Compose では、メモリはアプリケーション レベルではありません。何かがメモ化されると、それを呼び出したコンポーザブルのコンテキストで実行されます上の例では、 ですFilteredImage実際には、Compose は、コンポーザブル情報を格納するメモリ構造内のスロットの範囲からキャッシュされた値を検索します。これにより、このスコープではシングルトンに近くなります。同じコンポーザブルが異なる親クラスから呼び出された場合、その値の新しいインスタンスが返されます。

サスペンド機能との類似点

Kotlin のサスペンド関数は他のサスペンド関数からのみ呼び出すことができるため、呼び出しコンテキストも必要です。これにより、サスペンド関数は一緒にチェーンすることしかできないことが保証され、Kotlin コンパイラーにすべての計算レベルにわたってランタイム環境を注入および転送する機会が与えられます。これにより、各サスペンド関数の引数リストの末尾に追加のパラメータが追加されますContinuationこのパラメータも暗黙的であるため、開発者はそれを知る必要はありません。継続を使用すると、言語のいくつかの新しい強力な機能のロックを解除できます。

これは、前述の Compose コンパイラが行うことと非常によく似ていますね。

継続は、Kotlin コルーチン システムのコールバックに似ています。それはプログラムにどのように進めるかを指示します。

たとえば、次のコード:

ここに画像の説明を挿入

これは、Kotlin コンパイラーによって次のように置き換えられます。

ここに画像の説明を挿入

ContinuationKotlin ランタイムがプログラム内のさまざまな一時停止ポイントから実行を一時停止および再開するために必要なすべての情報が含まれています。これにより、ハングは、呼び出しコンテキストが実行ツリー全体で暗黙的な情報を伝達する手段としてどのように使用できるかを示すもう 1 つの良い例になります。高度な言語機能を有効にするために実行時に使用できる情報。

同様に、@Composable言語の特徴として理解することもできます。標準の Kotlin 関数を再起動可能、リアクティブなどにします。

現時点で、当然の疑問は、なぜ Jetpack Compose チームがsuspend望む動作を実現するために Jetpack Compose を使用しなかったのかということです。これら 2 つの機能は、実装するパターンが非常に似ていますが、どちらも言語で完全に異なる機能を有効にします。

Continuation インターフェイスは、実行の一時停止と再開という点で非常に特殊であるため、コールバック インターフェイスとしてモデル化されており、Kotlin はジャンプの実行、さまざまな一時停止ポイントの調整、一時停止ポイント間でのデータ共有などに必要なものを含むデフォルト実装を生成します。 . すべてのメカニズム。Compose の使用例は、実行時にさまざまに最適化できる大規模なコール グラフのメモリ内表現を作成することが目的であるため、これとは大きく異なります。

コンポーザブル関数とサスペンド関数の類似点を理解したら、「関数の色分け」のアイデアを検討してみると興味深いでしょう。

コンポーザブル関数の色

コンポーズ可能な関数には、標準関数とは異なる制限と機能があります。これらにはさまざまなタイプがあり (詳細は後ほど)、非常に具体的な懸念事項をモデル化します。これらは何らかの形で関数の別のクラスを表すため、この区別は関数の色付けの一形式として理解できます

関数の色付けは、 Google の Dart チームの Bob Nystrom によって、「What Color Are Your Functions?」というタイトルのブログ投稿で紹介されました。彼は、非同期同期が適切に連携しない理由を説明しています。同期も非同期にするか、非同期関数を呼び出してその結果を待機できる待機メカニズムを提供しない限り、同期関数から非同期関数を呼び出すことはできないからです。そのためPromise、 および はasync/await一部のライブラリや言語で導入されています。これは、構成可能性を取り戻す試みです。ボブ・ニストロムは、これら 2 つの機能カテゴリーを 2 つの異なる「機能カラー」と呼んでいます。

Kotlin でもsuspend同じ問題を解決することを目指しています。ただし、サスペンド関数は他のサスペンド関数内からのみ呼び出すことができるため、サスペンド関数にも色が付けられます。標準関数とサスペンド関数を使用してプログラムを作成するには、特別な統合メカニズム (コルーチン起動ポイント) が必要です。統合は開発者にとって不透明です。

一般に、この制限は想定内のことです。実際には、まったく異なる性質の概念を表す 2 つのクラスの関数をモデル化しています。まるで 2 つの異なる言語について話しているようです。2 つの操作があります。1 つはすぐに返される結果を計算するように設計された同期操作で、もう 1 つは時間の経過とともに展開され、最終的に結果が得られる (完了までに時間がかかる場合があります) 非同期操作です。

Jetpack Compose では、コンポーズ可能な関数の状況は同等です。標準関数からコンポーズ可能な関数を透過的に呼び出すことはできません。これを行うには、統合ポイント (例: Composition.setContent) が必要です。コンポーズ可能な関数には、標準関数とはまったく異なる目標があります。これらはプログラム ロジックを記述するために使用されるのではなく、ノード ツリーへの変更を記述するために使用されます。

これはばかげているように思えるかもしれません。コンポーズ可能な関数の優れた点の 1 つは、ロジックを使用して UI を宣言できることです。これは、標準関数からコンポーズ可能な関数を呼び出す必要がある場合があることを意味します。例えば:

ここに画像の説明を挿入
ここではSpeakerComposable が のforEachラムダ関数から呼び出されていますが、コンパイラはエラーを報告していないようです。異なる機能の色を混合するこの方法はどのように機能するのでしょうか?

その理由は、forEach関数がinlineインライン化されているためです。inlineセット演算子は、ラムダを呼び出し元にインライン化し、追加のギャップがないかのように有効にするように宣言されます。上の例では、SpeakerComposable への呼び出しが it にインライン化されていますSpeakerListが、両方とも関数であるため、これは許可されていますComposableインライン化を利用することで、関数の色付けの問題を回避して、組み合わせたロジックを作成できます。最終的には、ツリーも構成可能な関数のみで構成されます。

しかし、関数の色付けの問題は本当に問題なのでしょうか?

そうですね、2 種類の関数を組み合わせて、一方からもう一方へジャンプし続ける必要がある場合は、おそらくこれが当てはまるでしょう。ただし、またはsuspend@Composable場合はどちらにも当てはまりません。どちらのメカニズムも統合ポイントを必要とするため、そのポイントを超えると完全に色の付いた呼び出しスタック (任意のsuspend関数を含む) が得られます。Composableこれにより、コンパイラとランタイムが色付き関数を異なる方法で処理できるようになり、標準関数では不可能なより高度な言語機能が可能になるため、実際には利点になります。

Kotlin では、suspend非常に慣用的かつ表現力豊かな方法で非同期ノンブロッキング プログラムをモデル化できます。この言語は、関数に修飾子を追加するという極めて単純な方法で、非常に複雑な概念を表現できますsuspend一方、@Composable標準の Kotlin 関数では利用できない、標準関数の再起動可能、スキップ可能、応答性が向上します。

コンポーズ可能な関数の種類

@Composableアノテーションは、コンパイル時に関数の型を効果的に変更します。構文の観点から見ると、コンポーズ可能な関数の型は です@Composable (T) -> Aまたはその他の型 (関数が関数などの値を返す場合) です。開発者は、Kotlin の標準ラムダと同様に、この型を使用してコンポーザブル ラムダを宣言できます。AUnitremember

ここに画像の説明を挿入

コンポーズ可能な関数には型を持つこともできます@Composable Scope.() -> A。型は通常、情報の範囲を特定のコンポーズ可能なオブジェクトに限定するためにのみ使用されます。例えば:

ここに画像の説明を挿入
言語の観点から見ると、型は、迅速な静的検証を実行し、場合によっては便利なコードを生成し、実行時にデータがどのように使用されるかを境界設定/調整するためにコンパイラーに情報を提供するために存在します。@Composableアノテーションは実行時の関数の検証方法と使用方法を変更するため、アノテーションは通常の関数とは異なる型を持つとみなされます。

要約する

  • Composable 関数の意味は、LayoutNode ノードを複合の組み合わせに送信し、実行中にそれを複合ツリーに挿入することです。

  • @Composable アノテーションは実際に関数の型または式の型を変更し、Compose ランタイムはこれに基づいて、並列合成、インテリジェントな再編成、ロケーション メモリなどのランタイムの最適化を行います。

  • Compose コンパイラは、各コンポーザブル関数のパラメータ リストの末尾に Composer パラメータを追加します。これは、コンパイラ段階で IR を変更することによって実装され、開発者には表示されません。Composer のインスタンスは実行時に挿入され、ツリー全体でアクセスできるすべての子コンポーザブルに転送されます。

  • Compose コンパイラの制限ルール: コンポーズ可能な関数は、他のコンポーズ可能な関数からのみ呼び出すことができます。その理由はまさに、追加された Composer パラメータが下に転送されるためです。

  • コンポーズ可能な関数は、入力が非決定的になる副作用操作を直接実行することを避ける必要があります。そうしないと、ランタイムが複数の組み換えを安全に実行できることを保証できません。副作用操作は、Compose が提供する副作用 API を使用して実行する必要があります。副作用 API により、副作用操作でコンポーザブルのライフサイクルが認識されます。これらは、コンポーザブルがツリーからマウントされるときに開始でき、アンマウントされると自動的にキャンセルされます。

  • コンポーズ可能な関数には相互に順序がありません。コードが記述された順序で実行されるのではなく、ランタイムによって決定される任意の順序で実行することも、同時に実行することもできます。したがって、その順序に依存してコード ロジックを記述することはできません。

  • Compose ランタイムは Composable 関数への参照を保持するため、Composable 関数は再起動可能です。つまり、従来の関数呼び出しスタックとは異なり、複数回再実行できます。

  • ロケーション メモリ: Compose ランタイムは、コンポーザブル関数ごとに一意の ID (キー) を生成します。これには、ソース コード内のコンポーザブルの位置情報が含まれます。つまり、異なる場所で呼び出される同じパラメーター値を持つ同じ関数 ID は異なります。key() { } を手動で呼び出すことで、コンポーザブルに明示的なキーを割り当てることができます。

  • コンポーズ可能な関数は、驚くほど kotlin のサスペンド関数に似ています。たとえば、サスペンド関数は他のサスペンド関数内でのみ呼び出すことができます。Kotlin コンパイラは追加の Continuation パラメータをサスペンド関数に挿入します。

  • 関数の色付け: Kotlin のサスペンド関数の場合、非同期関数と同期関数の区別に関するもので、通常の関数とサスペンド関数は 2 つの異なる関数の色を表します。コンポーザブル関数の場合は、標準関数とコンポーザブル関数の区別が重要です。

  • コンポーザブル関数内の一部のコレクション操作 API で他のコンポーザブル関数を呼び出すことができます。これらのコレクション操作 API はインライン インライン、つまり任意のインライン関数であるため、「コンポーザブル関数は他のコンポーザブル関数からのみ呼び出すことができる」というルールに違反しません。コンポーザブル関数内の呼び出しは、他のコンポーザブル関数を直接呼び出すことができます。

  • サスペンド関数の目的は、サスペンドと再開、非同期関数と同期関数の組み合わせを解決することであり、コンポーザブル関数の目的は、再起動可能、スキップ可能、および応答性の問題を解決することであり、メモリ表現を構築または更新することです。木の状態。どちらのメカニズムも、実装に統合ポイントが必要です。

おすすめ

転載: blog.csdn.net/lyabc123456/article/details/129116380#comments_27492523