Edited at

[Swift] UnboundedRangeを“呼ぶ”

More than 1 year has passed since last update.


最初に

 この記事は、Swiftのコードを書く上で、実際に役に立つ内容は書いてありません

 ただ、UnboundedRangeがどのように実装されているのかを考察することで、多少コーディングの幅が広がることはあるかもしれません。

 そして「UnboundedRangeを“呼ぶ”」とはどういうことなのか、ということは最後まで読んでいただければわかるかと思います…。


そもそもUnboundedRangeとは

 UnboundedRangeは、その名の通りboundの無いrangeのことで、簡単に言えば全範囲を表すものです。コード上では単に...と書きます。

 たとえば、let array: Array<Int> = [0, 1, 2, 3, 4]という配列があったとき、array[...]と書くと、[0, 1, 2, 3, 4]というArraySlice<Int>が生成されます。


UnboundedRangeはどのように実装されているのか

 幸いにもSwiftはオープンソースなのでその実装を覗き見ることができます。Swift 4.1.2現在、UnboundedRangeの実装は次のようになっています(ソース):

public typealias UnboundedRange = (UnboundedRange_)->()

…おや?

UnboundedRangeは、「UnboundedRange_型のオブジェクト(インスタンス)を引数にとって戻り値のない関数」の別名となっているだけでした。


UnboundedRange_とは?

これもソースコードを覗き見てみましょう(ソース)。実装部分だけを抜き出すと次の通りになります:

public enum UnboundedRange_ {

public static postfix func ... (_: UnboundedRange_) -> () {
fatalError("uncallable")
}
}

UnboundedRange_は、一つだけstatic関数を持つ空のenumでした。


なぜUnboundedRange_を用意する必要があるのか?

 Swiftの言語仕様上(文法規則上)、...はoperator(演算子)であってidentifier(識別子)にはなり得ません。すなわち、let ...: UnboundedRange = UnboundedRange()みたいな書き方は絶対にできません。でも、他のrangeと同様にarray[...]という書き方はしたいという想いを実現したいとします。となると、...というoperatorをそのままsubscriptの引数に渡すしかありません。


operatorを引数にすることはできる

 一方で、Swiftではoperatorを引数にすることはできます。Arraysorted(by:)なんかは有名なところでしょう:

array.sorted(by:>) // -> [4, 3, 2, 1, 0]

なので、array[...]という書き方を実現したければ、...という適当な関数をでっちあげればいいことになります。


適当な関数を用意するために

...という関数が、そこらへんにありそうな関数(たとえば(Int) -> Intとか)だと混乱を来す可能性があります。(たぶん)そのために、UnboundedRange_は生まれたのです。実際のソースコードではUnboundedRange_のstatic関数として...が宣言されていますが、グローバルな領域でpublic postfix func ...(_:UnboundedRange_) {}と記述しても大丈夫なはずです。


これで実現

あとはArray側でsubscript(_:UnboundedRange)(即ちsubscript(_:(UnboundedRange_) -> ()))を実装すればarray[...]と書けるようになります。


ということは

subscriptの引数に(UnboundedRange_) -> ()を渡せば同じ効果が得られるので、次のようなこともできます。

func f(_:UnboundedRange_) {}

print(array[...] == array[f]) // -> true

したからといって何かあるわけではありませんが。


そして、UnboundedRangeを“呼ぶ”

これでやっとタイトルの内容に入ることになります。UnboundedRangeは関数ということが判明しました。関数であるからには、一度は呼んでみたい。そう思うのが人の子ではないでしょうか。

UnboundedRange(UnboundedRange_)->()であるので、まずは引数として渡すUnboundedRange_のインスタンスを生成することにしましょう。


UnboundedRange_のインスタンスを作る?

UnboundedRange_は上記の通り、空のenumでした。空のenumというのは、そもそも無駄なインスタンスを作らせないために用意するものです。即ちlet u = UnboundedRange_()とはできないのです。


UnboundedRange_のインスタンスだと偽る

素直にインスタンスを用意することができないことが分かりました。となると、unsafe系の関数を思いつくかと思います。即ち、C言語でいうキャストを(無理やり)行うのです。

たとえば、

let u = unsafeBitCast(0, to:UnboundedRange_.self)

は、どうでしょう?

…結論から言うと残念ながらできません。Fatal error: Can't unsafeBitCast between types of different sizesというエラーを吐き出します。MemoryLayout<UnboundedRange_>.size0なのでIntのサイズとは異なるのです。

なので、しかたなく、こうします:

struct S {}

let u = unsafeBitCast(S(), to:UnboundedRange_.self) // No error

これならうまくいきました。

あとはu...とすれば…

u... // -> "Fatal error: uncallable"

見事、Fatal error: uncallableというエラーを吐いてクラッシュしました!

"uncallable"な関数をcallすることに成功したのです。


別解

構造体を用意したくないのならば次のようにもできます:

var i = 0

withUnsafePointer(to:&i) {
$0.withMemoryRebound(to:UnboundedRange_.self, capacity:1) {
let u = $0.pointee
u... // -> "Fatal error: uncallable"
}
}


まとめ


  1. 特定の演算子を引数にする実装を実現したければ、空のenumを用意して行うのが吉。

  2. unsafe系の関数を使えば、(publicである限り)呼べない関数はない。