Swift Digest
SE-0308 | Swift Evolution

#if for postfix member expressions

Proposal
SE-0308
Authors
Rintaro Ishizaki
Review Manager
Saleem Abdulrasool
Status
Implemented (Swift 5.5)

01 何が問題だったのか

Swift の #if ... #endif は、従来は文(statement)単位の括りでしか書けませんでした。各節の本体は完全な文の並びでなければならず、式の一部分だけを条件付きで差し込むことはできません。特に result builder を多用する SwiftUI のようなコードで、ベース式に続くメソッドチェーンのうち一部の修飾だけをプラットフォーム別に切り替えたい、という要求に応えられませんでした。

たとえば次のように、Text に対して iOS のときだけ .iOSSpecificModifier() を挟み、最後に共通の .commonModifier() を適用したい場合を考えます。

VStack {
  Text("something")
#if os(iOS)
    .iOSSpecificModifier()
#endif
    .commonModifier()
}

このコードは現状パースできないため、同じ効果を得るには次のように書き換えなければなりませんでした。

VStack {
  let basicView = Text("something")
#if os(iOS)
  basicView
    .iOSSpecificModifier()
    .commonModifier()
#else
  basicView
    .commonModifier()
#endif
}

これは .commonModifier() が重複しており、見た目も冗長です。重複を避けようとすると、

VStack {
  let basicView = Text("something")
#if os(iOS)
  let tmpView = basicView.iOSSpecificModifier()
#else
  let tmpView = basicView
#endif
  tmpView.commonModifier()
}

のように中間変数を導入することになり、さらに読みにくくなります。ベース式に続くメソッドチェーンのうち「一部分だけ」を差し替えたいだけなのに、式の構造自体を大きく崩さなければ書けないのが問題でした。

02 どのように解決されるのか

#if ... #endif を postfix member expression(ベース式に続く . から始まるメンバー式)にも適用できるように拡張します。ベース式のあとにそのまま #if を続け、各節の本体を . から始まる後置式の並びにできるようになります。

使い方

冒頭の例は、次のようにそのまま書けます。

VStack {
  Text("something")
#if os(iOS)
    .iOSSpecificModifier()
#endif
    .commonModifier()
}

より一般には、次のように #if の本体で連続した後置式をまとめて切り替えられます。

baseExpr
#if CONDITION
  .someOptionalMember?
  .someMethod()
#else
  .otherMember
#endif

CONDITION が真のときは baseExpr.someOptionalMember?.someMethod()、偽のときは baseExpr.otherMember として解釈されます。

書ける形と書けない形

postfix-ifconfig-expression としてパースされるのは、#if 節の本体が . に続けて識別子・キーワード・整数リテラルで始まる場合だけです。この条件に合わないときは、#if ... #endif は式の一部ではなく通常のコンパイラ制御文として解釈されます。

一度 . で始めたあとは、メソッド呼び出し・subscript・forced value(!)・optional chaining(?)・後置演算子など、後置式のサフィックスを自由に続けられます。

// OK
baseExpr
#if CONDITION_1
  .someMember?.otherMethod()![idx]++
#else
  .otherMethod(arg) {
    // ...
  }
#endif

一方、後置式ではない二項演算子などで続けることはできません。

// ERROR
baseExpr
#if CONDITION_1
  .someMethod() + 12 // error: unexpected tokens in '#if' expression body
#endif

また、本体に式以外のトークン(別の文など)を混ぜることもできません。

#elseif#else の本体は空にしてもかまいません。空でない場合は #if 節と同じルール(. 始まりで、後置式のみ、式以外を含まない)が適用されます。

// OK
baseExpr
#if CONDITION_1
  .someMethod()
#elseif CONDITION_2
  // OK. Do nothing.
#endif

連結とネスト

postfix の #if ... #endif ブロックは続けて書けますし、そのあとにさらに通常の後置式を続けても構いません。

baseExpr
#if CONDITION_1
  .someMethod()
#endif
#if CONDITION_2
  .otherMethod()
#endif
  .finalizeMethod()

#if ブロックをネストすることもでき、内側の #if も同じ postfix-ifconfig-expression のルールに従います。さらに、postfix の #if は他の式や文の内側に埋め込むこともできます。

someFunc(
  baseExpr
    .someMethod()
#if CONDITION_1
    .otherMethod()
#endif
)

通常の #if 本体を式として扱う副次的な変更

この提案に合わせて、パーサは #if 条件の直後に続く .identifier を条件式の一部として取り込まないようになります。その副次的な効果として、通常の #if ... #endif の本体に「. から始まる式」だけを書くパターンも有効になります。たとえば次のコードは従来はエラーでしたが、有効な Swift コードとして受理されるようになります。

enum MyEnum { case foo, bar, baz }

func test() -> MyEnum {
#if CONDITION_1
  .foo
#elseif CONDITION_2
  .bar
#else
  .baz
#endif
}

後置メンバー式はコンパイル条件の位置に書ける要素ではなかったため、この変更で既存コードの意味が変わってしまうことはありません。