Edited at

Optionalな文字列同士を良い感じに結合する(ついでにreduceとflatMapの実装を覗いてみる)

More than 3 years have passed since last update.

Optionalに包まれた文字列が複数あって、それを良い感じに結合したり整形したものを出力したい、ということが最近実装してて何度かありました。今回はそんな時にどういう実装をするのが良いかなー、というのを考えてみました。


前提


  • Optional型に包まれた複数の文字列が存在する

  • 例えば、下記の様な文字列の場合だと Optional("abd") という出力を期待

  • さらに、セパレーターをつけて Optional("a&b&d") とかにしたい時もある

let a:String? = "a"

let b:String? = "b"
let c:String? = nil
let d:String? = "d"


nilが1つでもあった場合は、nilを返したい


ぱっと思いつく実装

ぱっと思いつく実装だと、下記の様になります。

let result: String?

if let a=a, b=b, c=c, d=d {
result = "a&b&c&d"
} else {
result = nil
}

このパターンの場合はまぁぶっちゃけ上の実装でも良いかなという気はします。ただ、対象の文字列が3つとか4つなら問題なさそうですが、例えばこれが数十個あると考えると無限に続く if let 文を見るのはなかなか辛そうです。そこで、たくさんあるいは不特定の数のOptionalな文字列がやってきても苦しくない実装を一応考えてみました。


考えてみた

この場合、下記の様に実装することでnilが1つでも含まれてた場合はnilを、そうでなかった場合は&で結合した文字列を返すことが出来ます。

[a,b,c,d].reduce("", combine: {(left:String?, right:String?) -> String? in

guard let left = left, right = right else { return nil }
return left.isEmpty ? right : left + "&" + right
})

最初のguradで結合する左右いずれの値もnilではないことを担保し、nilでなかった場合のみ両者を&で結合する、という処理です。


reduceについて

上記は便利なreduceという関数を利用して実装しています。せっかくSwiftがオープンソースになったことだし、公開されたソースを見ながらreduceが何者なのかを見てみましょう。

reduceで実行されるのはいわゆる「畳み込み(accumulate)関数」と呼ばれる処理で、Haskellではfold, rubyなどではinjectといった名前で実装されています。reduceと言われると何ぞとなるのですが、集合を操作して1つの値に畳み込む関数、だと考えるとわかりやすいです。

Swift版の畳み込み関数reduceは2つの引数を取ります。一つ目は畳み込まれる値の初期値、もう一つは値をたたみ込む処理を実装したクロージャです。では、実装を見てみましょう。

extension SequenceType {

/// Return the result of repeatedly calling `combine` with an
/// accumulated value initialized to `initial` and each element of
/// `self`, in turn, i.e. return
/// `combine(combine(...combine(combine(initial, self[0]),
/// self[1]),...self[count-2]), self[count-1])`.
@warn_unused_result
public func reduce<T>(
initial: T, @noescape combine: (T, ${GElement}) throws -> T
) rethrows -> T {
var result = initial
for element in self {
result = try combine(result, element)
}
return result
}
}

コメント部分が長いですが、勘所は下記です。

    var result = initial

for element in self {
result = try combine(result, element)
}

まず、畳み込み結果の初期値をresultに代入しています。これは、reduceの第一引数で渡す初期値です。

次に、self(対象が配列の場合は配列自身)をforループで回して、すべての要素に対してreduceの第2引数で渡したcombineというクロージャを適用しています。combineは、現状のresultの値と要素を引数に取り、新たなresultを算出する処理であることがわかります。


余談1. .swift.gybについて

オープンソースとして公開されたSwiftのコードを読んでいると、時たま*****.swift.gybというファイルを目にすることがあります。上記のreduceの実装も、SequenceAlgorithms.swift.gybというファイルに実装されていました。

これは、swiftのプロジェクトで利用されているgybというテンプレート形式で、特定箇所の値を動的に変更するために利用されているようです。例えば、上記のreduceの実装では${GElement}という文字列が唐突に出てきますが、これはSequenceAlgorithms.swift.gybの冒頭で下記のように定義されています。

%{

# We know we will eventually get a SequenceType.Element type. Define
# a shorthand that we can use today.
GElement = "Generator.Element"
}%

あまり詳しく追ってはないのですが、このテンプレート機構はpythonで実装されており、swiftのリポジトリ内に格納されています

# GYB: Generate Your Boilerplate (improved names welcome; at least

# this one's short). See -h output for instructions

gybはGenerate Your Boilerplateの略。これ豆知識。


nilの場合はスキップして、すべての文字列を連結したい


ぱっと思いつく実装

大分脇道に逸れましたが、次にnilの場合はスキップして結合したいんだ!というときのパターンについて考えてみます。まず、ぱっと思いつく実装だと、下記の様になります。

var result = ""

if let a = a {
result2 = a
}
if let b = b {
result = result + "&" + b
}
if let c = c {
result = result + "&" + c
}
if let d = d {
result = result + "&" + d
}

辛くなりそうなオーラが漂っている、というか既に辛い感じになっています。これが数100行続くコードとかをみると、そっと画面を閉じたくなりますね。


考えてみた

そこで、さきほどと同じくたくさんのOptionalな文字列がやってきても辛くない実装を考えてみました。

[a,b,c,d].flatMap{$0}.reduce(nil, combine: {(left:String?, right:String?) -> String? in

guard let left = left else { return right }
return left + "&" + right!
})

まず、flatMapを利用してnilをフィルタリングした後にさきほどとん同じくreduceを利用して要素を結合しています。

最初にleftのみguardしているのは、一番最初にreduceの処理を行うときに初期値のnilが渡ってくるためです。それ以降はflatMapでfilter済みのnot nilな要素が必ず渡ってくるはずなのでright!とforce unwrapしてます。


flatMapとnilフィルタリング

え、なんでflatMapでnilをフィルタリングできるんだ!と疑問に思った方もいることでしょう。Swiftは既にオープンソースなので、例のごとく疑問に感じたときは実装をのぞいてみましょう。

  public func flatMap<T>(

@noescape transform: (${GElement}) throws -> T?
) rethrows -> [T] {
var result: [T] = []
for element in self {
if let newElement = try transform(element) {
result.append(newElement)
}
}
return result
}

これでnilをフィルターできるのも皆様納得。下記でtransform(element)の結果をオプショナルバインディングを使ってunwrapしていることがわかります。

if let newElement = try transform(element) {


余談2. パターンマッチで結合する

結合の対象が2つだった場合は、下記の様にパターンマッチを利用することで分岐して、結合することも出来ます。

let joined: String?

switch (a, b) {
case let (a?, b?):
joined = a + b
case let (a?, nil):
joined = a
case let (nil, b?):
joined = b
case (nil, nil):
joined = nil
}


余談3. 演算子を定義してみる

あまりカジュアルに利用するとメンテナンスしづらくなるのでご利用は計画的に、ですがswiftでは独自の演算子を定義することもできます。プロダクトで利用するかどうかはさておき、せっかくなので先ほど実装した2つの文字列を結合する処理を演算子として実装してみましょう。

infix operator +? {associativity left}

func +?(lhs: String?, rhs: String?) -> String?{
switch (lhs, rhs) {
case let (lhs?, rhs?):
return lhs + rhs
case let (lhs?, nil):
return lhs
case let (nil, rhs?):
return rhs
case (nil, nil):
return nil
}
}

これで演算子を利用して、Optionalに包まれた文字列の結合を下記のように記述することが出来ます。

a +? b +? c +? d

以上、Optionalの文字列結合のお話でした!