1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SwiftでWhen-Then-Elseを表現してみる

Last updated at Posted at 2020-12-14

こんにちは、iOSエンジニアの paper_and_paper です。

Kotlinには switch文がないって。
デビュー当初は思わず疑ってしまいましたが... もちろん周知の事実です。
では、何で制御するかというと when式 という訳です。

今回は、Kotlinのwhen文をSwiftで実現してみよう〜というテーマでSwiftで頑張ってみました。
あくまでチャレンジなので、特別なメリット・デメリットは何もありません。敢えて言えば、Kotlinのwhen式についてiOS エンジニアでも理解できるくらいです。

どうぞよろしくお願いします🙏

1. when文の簡単なおさらい

1-1. when式の書き方

when(){
    1 -> 式の結果と値1が一致した場合の処理
    2 -> 式の結果と値2が一致した場合の処理
    ...
    else -> 式の結果がどの値とも一致しない場合の処理
}

whenに渡した式の結果により、処理を分岐します。最後のelseには、上記の値のいづれでもない場合の処理を記述しますが、必須ではありません。(nullableを扱う場合は例外です)

Kotlin の公式ドキュメント:
https://kotlinlang.org/docs/reference/control-flow.html#control-flow-if-when-for-while

なお、switch文で必要だったbreakも不要です。

val num = Random.nextInt(5)
when(num){
    0 -> println("大吉")
    1 -> println("中吉")
    2 -> println("小吉")
    4 -> println("大凶")
    else->println("吉")
}

1-2. whenも式である

when は式なので、その分岐結果から値を返すことができます。返す値の記述方法はif式と同じ、該当するブロックの最後に値を書いておきます。

Kotlin
val value = "hoge"
val result = when(value) {
    "hoge" -> "ほげ"
    "fuga" -> "ふが"
    else -> "piyo"
}
println(result) // => ほげ

(余談) Swift だとこんなイメージになるのかな...?

Swift
let value = "hoge"
let result = { v -> String in
   switch v {
   case "hoge":
      return "ほげ"
   case "fuga":
      return "ふが"
   default:
      return "piyo"
   }
}(value)
print(result)  // => ほげ

1-3. if式をwhenで代替できる

whenの引数を省略した場合、条件分岐はシンプルなBooleanを返す式となります。そのため、シングルアロー (->) の左辺の結果が true の場合には、右辺の処理が実行されます。つまり、whenを if-else if の代替として使用することもできます。

when {
   Booleanを返す式1 -> 処理1
   Booleanを返す式2 -> 処理2
   ...
   else -> 処理n
}

1-4. 型のチェックとの組み合わせ

Kotlin にも変数の型をチェックするため is演算子があります。これとwhenを組み合わせて、ある変数が目的の型かどうかをチェックすることが可能です。ある変数が目的の型かどうかをチェックすれば、チェック済みの型を持つ変数として使用できます。

val a: Any = "Kotlin"
when(a) {
   is Int -> print(a + a) // aはキャストなしでInt型として扱える
   is String -> println(a.toUpperCase()) // aはキャストなしでString型として扱える
}

1-5. switch文との主な相違点

  • switch が whenになった
  • case: が -> になった
  • 各条件の処理の終わりには break が不要となった
  • default が else になっている

2. Swiftで実現したいこと💪

早速、始めていきましょう。

今回はSwiftでwhen式ライクな記述を表現するにあたり、
次のようなa-cを満たすような実装を検討していきます。

a. if-else構文としてのWhen式

1-3のようにif式をwhenで代替できる。

let isProduction = true
when(isProduction)
    .then { print("production configuration is loaded") } // prints
    .else { print("debug configuration is loaded") }

b. 返り値を扱ったif-elseのWhen式

1-2のようにwhenを式として扱える。例えば、whenの返り値を結果として出力できる。

let isProduction = false
let state = when(isProduction)
    .then(1)
    .else(0)

print(state) // prints 0

b. 複数ケースに関するマッチング制御としてのWhen式


let price = 1000
when(price)
    .greater(than: 2000) { print("Mercedes-benz") }
    .greaterThan(orEqual: 1000) { print("BMW") } // prints
    .equal(to: 800) { print("AUDI") }
    .equal(to: 600) { print("Peugeot") }
    .lower(than: 200) { print("Aqua") }
    .lowerThan(orEqual: 100) { print("No Car") }
    .else { print("Cyber Truck") }
// 出力結果
"BMW"

let newAge = 0
when(newAge)
    .greater(than: 50) { print("Bentley") }
    .greaterThan(orEqual: 40) { print("AUDI RS6") }
    .equal(to: 40) { print("AUDI A3") }
    .equal(to: 20) { print("Ford Fiesta") }
    .in(0...5) { print("Remote Controlled Cars") } // prints
    .not(in: 0...5) { print("Metal Cars") }
    .found(in: [0, 1]) { print("Electric Bike!") } // prints
    .in(0..<5) { print("Bike") } // prints
    .not(in: 0..<5) { print("Trolley") }
    .else { print("Cyber Truck!!!") }

