LoginSignup
6
4

[Swift] Tuple タプルの七不思議

Last updated at Posted at 2024-01-13

Swiftのタプルには謎が多い

具体的に7つの不思議があるわけではありませんが、コード例を交えながらタプルの不思議(魅力)をお伝えできればと思います。

特に断りがない場合は、執筆時点の最新であるSwift 5.9.2を前提とします。


まず、はじめは、

いきなりですが、

`Tuple型`は存在しません

タプルに`型`は存在しますが、Tupleといった基底型プロトコル定義されていない、ということです。

例えば、xInt型かを判定する場合に`x is Int`と書きますが、
任意のタプルx`x is Tuple`では判定はできません。なぜなら、共通的な(汎用的な)Tuple型は存在しないからです。

print(10 is Int) //true
print((10, 20) is Tuple) //error: cannot find type 'Tuple' in scope
  • タプルはそれぞれが`型`
typealias Tuple1 = (Int, Int)
typealias Tuple2 = (Int, String)
let tuple1 = (10, 20)

print((10, 20) is Tuple1) //true
print(tuple1 is (Int, Int)) //true

print((10, 20) is Tuple2) //false
print(type(of: (10, 20))) //(Int, Int)
print(type(of: (a: 10, b: 20))) //(a: Int, b: Int)
print(type(of: (10, s: "A"))) //(Int, s: String)

