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
というパラメーター名がすでにあるのに、さらにわざわざtypealias
でPath
という名前を用意していて冗長- 他にメリットがあるなら良いがわざわざこのためだけに
typealias
使うのは違和感 - String型を受けるという情報を分かりにくくさせてしまっている
- 他にメリットがあるなら良いがわざわざこのためだけに
- Stringという単純な型に対して
typealias
で別名を与えているが、これは単なる別名であって、String
と全く同じ扱いになる-
String
とPath
は区別されない -
Path
とは何?と思ってコードを辿ると、ただのString
かと分かるようになっていて、まどろっこしいだけ
-
String
とPath
は区別されない
特にこの特性にも関わらず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
使った方が良いような、とは思いつつ。)