LoginSignup
40
28

More than 5 years have passed since last update.

Swiftのtypealiasは別名を与えるだけで型として区別されない、ということに注意して扱うべきという話

Last updated at Posted at 2016-11-15

inaka / Function Naming In Swift 3 という記事、概ね良いこと書いてあると思いつつ、1つ賛同出来ないと思った点がありました。

こちらがそのコードと説明です:

typealias Path = String 

func request(for path: Path) -> URLRequest {  }
let request = request(for: "local:80/users")

In this case, your parameter's type and its expected content are coherent and in harmony, because the parameter's type was made explicit through defining Path as a type alias for String.
This way, your function is still intuitive, readable, unambiguous, yet non-repetitive.

以下の違和感がありました。

  • pathというパラメーター名がすでにあるのに、さらにわざわざtypealiasPathという名前を用意していて冗長
    • 他にメリットがあるなら良いがわざわざこのためだけにtypealias使うのは違和感
    • String型を受けるという情報を分かりにくくさせてしまっている
  • Stringという単純な型に対してtypealiasで別名を与えているが、これは単なる別名であって、Stringと全く同じ扱いになる
    • StringPathは区別されない
    • Pathとは何?と思ってコードを辿ると、ただのStringかと分かるようになっていて、まどろっこしいだけ

StringPathは区別されない

特にこの特性にも関わらずStringという単純な型に別名を与えていることが問題だと思っています。
コード例のように文字列リテラルをそのまま与えられた場合にコンパイル通るのは良いとしても、以下のようにString型変数も与えてもコンパイル通ります。

let path: String = "local:80/users"
let r = request(for: path)

こういう使い方は一見良い感じに見えてしまうかもしれませんが、実際のメリットは無いと思っています。
Swiftのtypealiasが型を区別しない仕様に対しての不満ではなく、こういう単純な型に別名を与える用途には不適切、という意見です。

Swiftのtypealiasの正しい使いどころ

上述の通り、単純な型に別名を与える目的では使うべきでは無いと思っています。
では、どういう時に使うかというと、以下の2点に絞られるはずです。

  • 複雑な型に別名(簡略名)を与えたい時
    • タプル・クロージャー・階層の深いネストされた型など
  • protocolで宣言されたassociatedtypeに具体的な型を与える時

後者は完全に別の話になるので置いておいて、前者は以下のように同じ型を使い回したい時に定義を使い回すことが出来ます。

typealias PersonInfo = (firstname: String, lastname: String)
func foo(_ info: PersonInfo) {
    print("foo: \(info)")
}
func bar(_ info: PersonInfo) {
    print("bar: \(info)")
}
let info = ("First", "Last")
foo(info)
bar(info)
typealias MyClosure = (String) -> ()
func f1(_ closure: MyClosure) {
    closure("f1")
}
func f2(_ closure: MyClosure) {
    closure("f2")
}
f1 { print($0) }
f2 { print($0) }

Stringなどの単純な型はそれをベタに書けば済み、typealiasを使うと省力化するどころか、むしろ不便になると思っています。

Pathとは何?と思ってコードを辿ると、ただのStringかと分かるようになっていて、まどろっこしいだけ

言語によっては、単純な型に別名を与える目的で使うのもあり (Go言語など)

例えば、Go言語は以下の挙動なので、typeで定義してそれを型として使うのはあり(よくやるパターン)だと思っています。

type MyInt int

func test(i MyInt) {
    print(i)
}

func main() {
    anInt := 0
    test(anInt) // コンパイルエラー
    test(0) // OK
}

Goの場合、typeの名前で区別されるものとそうで無いものがあって、紛らわしいところもありますが…。
参考:
Why can I type alias functions and use them without casting? - Stack Overflow
(Goの挙動は、 https://play.golang.org などで気軽に試せます。)

Swiftのtypealiasはいかなるものも名前で区別されません。

では、型を区別したい時は?

基本的にStringなど単純な型をベタ書きで良いケースが多いと思っていますが、それでも型で区別したい時などもあると思います。
冒頭の、賛成出来ない例として出したコード例で説明します。

単純にstructなど独自型を作る

ベタな方法ですが、まずは、これが良いと思っています。
冒頭の例を書き換えるとこうなります。

struct Path {
    let value: String
    init(value: String) {
        self.value = value
    }
}

func request(for path: Path) -> URLRequest {
    return URLRequest(url: URL(string: path.value)!)
}
let path = Path(value: "local:80/users")
let r = request(for: path) // request(for: "local:80/users") はコンパイルエラー

こうすると、pathとして想定していない他のString変数を誤って使ってしまうことを型で防げます。

Pathが単純なStringの別名では無く、ドメイン駆動設計(DDD)の値オブジェクトみたいな感じにもなりましたね。

以下、少し蛇足かもしれませんが、もう少し"便利"にしたい時のパターンです。

Swiftの言語機能を活用して"便利"に

Protocolを活用

URLConvertibleというProtocolを宣言して、Pathをそれに準拠させてみます。

protocol URLConvertible {
    func asUrl() -> URL
}

struct Path {
    let value: String
    init(value: String) {
        self.value = value
    }
}

extension Path: URLConvertible {
    func asUrl() -> URL {
        return URL(string: value)!
    }
}

func request(for url: URLConvertible) -> URLRequest {
    return URLRequest(url: url.asUrl())
}
let path = Path(value: "local:80/users")
let r = request(for: path)

上のようにすると、URLConvertibleのみ引数として認めるようにすると、例えばStringもそれに準拠させて、以下のようにStringを渡せるようにもなります。

extension String: URLConvertible {
    func asUrl() -> URL {
        return URL(string: self)!
    }
}

request(for: "local:80/users")
let path2 = "local:80/users"
request(for: path2)

元々の、「型で区別」というメリットから遠ざかってきた感じもしますが、例えば以下のように異なるProtocolとして取り扱っている文字列を誤って利用してしまうミスは型で防げるので、シーンによっては活用出来ると思います。

protocol SomeProtocol {}
extension String: SomeProtocol {}
let path3: SomeProtocol = "local:80/users"
request(for: path3) // コンパイルエラー

このやり方は実際にAlamofireなどの実装にも取り入れられています
(意図しない型を防ぐというより、オーバーロードぽいことをスマートに実現するためのテクニックとして使われている気がしますが。)

ExpressibleByXXXLiteralを活用

上のProtocolを活用した例とは別のアプローチで、引数の型を区別しつつStringリテラルの利用を許可させてみます。
StringリテラルをPathとして受けられるように拡張します。

extension Path: ExpressibleByStringLiteral {
    init(stringLiteral value: StringLiteralType) {
        self.value = value
    }
    init(unicodeScalarLiteral value: String) {
        self.init(stringLiteral: value)
    }
    init(extendedGraphemeClusterLiteral value: String) {
        self.init(stringLiteral: value)
    }
}

そうすると、以下のようにStringリテラル直接使用した場合や、Path型を明示した場合は使えますが、String型として取り扱っている場合(変数path)はコンパイルエラーになります。

func request(for path: Path) -> URLRequest {
    return URLRequest(url: URL(string: path.value)!)
}
request(for: "local:80/users")
let path = "local:80/users"
request(for: path) // コンパイルエラー
let path2: Path = "local:80/users"
request(for: path2)

上に書いたGo言語でtypeを使った場合と似た挙動ですね。


最後の2つの例は利用シーンが限定されるとは思いますが、型の区別と取り扱いのしやすさを両立したい時の実装パターンでした。
(素直にベタにstruct使った方が良いような、とは思いつつ。)

40
28
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
28