強いてタプル型を判定するなら、『((左カッコ)で始まる』型、でしょうか。

タプルの「名前」はオプション

タプルの各項目には項目名を付けることができますが、オプションのため、項目名のある/なしの混在が可能です。

  • 名前付きタプルでも、名前による区別がされない場合がある
//typealias Tuple1 = (Int, Int)
print((a: 10, b: 20) is Tuple1) //true

typealias Tuple3 = (a: Int, b: Int)
typealias Tuple4 = (c: Int, d: Int)

print((10, 20) is Tuple3) //true
print((10, 20) is Tuple4) //true
print((a: 10, 20) is Tuple3) //true
print((10, d: 20) is Tuple4) //true

print((b: 10, a: 20) is Tuple3) //false ∴名前(の順序)が異なる
print((c: 10, 20) is Tuple3) //false ∴名前が異なる

タプルの「名前」はオプション(その2)

  • 「比較」において、名前は区別されない

要素(値)の並びだけで比較される。
(名前を無視して、要素の型の並びが同じであれば比較できる)

var tuple3 = (a: 10, b: 20)
var tuple4 = (c: 10, d: 20)
print(tuple3 == tuple4) //true
print(tuple3 < (x: 10, y: 21)) //true

print(tuple3 == (10, "A")) // error: tuple type '(Int, String)' is not convertible to tuple type '(Int, Int)'

代入時、名前付きタプルの扱いは要注意

  • 名前付きタプルの変数に、名前なしタプルの代入が可能
  • 名前付きタプルの変数に、異なる名前付きタプルの代入は不可(型が異なる)
//var tuple3 = (a: 10, b: 20)
tuple3 = (20, 30) //OK
tuple3 = (x: 10, y: 20) //error: cannot assign value of type '(x: Int, y: Int)' to type '(a: Int, b: Int)'

ここまでは、まだ理解できるが、次が問題。

  • 名前が同じであれば、名前の順序が異なっていても「代入」可能
//var tuple3 = (a: 10, b: 20)
let tuple5 = (b: 10, a: 20)
tuple3 = tuple5 //OK
print(tuple3 == tuple5) //false ⭐️
// マジで代入できちゃう、しかも代入後はイコールじゃない??

⭐️ 普通に考えると、代入すれば両者は同じになるはず。しかし、false とは何故?
中身を見ると、その理由が分かります。

print(tuple5) //(b: 10, a: 20)
print(tuple3) //(a: 20, b: 10))

名前の並び(10と20の並び)が異なっています。だから、同値では有りません。
つまり、右辺が名前付きタプルの場合の代入においては、名前の順序は無視されて、対応する名前の要素に代入される、ということです。

//let tuple1 = (10, 20)
//右辺が名前なしタプル`tuple1`の代入は、(位置(順序)で代入)
(tuple3.0, tuple3.1) = (tuple1.0, tuple1.1) //と等価に対して、

tuple3 = tuple5 //この代入は、(名前で代入)
(tuple3.a, tuple3.b) = (tuple5.a, tuple5.b) //と等価となる
//こう書くと、なぜか納得感がある

「型の判定」、「比較」、「代入」では、名前の扱いが違う

このことによって、アプリ開発で混乱することはあまり無いとは思いますが、
名前あり/なし混在のタプルを使う場合は、注意が必要です。

それにしても、どうしてこのような言語仕様になったのでしょうか??


話は変わって、

タプルの要素をKeyPathでアクセスできるか?

普通にKeyPathでアクセスできます。

let tuple4KeyPath_c = \Tuple4.c
print(type(of: tuple4KeyPath_c)) //WritableKeyPath<(c: Int, d: Int), Int>

print(tuple4[keyPath: tuple4KeyPath_c]) //10
print(tuple4[keyPath: \Tuple4.d]) //20
print(tuple4[keyPath: \Tuple4.0]) //10
print(tuple4[keyPath: \Tuple4.1]) //20

print(tuple4[keyPath: \Tuple4.2]) //error: value of tuple type 'Tuple4' (aka '(c: Int, d: Int)') has no member '2'

tuple4.0tuple4[0]のようにsubscriptでアクセスできないか考えましたが、簡単にはできそうにありません。
もし、できるならば、tuple4[n]のように動的に要素にアクセスできるのだが・・・

タプルの比較

初期のSwiftでは、タプル同士の比較ができませんでしたが、
どこかのバージョンでタプル同士の比較ができるようになり、すごく便利になりました。
タプルの要素を左から順に比較していきます。

let compareTuple = (1, 10, 10.0, "Z") < (1, 11, 1.1, "A")  //true

これを応用すると、複数のソート項目によるソートの比較関数が簡単に定義できます。ソート対象がタプル配列である必要はありません。structやclassの配列にも使えます。

typealias Point = (x: Int, y: Int)
let points: [Point] = [(8, 2), (1, 9), (7, 5), (5, 4), (5, 0)]
let sotredPoints = points.sorted(by: <) // $0 < $1
print(sotredPoints)
//[(x: 1, y: 9), (x: 5, y: 0), (x: 5, y: 4), (x: 7, y: 5), (x: 8, y: 2)]

//(タプル以外の配列でも可)X小さい、Y大きい順
let sotredPoints2 = points.sorted { ($0.x, $1.y) < ($1.x, $0.y) }
print(sotredPoints2)
//[(x: 1, y: 9), (x: 5, y: 4), (x: 5, y: 0), (x: 7, y: 5), (x: 8, y: 2)]

タプルを使わずに「X小さい、Y大きい順」の比較関数を定義すると、下記のように書く必要がありました。

//let sotredPoints2 = points.sorted { ($0.x, $1.y) < ($1.x, $0.y) }
let sotredPoints2 = points.sorted { 
    ($0.x == $1.x) ? ($0.y > $1.y) : ($0.x < $1.x)
}

ソート項目が3つ4つと増えると、ネストが増えて 可読性が下がります。タプルで書く方が断然分かり易いです。

タプルはEquatableComparableではない

タプルは、単独では、==で同値判定したり、<>などで大小比較できるのですが、SwiftプロトコルのEquatableComparableには準拠していません。
このため、タプル配列に対してcontainsメソッドは使えないし、sortも比較関数を書かないと使えません。

var points = [(8, 2), (1, 9), (7, 5), (5, 4), (5, 0)]
if points.contains((8, 2))  { /*・・・*/ } //error: cannot convert value of type '(Int, Int)' to expected argument type '((Int, Int)) throws -> Bool'
points.sort() //error: type '(Int, Int)' cannot conform to 'Comparable'

タプルはHashableでもない

さらに、Hashableにも準拠していないため、Set型の要素に使えないし、Dictonary型のkeyとしても使えないです。

let pointSet = Set(points) //error: type '(Int, Int)' cannot conform to 'Hashable'
var pointDict: [(Int, Int): Int] = [:] //error: type '(Int, Int)' does not conform to protocol 'Hashable'

これらを不便と感じる方も多いと思います。自分もその一人です。

タプルをEquatableComparableHashableに準拠させる

しかし、タプルはextensionが使えません💢。

typealias Point = (x: Int, y: Int)
extension Point { } // error: non-nominal type 'Points' (aka '(x: Int, y: Int)') cannot be extended

↓構造体を定義するしかないのです。
(Genericには出来ないが、マクロ化はできるかも?)

struct Tuple {
    typealias Element = (x: Int, y: Int) //カスタマイズする
    let tuple: Element
    init(_ _tuple: Element) { self.tuple = _tuple }
    func callAsFunction() -> Element { self.tuple }
}
extension Tuple: Equatable {
    static func == (lhs: Tuple, rhs: Tuple) -> Bool {
        lhs.tuple == rhs.tuple
    }
}
extension Tuple: Comparable {
    static func < (lhs: Tuple, rhs: Tuple) -> Bool {
        lhs.tuple < rhs.tuple
    }
}
extension Tuple: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(tuple.x)
        hasher.combine(tuple.y)
    }
}
extension Tuple: CustomStringConvertible {
    //おまけで`CustomStringConvertible`にも準拠しておく
    var description: String {
        "\(type(of: self))(\(tuple.x), \(tuple.y))"
    }
}

