Are there naming conventions for dealing with no variadic generics/tuples?

I don't think so. Just checking to see if I've missed some good ideas.

e.g.

/// Multiple integers packed into `Storage`.
public enum PackedInteger<Storage: FixedWidthInteger> {
  public enum Error: Swift.Error {
    /// `bitWidths`: The number of bits necessary to store each integer that was attemped to pack.
    case notEnoughBits(bitWidths: [Int])
  }

  /// Two integers packed into one.
  public struct Two<Integer0: FixedWidthInteger, Integer1: FixedWidthInteger> {
    /// - Parameter storage: The integer that two others are packed into.
    /// - Throws: `PackedInteger.Error.notEnoughBits`
    ///   if the two integer types won't fit into `Storage` together.
    public init(storage: Storage) throws {
      try Self.verifyBitsCanFit()
      self.storage = storage
    }

    /// The integer that two others are packed into.
    public let storage: Storage
  }

  /// Three integers packed into one.
  public struct Three<
    Integer0: FixedWidthInteger, Integer1: FixedWidthInteger, Integer2: FixedWidthInteger
  > {
    /// - Parameter storage: The integer that three others are packed into.
    /// - Throws: `PackedInteger.Error.notEnoughBits`
    ///   if the three integer types won't fit into `Storage` together.
    public init(storage: Storage) throws {
      try Self.verifyBitsCanFit()
      self.storage = storage
    }

    /// The integer that three others are packed into.
    public let storage: Storage
  }

  /// Four integers packed into one.
  public struct Four<
    Integer0: FixedWidthInteger, Integer1: FixedWidthInteger,
    Integer2: FixedWidthInteger, Integer3: FixedWidthInteger
  > {
    /// - Parameter storage: The integer that four others are packed into.
    /// - Throws: `PackedInteger.Error.notEnoughBits`
    ///   if the four integer types won't fit into `Storage` together.
    public init(storage: Storage) throws {
      try Self.verifyBitsCanFit()
      self.storage = storage
    }

    /// The integer that four others are packed into.
    public let storage: Storage
  }
}

private protocol PackedIntegerProtocol: Hashable {
  /// Same as `Storage`.
  /// [Nested generic types do not inherit associatedtypes like non-generic ones.](https://bugs.swift.org/browse/SR-12700)
  associatedtype _Storage: FixedWidthInteger

  /// The number of bits used for the underlying binary representations of each packed integer.
  static var bitWidths: [Int] { get }

  init(storage: _Storage) throws

  /// The integer that the others are packed into.
  var storage: _Storage { get }
}

extension PackedIntegerProtocol {
  /// - Throws: `PackedInteger<Storage>.Error.notEnoughBits`
  ///   if the desired integer types won't fit into `Storage` together.
  static func verifyBitsCanFit() throws {
    guard _Storage.bitWidth >= Self.bitWidths.sum! else {
      throw PackedInteger<_Storage>.Error.notEnoughBits(bitWidths: Self.bitWidths)
    }
  }

  /// - Parameter bitPatterns: Unsigned versions of the packed integers.
  /// - Throws: `PackedInteger<Storage>.Error.notEnoughBits`
  ///   if the desired integer types won't fit into `Storage` together.
  init(bitPatterns: _Storage.Magnitude...) throws {
    try self.init(storage:
      zip(bitPatterns, Self.bitWidths).reduce(into: 0) {
        ( packed,
          integer: (bitPattern: _Storage.Magnitude, bitWidth: Int)
        ) in
        packed <<= integer.bitWidth
        packed |= .init(truncatingIfNeeded: integer.bitPattern)
      }
    )
  }

  /// The untruncated bit patterns for the packed integers.
  var untruncatedBitPatterns: [_Storage.Magnitude] {
    Self.bitWidths.reversed().reduce( into: ([], storage.bitPattern) ) {
      bitPatterns, bitWidth in
      bitPatterns.0.append(bitPatterns.1)
      bitPatterns.1 >>= bitWidth
    }.0.reversed()
  }
}

//MARK: - PackedInteger.Two
public extension PackedInteger.Two {
  /// - Parameters:
  ///   - integer0: Will be bit-shifted left of `integer1`.
  ///   - integer1: Stored directly.
  /// - Throws: `PackedInteger.Error.notEnoughBits`
  ///   if the two integer types won't fit into `Storage` together.
  init(_ integer0: Integer0, _ integer1: Integer1) throws {
    try self.init(bitPatterns:
      .init(truncatingIfNeeded: integer0.bitPattern),
      .init(truncatingIfNeeded: integer1.bitPattern)
    )
  }

  /// Unpack two integers.
  var unpacked: (Integer0, Integer1) {
    ( .init(truncatingIfNeeded: untruncatedBitPatterns[0]),
      .init(truncatingIfNeeded: untruncatedBitPatterns[1])
    )
  }
}

extension PackedInteger.Two: PackedIntegerProtocol {
  typealias _Storage = Storage

  static var bitWidths: [Int] {
    [Integer0.bitWidth, Integer1.bitWidth]
  }
}

//MARK: - PackedInteger.Three
public extension PackedInteger.Three {
  /// - Parameters:
  ///   - integer0: Will be bit-shifted left of `integer1`.
  ///   - integer1: Will be bit-shifted left of `integer2`.
  ///   - integer2: Stored directly.
  /// - Throws: `PackedInteger.Error.notEnoughBits`
  ///   if the three integer types won't fit into `Storage` together.
  init(
    _ integer0: Integer0, _ integer1: Integer1, _ integer2: Integer2
  ) throws {
    try self.init(bitPatterns:
      .init(truncatingIfNeeded: integer0.bitPattern),
      .init(truncatingIfNeeded: integer1.bitPattern),
      .init(truncatingIfNeeded: integer2.bitPattern)
    )
  }

