Skip to content

Instantly share code, notes, and snippets.

@seanwoodward
Last active March 19, 2024 04:36
Show Gist options
  • Save seanwoodward/44e2f5aca0dda994e9595eee58750f65 to your computer and use it in GitHub Desktop.
Save seanwoodward/44e2f5aca0dda994e9595eee58750f65 to your computer and use it in GitHub Desktop.
Tuple struct using Parameter Packs to facilitate things like using a tuple as keys in dictionaries or values in sets and heaps.
#if swift(>=5.9)
import Foundation
public struct Tuple<T, each U> {
private let head: T
private let tail: (repeat (each U))
public var values: (T, repeat each U) {
(head, repeat each tail)
}
public init(_ head: T, _ tail: repeat each U) {
self.head = head
self.tail = (repeat (each tail))
}
}
extension Tuple: Sendable where T: Sendable, repeat (each U): Sendable {}
extension Tuple: Hashable where T: Hashable, repeat (each U): Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(self.head)
repeat (hasher.combine(each self.tail))
}
}
extension Tuple: Equatable where T: Equatable, repeat (each U): Equatable {
private struct NotEqual: Error {}
public static func == (lhs: Tuple<T, repeat (each U)>, rhs: Tuple<T, repeat (each U)>) -> Bool {
guard lhs.head == rhs.head else { return false }
func isEqual<V: Equatable>(_ left: V, _ right: V) throws {
guard left == right else { throw NotEqual() }
return
}
do {
repeat try isEqual(each lhs.tail, each rhs.tail)
} catch {
return false
}
return true
}
}
extension Tuple: Comparable where T: Comparable, repeat (each U): Comparable {
// https://github.com/apple/swift-evolution/blob/1b0b339bc3072a83b5a6a529ae405a0f076c7d5d/proposals/0015-tuple-comparison-operators.md
private struct NotOrderBefore: Error { }
// throwing short circuits the checks
public static func < (lhs: Tuple<T, repeat (each U)>, rhs: Tuple<T, repeat (each U)>) -> Bool {
func isOrderedBeforeThrows<V: Comparable>(_ left: V, _ right: V) throws {
if left != right {
guard left < right else { throw NotOrderBefore() }
}
}
do {
try isOrderedBeforeThrows(lhs.head, rhs.head)
repeat try isOrderedBeforeThrows(each lhs.tail, each rhs.tail)
} catch {
return false
}
return true
}
// exhaustive
// public static func < (lhs: Tuple<T, repeat (each U)>, rhs: Tuple<T, repeat (each U)>) -> Bool {
// var results: [(notEqual: Bool, orderedBefore: Bool)] = [ (lhs.head != rhs.head, lhs.head < rhs.head)]
//
// func isOrderedBefore<V: Comparable>(_ left: V, _ right: V) {
// results.append((left != right, left < right))
// }
//
// repeat isOrderedBefore(each lhs.tail, each rhs.tail)
//
// return results.first(where: { $0.notEqual})?.orderedBefore ?? results.last!.orderedBefore
// }
}
extension Tuple: CustomStringConvertible {
public var description: String {
func describe<V>(_ value: V) {
if let _ = value as? any StringProtocol {
let valueString = String(describing: value)
// allows the presentation of a string value with quotes
// e.g head = "red", the description will be "red" with quotes
// not just, red, without quotes.
descriptions.append("\(valueString.debugDescription)")
} else {
descriptions.append("\(String(describing: value))")
}
}
var descriptions: [String] = []
describe(self.head)
repeat (describe(each tail))
return descriptions.count == 1 ? "\(descriptions[0])" : "(\(descriptions.joined(separator: ", ")))"
}
}
extension Tuple: CustomDebugStringConvertible {
public var debugDescription: String {
"values = \(self.description)"
}
}
#endif
@randomeizer
Copy link

randomeizer commented Mar 14, 2024

How do you actually it use it?

@seanwoodward
Copy link
Author

@randomeizer, use it as an intermediary to allow an ad hoc tuple of values to either be a value in a set, a key in a dictionary, or conform to a protocol like Comparable. Consider the interview question regarding finding the maximized capital.

func findMaximizedCaptial(_ k: Int, _ w: Int, _ profits: [Int], _ capital: [Int]) -> Int {
  let getCapital: (Tuple<Int, Int>) -> Int = { $0.values.0 }
  let getProfit: (Tuple<Int, Int>) -> Int = { $0.values.1 }
  
  var maxProfit: Heap<Int> = []
  var minCapital: Heap<Tuple<Int, Int>> = .init(zip(capital, profits).map(Tuple.init) )
  
  var w = w
  
  for _ in 1...k {
    while !minCapital.isEmpty, let minC = minCapital.min, getCapital(minC) <= w {
      maxProfit.insert(getProfit(minCapital.removeMin()))
    }
    guard !maxProfit.isEmpty else { break }
    w += maxProfit.removeMax()
  }
  
  return w
}

Here, the zip function combines the elements from both arrays into a Swift tuple, (profit, capital). While you can use the comparison operator < with a Swift tuple, it cannot not conform to the Comparable protocol and cannot be evaluated as an element in a Heap. Instead of creating a specific struct to handle the combined values, the Tuple struct can take the values and allow them to be evaluated appropriately.

Are you using Parameter Packs in any interesting ways in your code?

@randomeizer
Copy link

I haven't really looked into exactly what they allow, to be honest. Can it support an arbitrary number of mixed types in the signature, like "real" tuples? Or is U constrained to a single type?

@seanwoodward
Copy link
Author

@randomeizer, yes parameter packs allow for arbitrary mixed types like built-in tuples. U types are not constrained, but they are not dynamic. You could not change an instance of one Tuple(1 as Int, 2.0 as Double) to an instance of Tuple("John", 490 as Int).

@randomeizer
Copy link

Neat. I need to dig into them a bit I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment