Swiftの文字列の置換にFunctional in Swiftの手法を取り入れてみる

Monday, March 14, 2016

また雨が降って急激に冷え込んできましたね。。月曜日からこれだと体がもたない…

最近、「Functional in Swift」を買うか買わないか迷ってて購入画面を行ったり来たりしています。
寧ろ「Advanced Swift」の方が欲しかったり。。

それで、「Functional in Swift」で、(おそらくCIFilterの)処理をFunctionalに書いているコードが、購入ページでちらっと見えたので、
練習がてら、違うAPIを例に関数型プログラミングっぽく書く練習をしました。
長く読みたくない人のために、先にGistを貼っておきます。

文字列の置換を題材にやってみる


今回は、Stringクラスの、
stringByReplacingOccurrencesOfString(_:withString:options:range)
を例に取ってみます。
正確な定義は、

@warn_unused_result
public func stringByReplacingOccurrencesOfString(target: String, withString replacement: String, options: NSStringCompareOptions = default, range searchRange: Range<Index>? = default) -> String

です。
この関数自体は、レシーバーの文字列中にある、targetを、withStringに置き換えるメソッドです。
更にオプションで、正規表現で置換したり、大文字/小文字の区別の指定もできるようになります。

例えばですが、
"hello, name"という文字列があったときに、文の最初の文字を大文字に変更(“h"→"H”)し、“name"を人の名前に置き換えて、更に、“Hi, ! Welcome to Japan!“という文字に置換すると考えた時に、普通に書くとこんな感じになります。

/// 文字の先頭を取り出すextensionを定義
extension String {
    var first: String {
        return String(characters.prefix(1))
    }
}

let str = "hello, name"
let p1 = str.stringByReplacingOccurrencesOfString(
    str.first,
    withString: str.first.capitalizedString
)
let p2 = p1.stringByReplacingOccurrencesOfString("name", withString: "Mike")
let result = p2.stringByReplacingOccurrencesOfString("Hello, (.+)", withString: "Hi, $1!! Welcome to Japan!", options: .RegularExpressionSearch)
print(result) // Hi, Mike!! Welcome to Japan!"

とても冗長的で長いですね。 どうしても順を追って置換する必要があるので、置換のプロセスが分かれてしまうのは仕方ないのですが、 なんとかこれを短くしたい。
ということで、これに、関数型プログラミングっぽいアプローチを導入してみます。

導入してみる


まず、typealiasを用いて、Replacerを定義します。

typealias Replacer = String -> String

Replacerは、 String を受け取って、 String を返すクロージャです。

次に、replaceメソッドを、Replacerを用いて定義します。

func replace(target: String, with: String, options: NSStringCompareOptions = []) -> Replacer {
    return { $0.stringByReplacingOccurrencesOfString(target, withString: with, options: options) }
}

関数の返り値として、Replacerを返しています。
そのために、 return { .... }として、クロージャを作成してそれを返すようにしています。 上記は、略記しているので見やすくすると、

func replace(target: String, with: String, options: NSStringCompareOptions = []) -> Replacer {
    return { str in
        str.stringByReplacingOccurrencesOfString(target, withString: with, options: options)
    }
}

となります。確かに、 String→String のクロージャを返しています。返すクロージャの型は、関数の最後に書いた、 -> Replacerによって推論されます。
なので、ここで記述するクロージャの内部に渡される引数が、 String であると推論されます。

こうすることで、

let replacer = replace("name", with: "Mike")
let result = replacer("hello, name") // "hello, Mike

このように書くことができるようになります。replacer変数に、Replacer型のクロージャを格納し(1行目)、
それに引数として文字列を渡して、置換操作を行います(2行目)。

また、それぞれのReplacerを連結することができます。

let r1: Replacer = replace("name", with: "Mike") // name → Mike
let r2: Replacer = replace("Mike", with: "John") // Mike → John
let r: Replacer = { str in r2(r1(str)) } // ★
let result = r("hello, name") // "hello, John

を付けた箇所が少し厄介ですね。
まず、 Replacerは、 String を受け取って、 String を返すクロージャなので、
{ str in <行う処理> return <文字列> } という書き方になります。
次に、行う処理ですが、r1r2と処理を行いたいので、

r1に文字列を渡して置換した結果の文字列をr2に渡して再度置換する

