Tryswiftnyc saul mora facebook

Result指向開発

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 "跟朋友们一起学习说中文 🇨🇳"
}

実際には、StringOptionalラッパーがあるということです。

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: ())
}

ご覧の通り、はっきりとNoneSomeが定義されており、ジェネリックを引数としてどんな型でもとります。ですが、さらに関数が2つあります。mapflatMapです。これはとても便利です。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があり、successfailure があります。

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)")
 }
}

ここでは、HTTPResourceGauge型は私が求めているデータ型のオブジェクトです。特定の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によって撮影・録音され、主催者の許可を得て公開しています。

Saul Mora

Trained in the mystical and ancient arts of manual memory management, compiler macros and separate header files. Saul Mora is a developer who honors his programming ancestors by using Optional variables in swift on all UIs created from Nib files. Despite being an Objective C neckbeard, Saul has embraced the Swift programming language. Currently, Saul resides in Shanghai China working at 流利说 (Liulishuo) helping Chinese learn English while he is learning 普通话 (mandarin).

4 design patterns for a RESTless mobile integration »

close