Saul Moraは以前に関数型プログラミングについて講演した際に、小さな関数を組み合わせることで、厄介で複雑で追うのが難しい関数は、最終的には小さな関数のパイプラインとして書けることを話しました。しかし、関数をパイプラインで繋げるためにオプショナル型だけを使うのは、この手法を最大限に活用するには不十分です。この try! Swift NYC の講演では、小さいが役に立つResult(またはEither)と呼ばれるモナドの助けを借りて、関数プログラミングの力を次のレベルに引き上げる手法をお話しします。
SwiftのResult指向プログラミング (00:00)
前回の Swiftでの関数プログラミングの冒険 では, 下記のようなObjective-Cから Swift に変換しただけの関数をお見せしました。
func old_and_busted_expired(fileURL: NSURL) -> Bool {
let fileManager = NSFileManager()
if let filePath = fileURL.path {
if fileManager.fileExistsAtPath(filePath) {
var error: NSError?
let fileAttributes = fileManager.attributesOfItemAtPath(filePath, error: &error)
if let fileAttributes = fileAttributes {
if let creationDate = fileAttributes[NSFileModificationDate] as? NSDate {
return creationDate.isBefore(NSDate.oneDayAgo())
}
}
else {
NSLog("No file attributes \(filePath)")
}
}
}
return true
}
これではただObjective-CからSwiftに直訳しただけです。そこで、bind(bind(A?, f(A)->B?) -> B?
)と呼ばれる新しい関数を導入し、独自の優れた関数型スタイルのオペレータを作りました。それから>>-(A?, f(A)->B?) -> B?
に書き換えました。この方が読みやすく、いくつかの共通機能が抽出されており、簡潔にかけます。
関数プログラミングとパイプを組み合わせてこのアイデアが得られたことを学びました。関数を使って関数を連鎖させる他の関数を構築することができます。これは本当に便利です。
func expired(fileURL: NSURL) -> Bool {
return fileURL.path
>>- fileExists
>>- retrieveFileAttributes
>>- extractCreationDate
>>- checkExpired
?? true
}
今回は、もう少し先に進めましょう。すでにResult
型の存在を知っています。Result
型は実際にはより強力なOptional
型です。これはエラー処理にとても優れていて、コードを読みやすくします。エラー処理するだけのコードで、わざわざif
文を書く必要が無くなります。
Result (01:16)
それではどのようにResult
型を作るのか見てみましょう。
enum Optional<T>
{
case none
case some(_ value: T)
}
Optional
から始めます。Optional
は2つのcaseを持っています。ただのSwiftの列挙体で、何かしらの関連型の値を持っているか、何も持っていないかです。欲しいのは、Optional
の何もない方の値に何か関連付けるものです。
enum Result<T>
{
case success(_ value: T)
case failure(_ error: NSError)
}
noneはもうありませんし、それにはerrorをもたせたので、failureのように名前を変えたいと思います。failureとsomeというのは組み合わせとしてはあまり良くありません。良いようにも思えるのですが、このシナリオに適した名前ではありませんね。
早速リネームしましょう。列挙体にはvalueを持った successと、errorを持った failureの2つのケースがあります。
enum Result<T, E: Error>
{
case success(_ value: T)
case failure(_ error: E)
}
これでOptinal
が無くなりました。これがResult
型と言われるものです。ジェネリックであるOptional
型なので、Objectve-Cの型やSwiftのOptional
型でラップすることができます。
そうするとこのように書けます。successとfailureの2つのケースがあり、errorのあるResult
です。
enum Result<T>
{
case success(_ value: T)
case failure(_ error: NSError)
}
ジェネリックのerrorを引数として扱っているResult
の実装は多く見かけるし、より特定の状況に合わせたエラー型やResultの型を与えることができます。この講演では、すべてがNSError
であると仮定します。これによりスライドがよりきれいになり、Result型が実際何であるかを考えやすくしてくれます。些細なエラーを特定することに気を取られることもありません。 以後このResult
型を使っていきましょう。
Optional
の形式に戻ります。以下のような関数があり、オプションの文字列を返します。これは便利なショートカットの構文糖衣です。
func doSomethingFun() -> String?
{
return "跟朋友们一起学习说中文 🇨🇳"
}
実際には、String
のOptional
ラッパーがあるということです。
func doSomethingFun() -> Optional<String>
{
return .some("跟朋友们一起学习说中文 🇨🇳")
}
この場合では、Optional
にラップされたString
が得られます。使う際には、これをアンラップする必要があります。Result
型を使うのととても似ています。そうするとこのようになります。
func doSomethingFun() -> Result<String>
{
return .success("Ride bike across the Rocky Mountains 🚵")
}
特定の型を内部に持ったResult
型があり、successの場合は、valueをラップします。この場合は、successのcaseの中にStringを持っています。
Result
型を使うと、ただ関数から得られたresultがあり、switch
で条件分岐できます。とてもシンプルです。
let result = doSomethingFun()
switch result {
case .success(let funThing):
print("Had fun: \(funThing)")
case .error:
print("Error having fun 😢")
}
まだできることがあります。Swift標準ライブラリのOptional
型を見てみましょう。XcodeでSwiftのライブラリからOptionalのヘッダを見てみると、以下のようなものを見ることができます。
/// A type that can represent either a `Wrapped` value or `nil`, the absence
/// of a value.
public enum Optional<Wrapped> : _Reflectable, NilLiteralConvertible {
case None
case Some(Wrapped)
/// Construct a `nil` instance.
public init()
/// Construct a non-`nil` instance that stores `some`.
public init(_ some: Wrapped)
/// If `self == nil`, returns `nil`. Otherwise, returns `f(self!)`.
@warn_unused_result
public func map<U>(@noescape f: (Wrapped) throws -> U) rethrows -> U?
/// Returns `nil` if `self` is `nil`, `f(self!)` otherwise.
@warn_unused_result
public func flatMap<U>(@noescape f: (Wrapped) throws -> U?) rethrows -> U?
/// Create an instance initialized with `nil`.
public init(nilLiteral: ())
}
ご覧の通り、はっきりとNone
とSome
が定義されており、ジェネリックを引数としてどんな型でもとります。ですが、さらに関数が2つあります。map
とflatMap
です。これはとても便利です。SwiftのOptional
では、map
が使えるのです。
Map vs. FlatMap (05:07)
__Map__はとても簡単なコンセプトです。入力する型と出力する型を1対1対応させます。例えば、バナナ🍌のコレクションをmap
して、リンゴ🍎のコレクションにします。こ���は1対1対応です。簡単ですね。
func map<U>(T-> U) -> FunctorOf<U>
__flatMap__も似たようなものです。コレクションのmap
変形を行いたいのですが、複数の構造がネストされていたり、より深い階層になっている場合には、これを一つのレベルに平坦化します。これがmap
のあとの平坦化のステップです。有効ではない値を取り除いたり、配列を平坦化したりします。map
に近いですが、関数プログラミングのコンセプトにおいてはとても有用です。このコンセプトが__ファンクター__や__モナド__の発想をもたらしました。
func flatMap<U>(T -> MondadOf<U>) -> MonadOf<U>
ファンクターとモナド (06:22)
__ファンクター__は他の型をラップする型、Tです(FunctorOf<T>
)。OptionalでResultで、map関数を持っていることは明らかです。 * 関数型を極端に重視する人たちは、すべてにおいて数学的に正しくするための3つステップを持ち出してきますが、プログラミングにおいては、これはしたいことをするためのものです。
ファンクターはmap
関数をもった型です。__モナド__はとても似ています。これは他の型をラップする型、Tです。monadOf<T>
というのはflatMap
を持っています。ファンクターはmapを持っていて、モナドはflatMapを持っています。とても単純ですね。
これを実際のコードに持ち込むにはどうしたらよいでしょうか?
下記のコードでは、String
の値があります。ここでは、?マークを使った省略なしの構文のOptionalです。map
を使いたければ、このようにするだけです。バインディングしたければ、構文を省略した演算子を使います。これはいいですね。
let string: String? = "string value"
let string = Optional<String>("string value")
string.map { print($0) }
string >>- { print($0) }
func map<U>(_ transform: T -> U) -> U?
{
switch self {
case .some(let x):
return .some(transform(x))
case .none:
return .none
}
}
map
関数を使いたければ、map
関数の中で、Optional
の中でやっているようなことを行うといいと思います。switch
で分岐しています。アンラップしただけの列挙体の中の関数です。valueをアンラップすると、この場合はx
ですが、transformを適応し、再びラップします。それを返り値として返します。
もちろん、none であれば、何も返しません。常にファンクターやモナドを返します。常に何かを返すのです。Result型については、map
関数に似たことを行っています。switch
があり、success や failure があります。
enum Result<T>
{
case success(_ value: T)
case failure(_ error: Error)
func map<U>(_ transform: T -> U) -> U
{
switch self {
case .success(let value):
return transform(value)
case .failure(let error):
return Result<T>.failure(error)
}
}
}
let result: Result<String> = doSomething()
result
.map { value in
print("Had fun: \(value)")
}
.mapError { error -> NSError in
print("Error having fun: \(error)")
}
2つのケースがあります。successでは、transformを適応し、valueを返します。transformの引数を見ると、T
をとり、U
を返しています。このtransformは直接返しているだけです。transformの返り値はmap
関数から直接来ています。
failure では、errorを新しいresultにmapし直して、それを返しているだけです。何かをして、常に値を返しています。ここでは、何かの結果を返すことを保証しており、valueかerrorになります。
flatMap
バージョンでも同じことができます。よく似ています。一度使い始めるとこのようになります。何回も使うと気づくと思いますが、Optional
を使っているときに if-let を使って明示的に外側で展開するのに対し、map
型の関数はアンラップされたものや、列挙型の中のswitch
構文をカプセル化し、きれいに保ちます。ちょっとしたいいトリックで、列挙体のトップにこの関数をもたせます。
これでResultを取り出し、値をmap
できるようになりました。値をmap
して、それを演算できます。map
関数に値をアンラップさせています。しかし、valueではなく、errorがあったらどうなるのでしょう?
Result
型の実装には、エラーをアンラップするmapError
関数のヘルパーがあります。map
は何度も動きますが、errorのmapは全く起こりません。同じシーケンスで両方の値が得られることは起こり得ないのです。どちらかだけです。処理の仕方が違います。結果のvalueを処理することが正しい方法というわけではありませんが、いくつかのケースでそれを行うのはきれいで良い方法です。
マイクロネットワークフレームワーク (10:30)
実例を用いてお話しましょう。マイクロネットワークフレームワークです。
HTTPスタックを構成するのはこれを試すのにいい例です。何度もやりたいと思うのは、よくわからないインターネットからデータを要求しているときに、Requestしているデータ型に焦点を当てることだからです。配管について心配したり、HTTPであることを知ったり、すべての詳細について知りたいとは思っていません。 どこかから非同期でデータをリクエストして、欲しいデータ型の返り値を得たいのです。するとオペレーションに移ることができます。
しばらく前に、Chris Eidhofが書いたマイクロネットワークライブラリに関するブログ記事を見たかもしれません。 彼はAPI Requestを搭載していて、引数を複数渡せる小さな関数を作っています。この関数について素晴らしい点は、URLを持っていて、Resouceの発想があることです。これには、ネットワークのエンドポイント(HTTPサーバー)に直接適用される他にもたくさんの要素があります。また、completionブロックもあります。
func apiRequest<A> (
modifyRequest: NSMutableURLRequest -> (),
baseURL: NSURL,
resource: Resource<A>,
failure: (Reason, NSData?) -> (),
completion: A -> ()
)
func request<R: HTTPResource> (
resource: R,
cachePolicy: URLRequest.CachePolicy,
requestTimeout: TimeInterval,
host: HTTPHost?,
completion: (Result<R.RequestedType>) -> Void
)
私たちにあるのは、RequestとResourceです。 *このResourceはどのように見えるのでしょうか? *Resourceプロトコルがあり、このプロトコルでは、Result
型があります。このResouceには、ある関連型の値があります。 personオブジェクトを持っているとします。これは非常に汎用的な方法です。したがって、このジェネリック型をプロトコルに関連付けることになります。また、エラー型も指定します。
さらに、他にもこのリソースをネットワークからリクエストするために必要なものが色々あります。そのうち1つはパスです。もう1つはクエリメソッドです。View Controlelrやアプリ内のより高いレベルのものから非同期にトークしているときには、気にしない程度の細かいことです。パスやメソッド、クエリメソッド、クエリパラメータ、パース関数があり、この場合、このパース関数にはResultがあります。
実際には、このパース関数は以前のコードブロックで誤って記述されていました。これが正しいバージョンです。別の関数へのエイリアスを持ちます。Swift3の構文を使用しており、typealias
を使っています。ジェネリックの引数を次の型に渡すことができます。これはいいですね。HTTPでパースするものを特定するジェネリクスを用いた方法があります。
public protocol HTTPResourceProtocol
{
associatedtype ResultType
associatedtype ErrorType: HTTPResourceError
var path: String { get }
var method: HTTPMethod { get }
var queryParameters: [String: String] { get }
var parse: Result<ResourceDataType, HTTPResponseError> { get }
}
public typealias ResourceParseFunction<ResourceDataType> =
(Data) -> Result<ResourceDataType, HTTPResponseError>
public protocol HTTPResourceProtocol
{
associatedtype ResultType
associatedtype ErrorType: HTTPResourceError
var path: String { get }
var method: HTTPMethod { get }
var queryParameters: [String: String] { get }
var parse: ResourceParseFunction<ResultType> { get }
}
まず、HTTPからResponseを返すときに、一般的にどう処理するのが理想的か考えてみましょう。まず、Responseコードを検証したいと考えています。401エラーだったのか、500エラーだったのか、201だったのか?または自分ができることが必要なのか、すぐに処理できることなのか、値は早く返した方がいいのか。それでも問題がなければ、データの再検証や前処理を試みます。
その後、データは良好で、破損していないと判断し、JSONでデシリアライズして、昨日の講演を元に、選んだJSONパーサを使ってデコードします。最後に、completionハンドラを呼び出します。
APIにはcompletionブロックがあることを覚えていますよね。completionハンドラは、最近使ったものです。ここでは、Resultをcompletionブロックに渡しました。これを全部をやりたいと思っています。この実装は以下のようになります。
func completionHandlerForRequest<R: HTTPResource>(
resource: R,
validate: ResponseValidationFunction,
completion: @escaping (Result<R.RequestedType>) -> Void
)
-> (Data?, URLResponse?, Error?) -> Void
{
return { (data, response, error) in
_ = Result(response as? HTTPURLResponse, failWith: .InvalidResponseType)
>>- validateResponse(error)
>>- validate(data)
>>- resource.parse
>>- completion
}
}
この特定のケースでは、HTTPの結果を処理する関数を返す関数を作ります。returnの後の中括弧が他の関数を定義していて奇妙に見えます。ここで真ん中の部分に焦点を当てます。標準のNSURLSessionTask
メソッドのcompletionハンドラみたいに見えます。駆け足で見てみましょう。
return { (data, response, error) in
_ = Result(response as? HTTPURLResponse,
failWith: .InvalidResponseType)
>>- validateResponse(error)
>>- validate(data)
>>- resource.parse
>>- completion
}
まず、このパイプを作ります。この矢印構文のバインド演算子があり、パイプをつくるのに役立ちます。パイプし始めるために、まずResultを取得する必要があります。このResultの実装では、このresponseをHTTPURLResponse
にキャストできます。failしたら、すでに定義しているfailureのエラー型を渡します。すでにfailしており、HTTPURLResponse
ではないものを返している場合は、残りのパイプがエラーを返します。
その後、エラーレスポンスのバリデーションに移ります。関数を返す関数を作っています。ここでは、この特定の時点で起こりうるエラーをあらかじめ入力しています。このパイプラインでは、後半のHTTPURLResponse
が結果の型でHTTPURLResponse
を返すだけです。ここでも、関数を返す別の関数であるパイプの次のステップに移り、繰り返され、再び実行されます。
func validateResponse(_ error: Error?) ->
(HTTPURLResponse?) -> Result<HTTPURLResponse,
HTTPResponseError>
func validate(Data?) -> (HTTPURLResponse) ->
Result<Data>
response.parse: (Data) -> Result<ResourceType>
validateResponse :
(HTTPURLResponse?) -> Result<HTTPURLResponse>
customValidate :
(HTTPURLResponse) -> Result<Data>
response.parse :
(Data) -> Result<ResourceType>
completion :
(Result<ResourceType>) -> Void
ここでは、Resourceにはパース関数もあります。パース関数は、データを取得し、探している型としてHTTPリソースで指定した最初に要求した型を返すように定義されています。しかし、まず、基本的にバイナリデータを取得しています。このデータは、NSData
、またSwift3のData
を型として取得しています。
ここでもパイプを見ることができます。検証を行い、カスタム検証を行い、パースが完了してから完了します。それはURL ResponseからURL Responseへ、そのresultはラップされ、次の関数にパイプされます。このレスポンスはResultを返し、パースされた関数にある次の関数に入ります。ResourceTypeのresultを返します。この出力は、completionブロックの次の関数の入力として、completionハンドラへパイプされます。
これはすべてパイプの一部です。このパイプは、前の関数のすべての出力を次の関数の入力として使えるので機能しています。これは関数をチェーンさせる方法です。出力は入力と一致する必要があります。これを見ると、result、データ、レスポンスを取得し、resultを使用して関数型的なパイプでチェーン化していることがわかります。
これの良いところは、ここで Result
を使うと、いずれかのステップで失敗した場合、result、すなわちエラーがcompletionブロックに至るまで伝播することです。失敗した場合にNSError
を作成し、failure型のcompletionブロックを呼び出すとか、successした場合はsuccess型を渡して呼び出す、ということは書いていません。successとfailureは同じものにみえます。success、failureは同じコードです。ひとつのコードです。それがこの方法で行うにあたっての良いところです。
この実装例として、GaugesというサービスサイトへのAPIを書きました。これはHTTPの解析をするサイトで、自分のウェブサイトに配置することができます。siteIDを使って、データを探します。
func siteGauge(siteID: SiteID) -> HTTPResource<Gauge>
{
let path = "gauges/\(siteID)"
return HTTPResource(path: path, parse: parse(rootKey: "gauge"))
}
request(resource: siteGauge(siteID: "my_site_id")) {
$0.map { site in
print("Found \(site)")
}
}
ここでは、HTTPResource
のGauge
型は私が求めているデータ型のオブジェクトです。特定のSiteIDのすべての情報をRequestしています。パスはこのGaugeのSiteIDです。私はただ良いパスを作成します。次に、特定のホストを使用するように設定されているこのリクエスト関数に入れ、ホストベースURLをパラメータとともにGaugeのURLに接続し、前の関数から与えられたすべての情報に基づいてリクエスト全体を構築します。このリクエスト関数を呼び出すと、リクエストの結果を非同期で取得し、その結果をmapし、便利なダンディなmap関数を使用してその結果をラップすることで処理できます。
エラーを処理するためにこのようにすることもできますし、古い方法でもっとはっきりさせたい場合は、switch
文でそれを扱うこともできます。
request(resource: siteGauge(siteID: "my_site_id")) {
result in
result.map { site in
print("Found \(site)")
}
.mapError { error -> HTTPResourceError in
print("Error requesting resource: \(error)")
return error
}
}
request(resource: siteGauge(siteID: "my_site_id")) {
result in
switch result {
case .success(let site):
print("Found \(site)")
case .failure(let error):
print("Error getting site: \(error)")
}
}
個人的には、map
を使う方が好きです。エラーを処理したくないこともあるし、failしてもいい場合があるからです。ですが、常にそうということはないので、エラーは処理してください。
ライブラリ (20:02)
このすばらしい機能をもったResult
は自分で実装しなきゃいけないでしょうか?いいえ、そんな必要はありません。このResult
を使いたければ、GitHub上にその実装があります。私がこのコーディングスタイルで実装したCoreHTTPライブラリは、こちらのGitHubリポジトリにて公開しています。
About the content
2016年9月のtry! Swift NYCの講演です。映像はRealmによって撮影・録音され、主催者の許可を得て公開しています。