( ↑準拠するプロトコルと必要メソッドの対応を分かり易くするため、敢えてそれぞれをextensionしています。)

使い方
let tupple = Tuple((10, 20))
let (x, y) = tupple()
print(x, y) //10 20

var array = [Tuple]()
if array.contains(tupple)  { /*・・・*/ } //ok
array.sort() //ok

しかし、上記の例Tuple(x: Int, y: Int)ように、同じ型の要素だけで 要素数も 2つ3つ程度なら、わざわざEquatableComparableHashableのためのコードを書かずに、単に配列にして対応する場合が多いです。

//本来の タプル (x, y) を、配列 [x, y] で代用。
let set = Set<[Int]>()
let dict = [[Int]: Int]()

要素の型が異なる場合にはArray<Any>にすれば可能ですが、参照するときの型変換が面倒かも知れません。

タプルは、Codableでもない

タプルは一切のどのプロトコルにも準拠していません。EquatableComparableHashableだけの話では無いのです。

当然、Codableにも準拠していません。これによる影響は、例えば、タプルをjsonに変換したり、jsonからタプルに変換することができません。
独自の型を定義して、Codableに準拠させる必要が有ります。ここまで来れば当然のことですね。

くどくなってきた感がありますので、プロトコルの話はここで終わりにします。


話は変わって、

タプルの要素をイテレートできるか?

extensionが使えないため、IteratorProtocolに準拠することは難しいですが、一旦、配列に変換することで代用できます。

let tuple8 = (a: 10, c: "A", 1.0, e: (x: 10, y: 20), [10, 20])

let array8 = Mirror(reflecting: tuple8).children.map { ($0.label ?? "unlabeled", $0.value) }
print(type(of: array8)) //Array<(String, Any)>
print(array8)
//[("a", 10), ("c", "A"), (".2", 1.0), ("e", (x: 10, y: 20)), (".4", [10, 20])]

let type8 = array8.map { "\(type(of: $0.1))" }
print(type8)
//["Int", "String", "Double", "(x: Int, y: Int)", "Array<Int>"]

タプルがネストしている場合は、これを再帰的に変換する必要がありますが、名前、値、要素の型が取得できます。

for (type, (name, value)) in zip(type8, array8) {
    print((name, type, value))
}
//("a", "Int", 10)
//("c", "String", "A")
//(".2", "Double", 1.0)
//("e", "(x: Int, y: Int)", (x: 10, y: 20))
//(".4", "Array<Int>", [10, 20])

配列化出来たので、各要素の動的アクセスやランダムアクセスが可能になりますが、実用的かと問われると、?です。


これまでは、タプル型、タプル式についての内容でした。

次はパターンとしてのタプルについて、です。

パターンとしてのタプル

switchやif文のcaseラベルに書くパターンのことです。

if文(guard文)でのパターン

(x, y)で示す座標があったとして、その座標が範囲を超えているかどうかを、簡単にチェックできます。

let (Width, Height) = (20, 10)
let x = 10, y = 20
if case (0 ..< Width, 0 ..< Height) = (x, y) {
    //範囲内 
} else {
    //範囲外
}

//guard case (0 ..< Width, 0 ..< Height) = (x, y) else { return }

=演算子が代入に見えてしまうのが残念ですが・・・。

switch文のパターン

switch文のcaseパターンには、ほんと多彩な書き方ができます。
(例を作るのが面倒だったので、公式ドキュメントからのコピペで失礼します)

