Generic Tuples

I’d like to make a generic swift class for querying a database. The result of a query should be an array of tuples, but the type of the tuple of course depends on the query.

I want to do something like this:

class Query<TupleType> {
     init(sql: String) { ... }
     func execute() -> [TupleType]
}

// example usage
let query = Query<(id: Int, name:String)>(sql:"SELECT id, name FROM users")
let rows = query.execute()
for user in query.execute() {
     print("\(user.id): \(user.name)")
 }

Now here’s the problem: At some point in my execute function I will need to convert the data from some type (probably [Any]) to the tuple type. But how can I do this? Does anybody have any ideas?

Is the length of your tuple fixed?

No, in general the tuple length is not fixed. But I could live with a workaround where I’d have different functions for different length tuples.

Swift won’t let you dynamically build a tuple. But it comes with other handy tools that will have your code look almost the same. Codable is one of them.

You may look at how GRDB lets you write, for example:

struct User: RowConvertible, Codable {
    var id: Int
    var name: String
}

let users = try User.fetchAll(db, "SELECT id, name FROM users")
for user in users {
    print("\(user.id): \(user.name)")
}

I have one solution in mind where you can overload the execute function for different tuple lengths, but it requires a missing generic feature (Parameterized extensions) and you’d lose the labels:

extension<A, B> Query where TupleType == (A, B) {
  func execute() -> [TupleType] {
    ...
  }
}

Combined with another missing generic feature (Variadic generics) you could get a dynamic version but you’d still lose the labels:

extension<A, ...B> Query where TupleType == (A, ...B) {
  func execute() -> [TupleType] {
    ...
  }
}

Thanks for the hints!

gwendal.roue: Codeable seems like a workable alternative, but I’d really prefer tuples. Most of the queries have different signatures, so I’d need to create a new struct for each query (which is doable, but not very elegant)

DevAndArtist: this looks interesting. I think I’ve found a similar solution for unlabelled tuples that uses factory methods and blocks for conversion. I could overload the factory method for different length tuples:

class Query<T> {
	private let converter: ([Any])->T
	init(sql: String, converter: @escaping ([Any])->T) {
		self.converter = converter
	}
	func execute() -> [T] {
		return [[1,"Jakob"], [2,"XY"]].map(converter)
	}
}

func makeQuery<A,B>(sql: String) -> Query<(A,B)> {
	return Query(sql: sql) { array in
		let a = array[0] as! A
		let b = array[1] as! B
		return (a,b)
	}
}

// this works
let query = makeQuery(sql:"SELECT id, name FROM users") as Query<(Int, String)>
for user in query.execute() {
	print("\(user.0): \(user.1)")
}

// unfortunately this doesn't work
let query = makeQuery(sql:"SELECT id, name FROM users") as Query<(id: Int, name: String)>
for user in query.execute() {
	print("\(user.id): \(user.name)")
}

// works, but is not as elegant as I'd hoped
let query = makeQuery(sql:"SELECT id, name FROM users") as Query<(Int, String)>
for user in query.execute() as [(id: Int, name:String)] {
	print("\(user.id): \(user.name)")
}
1 Like

Elegance is a matter of taste, fashion, and education, I guess. Unlabelled tuples are men of few words, and many see elegance in hard-core minimalism. But, as you can see, application code has to become verbose and strained in order to make up for their silence.

Most of the queries have different signatures

I totally understand that. A very common problem in such a situation is finding a proper name for each of those signatures. It may be difficult.

If it happens that some signatures are used in a single context (in a single file for example), then you can use private structs, and reuse the name of those private structs. For example, you’d have a private struct UserInfo in A.swift, and another private struct UserInfo in B.swift. Both private structs could evolve as the needs of A.swift and B.swift evolve, independently.

If it happens that some signatures are often used in many contexts (in many files of your app), then it’s worth thinking hard about finding a proper name for them. For example, you’d have a shared struct User that contains the usual user columns.

This straightforward pattern can make it much easier to name your signatures. You’ll maybe find some… elegance in it ;-)

YMMV: above all do as you please.

So I’ve made some more experiments. I can use named tuples if I stick with simple arrays:

enum MyError: Error {
	case cantConvert
}

class QueryResult {
	func tuples<A,B>() throws -> [(A,B)] {
		let array: [[Any]] = [[1,"Jakob"],[2,"X"]]
		var result = [(A,B)]()
		for row in array {
			guard let a = row[0] as? A else { throw MyError.cantConvert }
			guard let b = row[1] as? B else { throw MyError.cantConvert }
			result.append((a, b))
		}
		return result
	}
	func tuples<A,B,C>() throws -> [(A,B,C)] {
		let array: [[Any]] = [[1,"Jakob", "Egger"],[2,"X", "Y"]]
		var result = [(A,B,C)]()
		for row in array {
			guard let a = row[0] as? A else { throw MyError.cantConvert }
			guard let b = row[1] as? B else { throw MyError.cantConvert }
			guard let c = row[2] as? C else { throw MyError.cantConvert }
			result.append((a, b,c))
		}
		return result
	}
}

let result = QueryResult()

let tuples = try! result.tuples() as [(id: Int, name: String)]
for tuple in tuples {
	print("\(tuple.id): \(tuple.name)")
}

let triples = try! result.tuples() as [(id: Int, firstName: String, lastName: String)]
for triple in triples {
	print("\(triple.id): \(triple.firstName) \(triple.lastName)")
}

But I wanted to use my own result type, not just an array (so I can return additional information). It seems that Swift has special treatment of tuple arrays that don’t extend to other generic types. Consider the following example:

let arr1: Array<(Int,String)> = [(1,"Jakob")]

let arr2: Array<(id: Int, name: String)> = arr1
// works


struct MyStruct<T> {
	let tuple: T
}

let struct1: MyStruct<(Int,String)> = MyStruct(tuple:(1,"Jakob"))

let struct2: MyStruct<(id: Int, name: String)> = struct1
// error: cannot convert value of type 'MyStruct<(Int, String)>' 
//        to specified type 'MyStruct<(id: Int, name: String)>'

So it seems that Swift sometimes treats labelled and unlabelled as the same type, and sometimes as a different type.