しおメモ

雑多な技術系ブログです。

SourceryでBuilderを自動生成するテンプレート

若干需要があったので、テンプレートファイルを作成して、Sourceryを使ってBuilderを自動生成してみました。

実装は、Lombokの@BuilderのようなシンプルなBuilderのイメージです。

projectlombok.org

テンプレートと使い方

作成したテンプレートは、ここに置いてあります。

github.com

// sourcery: Builder
struct Params {
    let id: String?
    let name: String?
    let age: Int?
    let url: URL?

    var profile: String {
        return name ?? ""
    }
}

上の元々の構造体(Params)から、sourceryを実行することで、

extension Params {
    static var builder: Builder { return Builder() }

    final class Builder {
        private var id: String?
        private var name: String?
        private var age: Int?
        private var url: URL?

        func id(_ id: String?) -> Builder {
            self.id = id
            return self
        }

        func name(_ name: String?) -> Builder {
            self.name = name
            return self
        }

        func age(_ age: Int?) -> Builder {
            self.age = age
            return self
        }

        func url(_ url: URL?) -> Builder {
            self.url = url
            return self
        }

        func build() -> Params {
            return Params(id: id, name: name, age: age, url: url)
        }
    }
}

このようなコードが自動生成されます。

これにより、以下のようにインスタンスを生成できます。

let hoge = Params.builder
    .id("hoge")
    .name("hoge taro")
    .build()

SwiftのOptionalの注意点とmap/flatMap

最初は、OptionalmapflatMapの話だけ書いていたのですが、だんだんOptionalそのものの話まで膨らんできたので、一緒に書くことにしました。

今回の話はコードレビューで見つけたら割と指摘したい部分ではあるものの、毎回この内容を書くのも無理があるので、一度まとめておく価値はあるかなと思いました。

Optional自体は、今では様々な言語に取り入れられているので、Swiftに限定しなくても良かったのですが、もろもろのシンタックスシュガーと歴史のためか、割と使う頻度が高い上に乱用も多いと思ったので、今回はSwift限定にしました。

Optionalの復習

他の言語でも多い実装ですが、Optionalの実装はただのenumです。
簡単に書き直すとこのような感じになります。

enum Optional<Wrapped> {
    case none
    case some(Wrapped)

    init(_ some: Wrapped) { self = .some(some) }
}

普段使っているT?Optional<T>の、nil.noneのシンタックスシュガーです。

これらのシンタックスシュガーに隠れているため、Optionalenumであることを意識することは少ないかもしれないですが、たとえば、

enum Hoge {
    case none
    case ok
}

という定義をすると、Hoge.noneを使おうとして、

var hoge: Hoge? = .none

と書くとOptional.noneが入ってしまうというミスが生じます。

このため、case noneを定義するのは危険で、Xcode 11から上のようなあいまいな.noneの使い方をすると警告が出るようになりました。(SR-2176)

おまけ: Xcode 11からOptionalにswitchが使えるようになった

知ってると良いことあるかもしれません。(SR-7799)

let hoge: Hoge?

// Before
if let hoge = hoge {
    switch hoge {
    case .ok:
        return 200
    default:
        return 400
    }
}

// After
switch hoge {
case .ok:
    return 200
default:
    return 400
}

可能な限りOptionalでない方が嬉しい

Optional自体は強力な機能ですが、使いすぎてしまうことでデメリットもあります。
とくにOptionalを公開するインターフェースに使う場合、不都合が生じることがあります。

デメリットが生じる一つの例として、プロパティや戻り値を不必要にOptionalにすることで、それを使う人毎回unwrapするコストが発生してしまうケースがあります。
後で具体的なコードが出てきますが、Optionalでなくても成り立つものは、Optionalにしない方がインターフェースとしては優れています。
(もちろん例外を握りつぶすとか、force unwrapしろということではないです。)

引数のOptionalについても、mapflatMapを知っていれば減らせる場合があります。

些細な差かもしれないですが、パフォーマンス的にも値をラップする分Optionalの方がオーバーヘッドが生じ不利になります。

Optionalを使う時の方針

基本的な方針はOptionalを引き回さないということと、不用意にOptionalを生じさせないということです。

引き回しそうになった場合は、unwrapの処理を他のメソッドに任せず、if letguard letOptional.mapなどを使ってなるべく外側で処理するようにします。

また、Optionalを発生させて戻り値に利用するメソッドや、failable initializer(init?)を使う際には注意が必要です。

mapとflatMapの使い方

OptionalmapflatMapを活用することで、コードがシンプルになるケースや、Optionalが減るケースがあります。

mapの実装

mapは与えられたクロージャで中身を変換して、Optionalでラップします

func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U? {
    switch self {
    case .some(let y):
        return .some(try transform(y))
    case .none:
        return .none
    }
}

中身(Wrapped)をUに変換するクロージャを渡すと、U?が返ってきます。

flatMapの実装

それに対してflatMapは、

func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U? {
    switch self {
    case .some(let y):
        return try transform(y)
    case .none:
        return .none
    }
}

中身(Wrapped)をOptional<U>に変換するクロージャを渡すと、Optional<U>が返ってきます。

mapとflatMapの使い分け

map(Wrapped) -> Uに対してU?を返すので、もしUHoge?だった場合、Hoge??が返ります。

let urlString: String? = "hoge"
let url = urlString.map(URL.init(string:)) // Hoge??

上のようなコードを書いた場合、urlの型はURL??と2重のOptionalになってしまいます。

多くの場合2重にする必要性はないため、Optionalを返すクロージャに対しては、flatMapを使うことが多いです。