let somePoint = (1, 1)
switch somePoint {
case (0, 0):
    print("\(somePoint) is at the origin")
case (_, 0):
    print("\(somePoint) is on the x-axis")
case (0, _):
    print("\(somePoint) is on the y-axis")
case (-2...2, -2...2):
    print("\(somePoint) is inside the box")
default:
    print("\(somePoint) is outside of the box")
}
// Prints "(1, 1) is inside the box"
  • Value Bindings
let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
    print("on the x-axis with an x value of \(x)")
case (0, let y):
    print("on the y-axis with a y value of \(y)")
case let (x, y):
    print("somewhere else at (\(x), \(y))")
}
// Prints "on the x-axis with an x value of 2"
let stillAnotherPoint = (9, 0)
switch stillAnotherPoint {
case (let distance, 0), (0, let distance):
    print("On an axis, \(distance) from the origin")
default:
    print("Not on an axis")
}
// Prints "On an axis, 9 from the origin"
  • where (おまけ)
let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
    print("(\(x), \(y)) is on the line x == y")
case let (x, y) where x == -y:
    print("(\(x), \(y)) is on the line x == -y")
case let (x, y):
    print("(\(x), \(y)) is just some arbitrary point")
}
// Prints "(1, -1) is on the line x == -y"

where句はホント便利ですね。for文で使うことが度々有ります。

enumにおけるタプル

Swiftのenumは、単に「場合分け」を表すだけではなく、それに応じた値(Associated Values)を保持できるところが特徴です。

enum Barcode {
    case upc(system: Int, Int, Int, Int)
    case qrCode(String)
}

var productBarcode = Barcode.upc(system: 8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

このenum変数をswitch文で判定するときに、前項のcaseパターンが活躍します。

switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
    print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
    print("QR code: \(productCode).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."

enumのAssociated Valuesは、if文やswitch文のcaseパターンでしか、読み取る(受け取る)ことができません。


話が逸れますが、
case letのところは、case varとも書けることをご存知でしょうか?
varで受け取ると、それは変数となり「値の更新」ができます。
if letguard letでも同じです。


次は、またタプル型に戻りまして、

要素数0のタプル

Swiftでは、要素を一つも持たない要素数0のタプル(空のタプル)が認められています。

let emptyTuple = ()

実はこれは、Voidの実体です。Swiftの言語仕様でVoidは「空のタプル」と定義されています。

typealias Void = ()

Voidは、関数定義で戻り値を定義しなかった場合に仮定される型です。

//どちらも戻り値はVoid
func noResultFounction() /* -> Void を省略 */ { /*・・・*/ }
func voidResultFounction() -> Void { /*・・・*/ }

Voidを返す関数は、returnVoidの関数を呼び出すことができます。
(こういった使い方はあまり無いと思いますが。。。)

func noResultFounction() { 
    return //普通は書かないが、
    return Void() // とも書ける。さらに、
    return print(0) // Voidを返す関数も書ける
}

あえて、付け加えておきますが、
Voidを引数にとる関数を定義した場合は、これを呼び出すときは、引数としてVoidを渡す必要があります。戻り値Voidのように、省略することはできません。

func funcVoid(_ arg: Void) { }
funcVoid(Void()) //ok
funcVoid(()) //ok
funcVoid() //error: missing argument for parameter #1 in call

C / C++ のvoid func(void);では、呼び出す時に引数は書きませんが、それとは違うということです。念の為。

要素数1のタプル

残念なことに、Swiftでは、要素数1のタプルは定義できません。

var t1 = (1) //と書いても、単にInt型の変数
print(type(of: t1)) //Int

var t2 = ("A") //と書いても、単にString型の変数
print(type(of: t2)) //String

また、Pythonのように (1,)とも書けません。

var t3 = (1, ) //error: unexpected ',' separator


ちなみに、
どこかのバージョンで、Arrayの最終要素の後ろの,(カンマ)の記述を認めるようになりましたが、 タプルでは許していないようです。
(2個目以降のカンマならば認めて欲しいところですが、(1,)を認めないことに起因しているのかも知れません。)

let a4 = [1, 2, 3, 4, ] //OK
let t4 = (1, 2, 3, 4, ) //error: unexpected ',' separator


以上で、タプルを語り尽くせたと思いますが、もし抜け漏れや間違いなどがあれば、ご指摘ください。

この記事が何かの参考になれば幸いです。


最後に、 Swift言語文法サマリーに登場する「Tuple」の箇所を抜き出して終わりにします。

Tuple出現箇所

tuple.png

6
4
0

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
6
4