[Swift] “本当の”CharacterSetを作ったよ。

# はじめに

以前、[「CharacterSetはCharacterのsetではありませんよ？」という記事](https://qiita.com/YOCKOW/items/13360d7245c491932673)を書きました。簡単に言うと、「`Foundation`フレームワークで提供される`CharacterSet`は、`Character`のsetではなく`Unicode.Scalar`のsetだ」ということです。
でもやっぱり、それで投げっぱなしは良くないと思ったのです。

というわけで、作りました。`Character`のsetをね。
そして、[SwiftBonaFideCharacterSet](https://github.com/YOCKOW/SwiftBonaFideCharacterSet)という名前でGitHubに上げました。

# `Character`のsetを作る過程

`Character`のsetを作ろうと思っても一筋縄では行かなかったので、その壁をいくつかここに記載しておこうと思います。

## `Character`の種類は無限大！

Unicodeのスカラ値は所詮U+0000からU+10FFFFまでの範囲内に収まります。しかし、`Character`はそうはいきません。[結合文字](https://ja.wikipedia.org/wiki/%E7%B5%90%E5%90%88%E6%96%87%E5%AD%97)を組み合わせることでいくらでも文字(Unicode的には「拡張書記素クラスタ」)を作れます。
たとえば、U+20DDは"COMBINING ENCLOSING CIRCLE"といって、前の文字に丸をつけるための結合文字です。そして、結合文字は基本的にいくつ繋げてもいいことになっています(それに意味があるかどうかは別として)。
Swiftで書くと：

```Swift
print("A".count) // -> 1
print("A\u{20DD}".count) // -> 1
print("A\u{20DD}\u{20DD}".count) // -> 1
print("A\u{20DD}\u{20DD}\u{20DD}".count) // -> 1
print("A\u{20DD}\u{20DD}\u{20DD}\u{20DD}".count) // -> 1
```

となります。

たとえば、`Character`の集合として、`{ c | c は"A"から"Z"までの文字またはその文字に結合文字が結合した文字 }`を定義したいとして、この集合は(理論的には)無限集合となります[^uncountably_infinite_set]。

[^uncountably_infinite_set]: 非可算無限集合かな？

高々有限個の文字の集合であれば`Set<Character>`を使えますが、汎用的な`Character`のsetを考えるなら、自分で実装するしかありません。

## Rangeの種類が足りない！

というわけで、[前回の記事](https://qiita.com/YOCKOW/items/4870f70b007e83284a85)のように[predicate](https://en.wikipedia.org/wiki/Predicate_(mathematical_logic))で集合を定義することを考えました。そうすれば、有限だろうが無限だろうが集合を定義することはできるはずです。
`Character`は`Comparable`プロトコルに準拠しているので、`Range`を使えば`Character`の集合を表すことができそうです。たとえば、前述の`{ c | c は"A"から"Z"までの文字またはその文字に結合文字が結合した文字 }`を`{ c | cは"A"以上"["未満の文字 }`[^u_5b]というように考えれば、Swift的には`"A"..<"["`という`Range`に含まれる文字というpredicateを集合の定義に使えば良さそうです。

[^u_5b]: "Z"がU+005Aで、"["がU+005Bなので…とりあえず。

しかし、今考えているのは汎用的な`Character`のsetなので、`SetAlgebra`に準拠させることを考えなくてはいけません。そこで問題となるのが、たとえば`subtracting(_:)`といったメソッドです。

さて、突然ですが、《`"A"..<"["`という`Range`で定義される集合》から《`"A"..."C"`という`ClosedRange`で定義される集合》を引いたらどうなるでしょう？
アルファベットでCの次はDだから…《`"D"..<"["`という`Range`で定義される集合》ですか？
**いいえ、違います。**
さっき紹介したU+20DDという結合文字を使った`"C\u{20DD}"`という`Character`について考えてみましょう。`"C\u{20DD}"`は当然`"A"..<"["`に含まれます。そして、`"C\u{20DD}"`は"C"よりも大きいので`"A"..."C"`には含まれません。即ち【《`"A"..<"["`という`Range`で定義される集合》から《`"A"..."C"`という`ClosedRange`で定義される集合》を引いた集合】に`"C\u{20DD}"`は含まれているはずです[^set_difference]。

[^set_difference]: [差集合](https://ja.wikipedia.org/wiki/%E5%B7%AE%E9%9B%86%E5%90%88)の定義から

したがって、この問題の答えは`{ c | cは"C"より大きく"["未満の文字 }`で示される集合となります。しかし、残念ながらSwiftには下端(lower bound)を含まない範囲を表す構造体が標準では存在しません。
というわけで、そういった範囲を表す構造体を実装する必要があります。それが、以前2回に渡って記事にした[^ranges][SwiftRanges](https://github.com/YOCKOW/SwiftRanges)です。

[^ranges]: [Rangeの種類が足りない！①](https://qiita.com/YOCKOW/items/0cf8ababc9b81b730fc3), [Rangeの種類が足りない！②](https://qiita.com/YOCKOW/items/d670a347757d4bc0e812)

こうして、なんとか`Character`のsetを作ることができそうなところまで来ました。


## `Character`の比較結果が安定しない

`Character`が`Comparable`に準拠しているのは確かなのですが、Swiftのバージョンによってその比較結果が違う場合があるのです[^character_comparison]。

[^character_comparison]: これは[SR-530](https://bugs.swift.org/browse/SR-530)の[修正](https://github.com/apple/swift/pull/13877)と関連しているのかもしれません(未確認)。

たとえば、"Ä"という文字があります。Unicodeにおける名前は"Latin Capital Letter A with Diaeresis"で、Aというアルファベットにウムラウト(またはトレマ)が付いた文字です。スカラ値はU+00C4が割り当てられていますが、当然"A"(U+0041)に結合文字としてのウムラウト(U+0308)が続く2つのスカラ値で表される書記素クラスタも同じ文字を表します:

```Swift
"\u{00C4}" == "\u{0041}\u{0308}" // -> true
```

問題はこの文字が"B"より小さいのかどうかということです:

```Swift
"\u{00C4}" < "B" // -> 結果は？
```

結論から言うと、Swift 4.1.2 (Xcode 9.4.1)では`true`です。そして、Swift 4.2 (Xcode 10)では`false`です。さらにいえば、(意外なことに？)Swift 4.1.50 (Xcode 10のSwift 4.1互換モード)でも`false`です。

このことを把握していればいいかもしれませんが、場合によってはSwiftのバージョンを意識せずにコードを書いてバグを生み出す可能性もあります。

というわけで、SwiftBonaFideCharacterSetでは、[`DecomposedCharacter`](https://github.com/YOCKOW/SwiftBonaFideCharacterSet/blob/master/Sources/BonaFideCharacterSet/DecomposedCharacter.swift)と[`PrecomposedCharacter`](https://github.com/YOCKOW/SwiftBonaFideCharacterSet/blob/master/Sources/BonaFideCharacterSet/PrecomposedCharacter.swift)という構造体を用意。それぞれ、常にNFDとNFCでnormalizeされ、比較の際にはユニコードスカラ値の配列として大小を比較する設計となっています:

```Swift
DecomposedCharacter("Ä") < DecomposedCharacter("B")
// 常にtrue

PrecomposedCharacter("Ä") < PrecomposedCharacter("B")
// 常にfalse
```

こうしてsetのほうも`BonaFideCharacterSet`のほか、`DecomposedCharacterSet`と`PrecomposedCharacterSet`も用意することになりました。


### そんなにたくさん構造体を実装したくない

でも、同じような構造体を3つも別々で実装するなんて面倒ですよね。[DRY](https://ja.wikipedia.org/wiki/Don%27t_repeat_yourself)という言葉も耳にタコができるぐらい聞きますし。
そこでまずは、[`CharacterExpression`というプロトコル](https://github.com/YOCKOW/SwiftBonaFideCharacterSet/blob/master/Sources/BonaFideCharacterSet/CharacterExpression.swift)を用意して、`Character`も`DecomposedCharacter`も`PrecomposedCharacter`も、そのプロトコルに準拠することにします[^normalized_character]。

[^normalized_character]: 実際には`DecomposedCharacter`と`PrecomposedCharacter`は共通点が多いので、`CharacterExpression`を継承した[`NoramalizedCharacter`というプロトコル](https://github.com/YOCKOW/SwiftBonaFideCharacterSet/blob/master/Sources/BonaFideCharacterSet/NormalizedCharacter.swift)を挟んでいます。

そうすれば、あとは前回の記事で紹介した`TotallyOrderedSet<Element>`を使って次のようにするだけでよくなります：

```Swift:BonaFideCharacterSet.swift
public typealias CharacterExpressionSet<C> = TotallyOrderedSet<C> where C:CharacterExpression
public typealias BonaFideCharacterSet = CharacterExpressionSet<Character>
public typealias DecomposedCharacterSet = CharacterExpressionSet<DecomposedCharacter>
public typealias PrecomposedCharacterSet = CharacterExpressionSet<PrecomposedCharacter>
```

そして、たとえば`StringProtocol`に対する拡張も次のように簡単にできます：

```Swift:StringProtocol+CharacterExpressionSet.swift
extension StringProtocol {
  public func split<C>(separator:CharacterExpressionSet<C>,
                       maxSplits:Int = Int.max,
                       omittingEmptySubsequences:Bool = true) -> [SubSequence]
    where C:CharacterExpression
  {
    return self.split(maxSplits:maxSplits,
                      omittingEmptySubsequences:omittingEmptySubsequences,
                      whereSeparator:{ separator.contains(C($0)) })
  }
}
```

これでDRYは達成…ですかね？


# おわりに

というわけで、“本当の”CharacterSetを作ってみたというお話でした。ただ、作ってみたのはいいけど、役に立つのかどうかは分からないというオチなのでした。