let urlString: String? = "hoge"
let url = urlString.flatMap(URL.init(string:)) // Hoge?

urlString.flatMap(URL.init(string:))urlString.flatMap { URL(string: $0) }とほぼ同じです。

あまり良くないインターフェース

ありがちなケースを2つほど紹介します。

nilを伝播させるだけのインターフェースはアンチパターン

Optionalを引数にとって、nilをそのまま返り値のOptionalに伝播させるだけのインターフェースはアンチパターンです。

func generateMessage(count: Int?) -> String? {
    guard let count = count else { return nil }

    return "あなたは\(count)人目の訪問者です"
}

かなり極端な例ですが、上のようなメソッドは良いインターフェースとは言えません。
Optionalを渡すだけなら困りませんが、下のコードのように引数に、ただのIntを渡した際にもOptionalが返ってきてしまいます。

let message = generateMessage(count: 10) // String?

1引数の場合は、guard文のところはOptional.mapと全く同じことをやっているので、元のメソッドは単純に、

func generateMessage(count: Int) -> String {
    return "あなたは\(count)人目の訪問者です"
}

として、Int?を渡したいときは上のコードのようにすれば同じことができます。

こちらはIntを渡したときもStringが返ってくるので、インターフェースとしては最初のものよりも優れています。

let count: Int? = 10
let message = count.map(generateMessage(count:))

また、複数引数がある場合も、同じように不必要にOptionalを発生させるケースがあるので、nilを伝播させるためだけに引数や戻り値にOptionalを使うのは良いインターフェースとは言えません。

// Optionalだらけ
func generateMessage(first: String?, second: String?) -> String? {
    guard let first = first, let second = second else { return nil }

    return "\(first) feat. \(second)"
}

ただ、mapflatMapを使う場合は若干見づらくなるので、引数が多い場合はif letguard letを使うほうが良いと思います。

func generateMessage(first: String, second: String) -> String {
    return "\(first) feat. \(second)"
}

let first: String? = "John"
let second: String? = "Paul"

if let first = first, let second = second {
    let message = generateMessage(first: first, second: second)
    // ...
}

// or
let message = first.flatMap { str in second.map { generateMessage(first: str, second: $0) }}

このようにできるだけ外でunwrapするようにしておけば、引数がそれぞれnilだった際のハンドリングも個別に分けることができるため、メソッドの中でunwrapするよりも柔軟に対応できます。
(逆に、上のような書き方は2つのnilを1つにまとめてしまっているので、ある意味握りつぶしてしまっています)

failable intializer

failable initializerは、IUOやforce unwrapを使わない限り、必ずインスタンスをOptionalで持つことを強制します。
とくに、failable initializerしか用意されていないインターフェースにした場合、使う側にとってはインスタンスを作るごとに毎回unwrapしなくてはいけないため、非常にコストになります。

この点から、特定の状況以外では使わない方が良いと個人的には考えています。

具体的に例外のケースは、

  • super.initがfailable
  • イニシャライザの中で複数Optionalを使うメソッドやthrowするメソッドがある場合

くらいかと思います。

1つ目のケースはrequired init?(coder:)のように、スーパークラスがfailable initializerを持つクラスです。
2つ目のケースはJSONのデコードをイニシャライザの中で行うレスポンスのデータ型のような、複数Optionalが登場して、なおかつどれか失敗したら全体をnilとしたいクラスです。

特に、中でOptionaltry?を使っていないにも関わらず、init?を使っていたら危険信号です。

final class Hoge {
    init?(members: [Member]) {
        guard let !members.isEmpty else { return nil } // 余計なお世話じゃ

        // ...
    }
}

長いのでまとめ

上記のような、必要ない場面でOptionalを使ってしまうケースさえ気をつければ、非常に強力な機能なので、うまく付き合っていきましょう。

読んだ: インターフェースデザインのお約束

プログラミング関係の本は食傷気味なので、気分を変えて読んでみました。
勝手に付与されたPayPayなんとかポイントで買いました。

インタフェースデザインのお約束 ―優れたUXを実現するための101のルール

インタフェースデザインのお約束 ―優れたUXを実現するための101のルール

  • 作者:Will Grant
  • 発売日: 2019/11/09
  • メディア: 単行本(ソフトカバー)

内容

主にWebサイト(と若干アプリ)のUXやUIについての法則やアンチパターンについて書かれています。
読んでみて、UIとUXが7:3くらいのウェイトなのかなと思いました。

内容はWeb周りのエンジニアなら知ってそうなことが半分くらいでしたが、のこりは割とためになる内容でした。

アプリエンジニアの視点から言うと、

  • 049 ユーザーが入力したデータは指示されない限り消すな
  • 059 無限スクロールが必須ならユーザーの現在位置を保存し、そこに戻れ
  • 097 「それ、モバイルでも動く?」はもはや過去の質問

などが割と関心のある内容かなと思いました。

感想

後ろの方に書いてある、UIは模倣から始まるという内容はその通りだと感じています。
ただ、模倣される側としては当然不利益を被るので、どこまでが許されるかというのは現状モラルに委ねられる部分もあり、難しい問題かと思います。

ネイティブエンジニアとしては、モバイルで動くのが当たり前という節では、宿敵の「それWebでよくね?」おじさんみたいな話が出てきて、耳が痛かったです。

この本の総評としては、本の中で繰り返される内容やある意味常識的な内容もあり、若干荒削りなため、めちゃくちゃオススメというではないですが、さっと読めてお手頃な値段なので、買って損はないかなと思いました。☆3.2くらいです。

なぜか終始disり口調なので、読んで面白いというのはありました。