// 出力結果
"Remote Controlled Cars"
"Electric Bike!"
"Bike"

3. Whenをどう定義するか?

whenは なので、まずはグローバルな関数 when() として定義していきます。

func when<Value>(_ value: Value) -> Some<Value> {
    Some(input: value)
}

さらに返り値には Some<Value> という構造体を導入します。(Swift5.1から導入された Opaque Result Type some とは別物です。) この構造体は、then-elseでの構文制御の責務を任せる予定です。

struct Some<Input> {
    fileprivate let input: Input
    private let isTerminated: Bool

    init(input: Input, isTerminated: Bool = false) {
       self.input = input
       self.isTerminated = isTerminated
    }

    func `else`(_ execute: () -> Void) {
       guard !isTerminated else { return }
       execute()
    }

    func `else`<Value>(_ value: Value) -> Value {
        guard !isTerminated else { return value }
        return value
    }
}

when() で与えられたValueに応じてパターンマッチができるよう、上記ではInputという型パラメータを導入しています。型パラメータ value を使って then, else を定義した時に、引数からの型推論が成功すればコンシステントな仕組みが成立するはずです。

また、elseを省略するケースを実現するためisTerminatedフラグを導入することにします。(もっと良い方法があるはずだが...)通常は false なので else まで記述することになりますが、 true をセットしてあげれば else を省略させることができます。

その1. Inputの制約条件: Bool型

Someの拡張条件として、Bool型のInputを検討してみます。

extension Some where Input == Bool {
    @discardableResult
    func then( _ execute: () -> Void) -> Self {
        guard input else { return self }
        execute()
        return Some(input: input, isTerminated: true)
    }

    @discardableResult
    func then<Value>( _ value: Value) -> Some<Value> {
        guard input else {
            return Some<Value>(input: value, isTerminated: false)
        }
        return Some<Value>(input: value, isTerminated: true)
     }

}

その2. Inputの制約条件: Comparableプロトコルへの準拠

Someを拡張して、型パラメータ InputComparable プロトコル を制約条件として与えてあげれば、 range や closed range を使った比較なども実現することができます。

extension Some where Input: Comparable {

    @discardableResult
    func `in`(_ range: ClosedRange<Input>, _ execute: () -> Void) -> Some<Input> {
        guard range.contains(input) else { return self }
        execute()
        return Some(input: input, isTerminated: true)
    }

    @discardableResult
    func `in`(_ range: Range<Input>, _ execute: () -> Void) -> Some<Input> {
        guard range.contains(input) else { return self }
        execute()
        return Some(input: input, isTerminated: true)
    }

    @discardableResult
    func not(in range: ClosedRange<Input>, _ execute: () -> Void) -> Some<Input> {
        guard !range.contains(input) else { return self }
        execute()
        return Some(input: input, isTerminated: true)
    }

    @discardableResult
    func not(in range: Range<Input>, _ execute: () -> Void) -> Some<Input> {
        guard !range.contains(input) else { return self }
        execute()
        return Some(input: input, isTerminated: true)
    }

    @discardableResult
    func found(in values: [Input], _ execute: () -> Void) -> Some<Input> {
        guard values.contains(input) else { return self }
        execute()
        return Some(input: input, isTerminated: true)
    }

    @discardableResult
    func greater(than inputValue: Input, _ execute: () -> Void) -> Some<Input> {
        return compare(>, inputValue, execute)
    }

    @discardableResult
    func greaterThan(orEqual inputValue: Input, _ execute: () -> Void) -> Some<Input> {
        return compare(>=, inputValue, execute)
    }

    @discardableResult
    func lowerThan(orEqual inputValue: Input, _ execute: () -> Void) -> Some<Input> {
        return compare(<=, inputValue, execute)
    }

    @discardableResult
    func lower(than inputValue: Input, _ execute: () -> Void) -> Some<Input> {
        return compare(<, inputValue, execute)
    }

    @discardableResult
    func equal(to inputValue: Input, _ execute: () -> Void) -> Some<Input> {
        return compare(==, inputValue, execute)
    }

    /// Helper method for comparison
    @discardableResult
    private func compare(
        _ comparator: (Input, Input) -> Bool,
        _ inputValue: Input,
        _ execute: () -> Void
    ) -> Some<Input> {
        guard comparator(input, inputValue) else { return self }
        execute()
        return Some(input: input, isTerminated: true)
    }

}

完成👏

References

Control Flow: if, when, for, while - Kotlin
Swift Control Flow With When-Then-Else - medium.com

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?