LoginSignup
13
2

More than 3 years have passed since last update.

if文をメソッドチェーンにしてしまう

Last updated at Posted at 2019-12-11

じょ

何言ってるんだ?というタイトルですがこういうことです。

let a = 0
ifTrue(a == 0) { print("0") }
    .elseIf(a == 1) { print("1") }
    .else { print("other") }

先日メソッドチェーン原理主義という言葉を思いつきまして、メソッドチェーン原理主義者であれば制御構文もメソッドチェーンでなければ許されないのではないかと考え作ってみました。

賢明な皆さまはもうお気づきだと思いますが、このエントリは「特に何の意味もない」エントリです。戯れ言には付き合わないタイプの方はすぐさまお引き取り頂ければ幸いです。

なお、以下のコードはすべてSwift5.1で書かれています。

最初の実装

メソッドチェーンを行うためにはチェーンを繋ぐ「糊」に当たるものが必要です。今回はこんな感じです。

enum IfResult {
    case `true`, `false`
}

もう既にバカっぽさ全開です。
メソッド群は条件が真のときはIfResult.trueを偽の時はIfResult.falseを戻すように作成します。

入口

IfResultを使って早速メソッドチェーンの起点となる関数を作成します。

private func doAndTrue(_ f: () -> Void) -> IfResult {
    (f() is Void) ? .true : .false
}

func ifTrue(_ condition: @autoclosure () -> Bool, _ excute: () -> Void) -> IfResult {
    condition() ? doAndTrue(excute) : .false
}

頭のおかしすぎる関数が混ざっていますが、ifTrue(_:_:)自体は難しくはないですね。
当然のようにdoAndTrue(_:)関数で警告が出ていますが無視しましょう!(後で消えてなくなるのでご安心ください)

繋げる

起点はできましたのでIfResultを拡張してメソッドチェーンが作れるようにしていきます。

enum IfResult {
    case `true`, `false`

    @discardableResult
    func elseIf(_ condition: @autoclosure () -> Bool, _ excute: () -> Void) -> Self {
        self == .false && condition() ? doAndTrue(excute) : self
    }
}

IfResultを戻したメソッドの条件が真であった場合は以降の処理はすべてキャンセルされます。
ですので、IfResultfalseの時だけ条件を検査し処理するようにします。

さらにもう一つ

enum IfResult {
    case `true`, `false`

    func `else`(_ excute: () -> Void) {
        self == .false ? excute() : ()
    }
}

これは簡単ですね。

できた

では冒頭で提示したメソッドチェーンを実行してみましょう

func f(_ a: Int) {
    ifTrue(a == 0) { print("0") }
        .elseIf(a == 1) { print("1") }
        .else { print("other") }
}

(0...3).forEach(f)
// prints ...
// 0
// 1
// other
// other

ちゃんと動いてますね。

改良された実装

ちゃんと動いてはいますが本当にこれでいいでしょうか?
else(_:)メソッドが何も戻さないためメソッドチェーンはここで途切れることになります。
次につながるようになっていた方がよさそうです。では次に何を渡せばいいでしょう。
やはり何らかの値を伝播したいですね。
例えば

let a: String = ifTrue(cond) { "なにか" }.else { "なんだ?" }

のような感じです。

メソッドチェーンを次につなげるために先の実装を改良していきましょう。

enum IfResult<Value> {
    case `false`
    case value(Value)

    struct IfResultError: Error {}
    func get() throws -> Value {
        guard case let .value(v) = self else { throw IfResultError() }
        return v
    }
}

値を持てるようにしました。
またget()メソッドで値そのものを取り出せるようにしました。

入口

ifTrue(_:_:)メソッドを改修します。

func ifTrue<T>(_ condition: @autoclosure () -> Bool, _ excute: () -> T) -> IfResult<T> {
    condition() ? .value(excute()) : .false
}

不格好なdoAndTrue(_:)が不要になりすっきりしました。

繋げる

elseIf(_:_:)else(_:)もサクッと実装します。
再掲になる部分は除いています

enum IfResult<Value> {
    case `false`
    case value(Value)

    @discardableResult
    func elseIf(_ condition: @autoclosure () -> Bool, _ excute: () -> Value) -> Self {
        switch self {
        case .false: return condition() ? .value(excute()) : .false
        case .value: return self
        }
    }

    @discardableResult
    func `else`( _ excute: () -> Value) -> Self {
        switch self {
        case .false: return .value(excute())
        case .value: return self
        }
    }
}

ちょっと長くなってしまいましたがごく素直な実装です。

できた

新しいメソッドチェーンを見てみましょう。

func f(_ a: Int) -> IfResult<String> {
    ifTrue(a == 0) { "0" }
        .elseIf(a == 1) { "1" }
        .else { "other" }
}

(0...3)
    .map(f)
    .map { r in Result { try r.get() } }
    .forEach { _ = $0.map { print($0) } }
// prints ...
// 0
// 1
// other
// other

問題なく動いています。
完成です。

拡張された実装

ホントに完成?
値を取り出すのにResultに頼っています。これはちょっとどうかと思います。
なので実装を拡張します。

もなど

extension IfResult {
    @discardableResult
    func map<T>(_ transform: (Value) -> T) -> IfResult<T> {
        if case let .value(v) = self { return .value(transform(v)) }
        return .false
    }

    @discardableResult
    func flatMap<T>(_ transform: (Value) -> IfResult<T>) -> IfResult<T> {
        if case let .value(v) = self { return transform(v) }
        return .false
    }
}

利用例

func fizzBuzz1(_ i: Int) -> IfResult<String> {
    ifTrue(i < 1) { fatalError("あかんて") }
        .elseIf(i.isMultiple(of: 3) && i.isMultiple(of: 5)) { "FizzBuzz" }
        .elseIf(i.isMultiple(of: 3)) { "Fizz" }
        .elseIf(i.isMultiple(of: 5)) { "Buzz" }
        .else { String(i) }
}

(1...100)
    .map(fizzBuzz1)
    .forEach { $0.map { print($0) } }
_人人人人人人人人人_
> 突然のFizzBuzz <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄

比較

ついでなのでEquatableにしておきましょう。
Valueの型がEquatableでないと比較することができないので制約付きのextensionになります。

extension IfResult: Equatable where Value: Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool {
        switch (lhs, rhs) {
        case (.false, .false): return true
        case let (.value(v1), .value(v2)): return v1 == v2
        default: return false
        }
    }
}

まとめ

突然の思い付きにしてはよくできたのではないでしょうか?
関数は可能な限り1つのメソッドチェーンで完結するように書いています。
いくつか難しいものがあり妥協がたくさんありました。
真のメソッドチェーン原理主義者様ならそれらもメソッドチェーンで書くのでしょう。

告白

最後に真実を告白させていただきます。
実は私はメソッドチェーン原理主義者ではありません。
ですのでこのエントリで作成したメソッド群は使うことはないでしょう。
使う人がいるのかも疑問です。

ちゃんと最初に「特に何の意味もない」戯れ言だって断ってるからね?
怒っちゃだめよ。

13
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
13
2