  /// Unpack three integers.
  var unpacked: (Integer0, Integer1, Integer2) {
    ( .init(truncatingIfNeeded: untruncatedBitPatterns[0]),
      .init(truncatingIfNeeded: untruncatedBitPatterns[1]),
      .init(truncatingIfNeeded: untruncatedBitPatterns[2])
    )
  }
}

extension PackedInteger.Three: PackedIntegerProtocol {
  typealias _Storage = Storage

  static var bitWidths: [Int] {
    [Integer0.bitWidth, Integer1.bitWidth, Integer2.bitWidth]
  }
}

//MARK: - PackedInteger.Four
public extension PackedInteger.Four {
  /// - Parameters:
  ///   - integer0: Will be bit-shifted left of `integer1`.
  ///   - integer1: Will be bit-shifted left of `integer2`.
  ///   - integer2: Will be bit-shifted left of `integer3`.
  ///   - integer3: Stored directly.
  /// - Throws: `PackedInteger.Error.notEnoughBits`
  ///   if the three integer types won't fit into `Storage` together.
  init(
    _ integer0: Integer0, _ integer1: Integer1,
    _ integer2: Integer2, _ integer3: Integer3
  ) throws {
    try self.init(bitPatterns:
      .init(truncatingIfNeeded: integer0.bitPattern),
      .init(truncatingIfNeeded: integer1.bitPattern),
      .init(truncatingIfNeeded: integer2.bitPattern),
      .init(truncatingIfNeeded: integer3.bitPattern)
    )
  }

  /// Unpack four integers.
  var unpacked: (Integer0, Integer1, Integer2, Integer3) {
    ( .init(truncatingIfNeeded: untruncatedBitPatterns[0]),
      .init(truncatingIfNeeded: untruncatedBitPatterns[1]),
      .init(truncatingIfNeeded: untruncatedBitPatterns[2]),
      .init(truncatingIfNeeded: untruncatedBitPatterns[3])
    )
  }
}

extension PackedInteger.Four: PackedIntegerProtocol {
  typealias _Storage = Storage

  static var bitWidths: [Int] {
    [Integer0.bitWidth, Integer1.bitWidth, Integer2.bitWidth, Integer3.bitWidth]
  }
}
public extension Sequence {
  var tuple2: (Element, Element)? { makeTuple2()?.tuple }
  var tuple3: (Element, Element, Element)? { makeTuple3()?.tuple }
  var tuple4: (Element, Element, Element, Element)? { makeTuple4()?.tuple }

  private func makeTuple2() -> (
    tuple: (Element, Element),
    getNext: () -> Element?
  )? {
    var iterator = makeIterator()
    let getNext = { iterator.next() }

    guard
      let _0 = getNext(),
      let _1 = getNext()
    else {
      return nil
    }

    return ( (_0, _1), getNext )
  }

  private func makeTuple3() -> (
    tuple: (Element, Element, Element),
    getNext: () -> Element?
  )? {
    guard
      let (tuple2, getNext) = makeTuple2(),
      let _2 = getNext()
    else {
      return nil
    }

    return ( (tuple2.0, tuple2.1, _2), getNext )
  }

  private func makeTuple4() -> (
    tuple: (Element, Element, Element, Element),
    getNext: () -> Element?
  )? {
    guard
      let (tuple3, getNext) = makeTuple3(),
      let _3 = getNext()
    else {
      return nil
    }

    return ( (tuple3.0, tuple3.1, tuple3.2, _3), getNext )
  }
}

Looking at Apple's Combine framework for those cases, here is how CombineLatest is declared

public struct CombineLatest<A, B> : Publisher where A : Publisher, B : Publisher, A.Failure == B.Failure { ...

public struct CombineLatest3<A, B, C> : Publisher where A : Publisher, B : Publisher, C : Publisher, A.Failure == B.Failure, B.Failure == C.Failure { ...

public struct CombineLatest4<A, B, C, D> : Publisher where A : Publisher, B : Publisher, C : Publisher, D : Publisher, A.Failure == B.Failure, B.Failure == C.Failure, C.Failure == D.Failure { ...
1 Like

There's also precedent in Combine's Publishers.Merge and Merge{3,4,5,6,7,8} and Publishers.Zip and Zip{3,4}. Also Publishers.MapKeyPath has MapKeyPath{2,3}.

In the standard library we also have Zip2Sequence, although there's no Zip3Sequence.

The various arities that are defined seems somewhat arbitrary.

I can't wait until we get variadic generics.

2 Likes

The lack of 2 in most of those makes me think that I got it right with my first example above, for consistency. (i.e. Imagine Merge being an enum, with struct for each number.) Trouble with that is the inability to use a numeral. Probably should just come up with some emoji to prefix them with…

Looks like Publishers is going to be much nicer with the variadics. But I also suspect its life won't be long; that s on the end is a hack to deal with not being to nest types in protocols.

1 Like

Consider that these are implementation details and usually "hidden" behind a function that creates them. In my oppinion whatever you choose and makes sense for you is the best way to go as long as you are consistent :slight_smile:

Since you can’t use protocols with associated types as opaque return types, i.e can’t return “some publisher where output is Foo” you often need to deal with these very deep and cumbersome type hierarchies. I guess this problem is orthogonal to the issue of variation generics, but its omission certainly doesn’t help make the type hierarchy simpler.

2 Likes