【Swift】スコープ関数が欲しい→パイプライン演算子になった

Swift にも Kotlin の「スコープ関数」のようなものが欲しい、と思って試行錯誤していたら、
いくつかのプログラミング言語に導入されている「パイプライン演算子」になりました。


## 困りごと

まずある処理 a を行い、次にその結果を使って処理 b を行い、更にその結果を使って処理 c を行い…というようなことがあります。

そのような場合、ローカル変数が大量になるか、

```Swift
let resultOfA = a()
let resultOfB = b(resultOfA)
let resultOfC = c(resultOfB)
```

入れ子が深くなり、

```Swift
c(b(a()))
```

コードが読みにくくなってしまいます。


## Kotlin のスコープ関数

プログラミング言語 Kotlin には「[スコープ関数](https://kotlinlang.org/docs/reference/scope-functions.html)」という関数群があります。
先ほどのような場合でも、これらを使うとチェーン呼び出しでローカル変数や入れ子を少なく書くことができます。

先ほどの例であれば次のように書けます。
（以下、Kotlin の関数を説明する際にも Swift 式の記法で書きます。）

```Swift
a().let { b($0) }.let { c($0) }
```

スコープ関数はどんなオブジェクトに対しても使えます。

以下で各スコープ関数について簡単に説明します。

なお、以下では次の用語を使用します。

- レシーバー：`s.f { ... }` における `s`。
- ブロック：`s.f { ... }` における `{ ... }`。 

### 4つのスコープ関数

スコープ関数には `let`、`also`、`run`、`apply` の4つ[^with]があります。

[^with]: `with` 関数というものもありますが、ちょっと形式が異なりますし、`run` で書き換えられるので、省略します。

#### `let` 関数

レシーバーを引数にしてブロックを呼び出し、その返値を返します。

```Swift
print(
    "app".let { $0 + "le" }
) // > apple
```

#### `also` 関数

レシーバーを引数にしてブロックを呼び出し、レシーバーを返します。

```Swift
print(
    "app".also {
        print($0 + "le") // > apple
    }
) // > app
```

#### `run` 関数

レシーバーを `self` としてブロックを呼び出し、その返値を返します。

```Swift
print(
    "apple".run { dropLast(2) }
) // > app
```

#### `apply` 関数

レシーバーを `self` としてブロックを呼び出し、レシーバーを返します。

```Swift
print(
    "apple".apply {
        print(dropLast(2)) // > app
    }
) // > apple
```

### `takeIf` と `takeUnless`

スコープ関数ではありませんが、似たところがある標準の関数に `takeIf` と `takeUnless` があります。

#### `takeIf` 関数

レシーバーを引数としてブロックを呼び出し、その返値が `true` ならレシーバーを、`false` なら `nil` を返します。

```Swift
print(
    "apple".takeIf { !$0.isEmpty } ?? "(empty)"
) // > apple
print(
    "".takeIf { !$0.isEmpty } ?? "(empty)"
) // > (empty)
```

#### `takeUnless` 関数

レシーバーを引数としてブロックを呼び出し、その返値が `true` なら `nil` を、`false` ならレシーバーを返します。

```Swift
print(
    "apple".takeUnless { $0.isEmpty } ?? "(empty)"
) // > apple
print(
    "".takeUnless { $0.isEmpty } ?? "(empty)"
) // > (empty)
```


## `run` と `apply` は諦める:see_no_evil:

いきなりですが、`run` と `apply` は実現を諦めました。

前述の通り、関数 `run` および `apply` はレシーバーを `self` としてブロックを呼び出します。
Swift でこれを実現する方法はなさそうです。（あれば教えてください:bow:）

これらはそれぞれ `let` と `also` で代替できます。
`self.` を省略して書けたはずのところを `$0.` と書けばよいだけなので、なくてもそれほど困りません。


## extention（拡張）[^extension]で実装してみる

[^extension]: 「拡張」というと一般的な用語すぎて違和感があるので、「extension」と書くことにします。

Kotlin のスコープ関数は拡張関数という仕組みを使って実装されています。
拡張関数を使うと、任意の型に対して、その型を定義したソースコードを書き換えることなく、メンバ関数を追加することができます。

Swift の extension と似ていますね。
なので extension で実装してみましょう。

```Swift
protocol Scopable { }
extension Scopable {
    @inline(__always) func `let`<R>(block: (Self) -> R) -> R {
        block(self)
    }
    @inline(__always) func also(block: (Self) -> Void) -> Self {
        block(self)
        return self
    }
}
```

使用例：

```Swift
extension String: Scopable { }
extension Int: Scopable { }

"2".let { Int($0)! }.let { $0 + 3 }.also { print($0) } // > 5
```

チェーンで簡潔に書けるようになりました！

しかし `Any` 型に対して extension を宣言することができないため、
スコープ関数を生やしたい型それぞれに `extension` を宣言しないといけないのが面倒です。


## 演算子オーバーロード

演算子オーバーロードを使うのはどうでしょうか。

`let` 関数の代わりとして仮に `||` 演算子をオーバーロードしてみると次のようになります。

```Swift
@inline(__always) func || <T, R>(receiver: T, block: (T) -> R) -> R {
    block(receiver)
}
```

使用例：

```Swift
"2" || { Int($0)! } || { $0 + 3 } || { print($0) } // > 5
```

うまくいきました！

あれ、これどこかで見たような…
ってこれ、パイプライン演算子やんけ!


## パイプライン演算子

パイプライン演算子は、いくつかのプログラミング言語に導入されている二項演算子です。
最近だと JavaScript に実験的に導入されました。
ほとんどの言語では `|>` で表されます。
`A |> B` は `B(A)` と等価です。`A |> B |> C` は `C( B( A ) )` と等価です。


## カスタム演算子 `|>` を実装する

では `|>` 演算子を実装しましょう。

まず、演算子の結合規則（結合方向）と結合の優先順位を決める必要があります。
`A |> B |> C` が `( A |> B ) |> C` と等価になって欲しいので、左結合ですね。
優先順位については、[JavaScript では `||` より低く三項演算子よりは高い](https://tc39.es/proposal-pipeline-operator/#sec-intro)としていますので、それに倣いましょう。

```Swift
precedencegroup PipelinePrecedence {
    associativity: left
    lowerThan: LogicalDisjunctionPrecedence
    higherThan: TernaryPrecedence
}
```

カスタム演算子を宣言します。

```Swift
infix operator |>: PipelinePrecedence
```

演算子をオーバーロードします。

```Swift
@inline(__always) func |> <T, R>(receiver: T, block: (T) -> R) -> R {
    block(receiver)
}
```

使用例：

```Swift
print(
    "2" |> { Int($0)! } |> { $0 + 3 }
) // > 5
```

できました！


## `also` なども演算子にする？

`also` も同じように演算子にしましょうか。

演算子の名前には記号類しか使えません。
→[Lexical Structure — The Swift Programming Language (Swift 5.3) # Operators](https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html#ID418)
`also` の挙動がイメージできるような名前は…。

そうだ、`takeIf` や `takeUntil` も欲しいですよね。
これらに対応する演算子も考えないと。

…やめましょう。
新しい演算子をいくつも作っても読みづらいだけです。
別の方法を考えましょう。[^also演算子]

[^also演算子]: もし「これらもカスタム演算子にしたい」という方がいれば：`also`→`|^`、`takeIf`→`?:`、`takeUnless`→`?!`、はいかがでしょうか。


## サポート関数

```Swift
"2".also { print($0) }.let { Int($0)! }.let { $0 + 3 }.also { print($0) }
```

↑を↓のように書けるようにするのはどうでしょうか。

```Swift
"2" |> also { print($0) } |> { Int($0)! } |> { $0 + 3 } |> also { print($0) }
```

これなら分かりやすく、演算子名に悩むこともありません。

この方針で `also` を実装するとこうなります。

```Swift
@inline(__always) func also<T>(block: @escaping (T) -> Void) -> (T) -> T {
    return {
        block($0);
        return $0;
    }
}
```

`also` 関数は `(T) -> T` 型の関数を返します。
その関数は引数を1つ受け取り、それをそのまま返します。
また、その関数は、`also` 関数の引数として渡された `(T) -> T` 型の関数 `block` を実行します。

これで先ほどの記法を実現できるようになりました。

```Swift
"2"
    |> also { print($0) } // > 2
    |> { Int($0)! }
    |> { $0 + 3 }
    |> also { print($0) } // > 5
```

`takeIf` と `takeUntil` も実装しておきましょう。

```Swift
@inline(__always) func takeIf<T>(predicate: @escaping (T) -> Bool) -> (T) -> T? {
    return {
        predicate($0) ? $0 : nil
    }
}
@inline(__always) func takeUnless<T>(predicate: @escaping (T) -> Bool) -> (T) -> T? {
    return {
        predicate($0) ? nil : $0
    }
}
```

使用例：

```Swift
""
    |> takeUnless { $0.isEmpty }
    |> { $0 ?? "(empty)" }
    |> also { print($0) } // > (empty)
```


## 懸案

クロージャ式を大量に書くことになるため、
パフォーマンスは少々落ちるかもしれません。[^パフォーマンス]
最適化してくれるかな？（詳しい方、ご意見をいただければ幸いです:bow:）

[^パフォーマンス]: Kotlin の場合は、インライン関数（スコープ関数はすべてインライン関数です）の引数として書かれたクロージャ式（Kotlin ではラムダといいます）はすべてインライン化されるので、パフォーマンスの問題はありません。


## まとめ

Swift にスコープ関数のようなものが欲しい場合、
スコープ関数 `let` の代わりにパイプライン演算子 `|>` を実装するとよい。
`also` などは、パイプライン演算子と併せて使うサポート関数で実現する。

実装：

```Swift
precedencegroup PipelinePrecedence {
    associativity: left
    lowerThan: LogicalDisjunctionPrecedence
    higherThan: TernaryPrecedence
}
infix operator |>: PipelinePrecedence
@inline(__always) func |> <T, R>(receiver: T, block: (T) -> R) -> R {
    block(receiver)
}

@inline(__always) func also<T>(block: @escaping (T) -> Void) -> (T) -> T {
    return {
        block($0);
        return $0;
    }
}
@inline(__always) func takeIf<T>(predicate: @escaping (T) -> Bool) -> (T) -> T? {
    return {
        predicate($0) ? $0 : nil
    }
}
@inline(__always) func takeUnless<T>(predicate: @escaping (T) -> Bool) -> (T) -> T? {
    return {
        predicate($0) ? nil : $0
    }
}
```

使用例：

```Swift
"2"
    |> also { print($0) } // > 2
    |> { Int($0)! }
    |> { $0 + 3 }
    |> also { print($0) } // > 5

""
    |> takeUnless { $0.isEmpty }
    |> { $0 ?? "(empty)" }
    |> also { print($0) } // > (empty)
```

`run` や `apply` のような、ブロック内の `self` を書き換えるような関数は、Swift では実現できないので諦める。

パフォーマンスは少々落ちるかもしれない。

／以上