これを形にすると、
r2(r1(str))
となります。
最後にこれを組み合わせ且つ、クロージャが1行で済む場合はreturnを省略して1行目の処理の返り値を返す事ができるのを利用すると、
let r: Replacer = { str in r2(r1(str)) }
となります。ちなみに型推論が必要になるため、let r: Replacerと、型を宣言する必要があります。

ここまでを組み合わせれば、複数の置換操作を組み合わせて…となりますが、
先の例の通り、組み合わせる時に毎回クロージャで包まないといけないのは 面倒 だし、 {}のネストが増えたり仮変数を宣言したり であまり良くないので、結局イマイチな書き方になります。

Custom Operatorを導入する


そこで、Custom Operatorを宣言して、より書きやすく、クロージャを畳み込む部分を封じ込めます。

typealias Replacer = String -> String

infix operator ~> { associativity left }

func ~> (replacer1: Replacer, replacer2: Replacer) -> Replacer {
    return { replacer2(replacer1($0)) }
}

こんな感じで、 replacer1 ~> replacer2と連結できるようになる演算子~>を定義します。
例によって略記しているので、見やすくすると、

func ~> (replacer1: Replacer, replacer2: Replacer) -> Replacer {
    return { str in replacer2(replacer1(str)) }
}

となります。 これを用いると、先ほどの例が、

let replacer = replace("name", with: "Mike") ~> replace("Mike", with: "John")
let result = replacer("hello, name")

こんな感じにスッキリとまとまります。
関数(クロージャ)を繋げてあれこれ処理をしていくのが関数型プログラミングの強みですね!
ここまでを用いて、少し必要な処理をReplacerを用いて宣言してあげることで、

"hello, name"という文字列があったときに、文の最初の文字を大文字に変更(“h"→"H”)し、“name"を人の名前に置き換えて、更に、“Hi, ! Welcome to Japan!“という文字に置換すると考えた時に、普通に書くとこんな感じになります。

これがスッキリ書けるようになります。


let lowerCaseReplacer: Replacer = {
    $0.lowercaseString
}
let upperCaseReplacer: Replacer = {
    $0.uppercaseString
}
let capitalizeFirstLetterReplacer: Replacer = {
    replace($0.first, with: $0.first.capitalizedString)($0)
}

let replacer1 = capitalizeFirstLetterReplacer
    ~> replace("name", with: "Mike")
    ~> replace("Hello, (.+)", with: "Hi, $1!! Welcome to Japan!", options: .RegularExpressionSearch)

let result1 = replacer1(str) // "Hi, Mike!! Welcome to Japan!"

更に、上記処理を拡張して、最終的に出力する文字列はUpper Caseにしたい!という場合は、

let replacer2 = replacer1 ~> upperCaseReplacer
let result2 = replacer2(str) // "HI, MIKE!! WELCOME TO JAPAN!"

とするだけで済むので、拡張するのも手軽になります!
おまけで、人の名前を、上記の例に当てはめつつ、 “Hi, <人の名前>!! Welcome to Japan!!" みたいにしたい場合は、

let str = "hello, name"
let names = ["Mike", "John", "Sara"]
let nameReplacer: String -> Replacer = { name in
    capitalizeFirstLetterReplacer
        ~> replace("name", with: name)
        ~> replace("Hello, (.+)", with: "Hi, $1!! Welcome to Japan!", options: .RegularExpressionSearch)
}

let greetings = names.map { nameReplacer($0)(str) }
print(greetings)

こんな感じで書くことができます。より関数型プログラミングっぽい書き方になりましたね。

nameReplacer($0)(str)となっていて、ちょっと見慣れない形になっていますが、
nameReplacer($0)までが、 String→Replacer 型のクロージャで、その関数に引数として文字列を与えています("(str)の部分”)。
この例では、mapを使って名前を取り出して、nameReplacer($0)という形で名前を渡すことで、任意の名前に置換しつつ挨拶文に変えるReplacerを作成し、それに、基となる文字列を渡しています。
こんな感じて、購入ページからかいつまんだ程度ですが、見事に文字列の置換操作に、Funtionalなアプローチを導入することができました。
元々Objective-Cでとにかく オブジェクト指向 でやっていたのもあって、なかなかこういうアプローチには慣れないですが、
少しずつ慣れてより簡単且つ強力に書けるようになりたいなと思います。本買おうかなあ…。

丸々コピペして、Playgroundに貼り付けることで試せるコードは、Gistにあげておきました。よければどうぞ!

techSwiftTipsFunctional

RxSwiftで実行するSchedulerの作り方とお行儀良く扱うためのメモ

インプットとアウトプット