Runtime Generics, or something?

Hello, I'm trying to do something that I think should be simple but I can't figure it out.

Basically I'm trying to make my class respond to a String input and assume a certain associated type for all future operations. I could do this with subclasses and a factory pattern but that seems clunky given Swift's more modern features.

I have tried a few different iterations of giving the Datafile a generic parameter as in class Datafile<DataType: FixedWidthInteger> {} or similar (class Datafile<DataType> where DataType: FixedWidthInteger) but I keep getting stuck with that too.

Does anyone have an idea whether this is possible? Here's the essential code I'm working with:

class Datafile {
    fileprivate let CurrentDataType: DataType.Type
    
    let activePoints: [Bool]

    init?(filepath: String) {
        if let CurrentDataType = DataType(from: filepath) {
            self.CurrentDataType = CurrentDataType
        } else {
            return nil
        }
    }

    // ...

    getDataPoints() -> [Int16] {
        let pointSize = MemoryLayout<CurrentDataType>.size // Swift doesn't agree this is a Type
        // ...
        var tempBufferArray = [CurrentDataType](repeating: 0, count: 128) // not possible
        fread(&tempBufferArray, size, numberOfDataPoints, inputFile)
    }
}

The closest I've got is defining a protocol

protocol DataReader {
    associatedtype DataType where DataType: FixedWidthInteger
}

and making Datafile conform to it. But then I'm stuck defining multiple types again, each with their own DataType typealias.

Although you can't use P.Type variables as types in Swift directly, one thing you can do instead is put an extension on the protocol P. Within the extension, the type Self refers to the dynamic type of the value, so you can try this:

extension DataType {
  static func _readDataPoints() -> [Int16] {
    let pointSize = MemoryLayout<Self>.size
    ...
    var tempBufferArray = [Self](repeating: 0, count: 128)
    fread(&tempBufferArray, size, numberOfDataPoints, inputFile)
    ...
    return result
  }
}

class Datafile {
  let CurrentDataType: DataType.Type

  func getDataPoints() -> [Int16] {
    return CurrentDataType._readDataPoints()
  }
}
1 Like

Thanks for the tip! This is a side project I was working on so I’m not sure when I’ll get back to it but I’ll keep it in mind.

1 Like

I tried this but I keep getting stuck with defining DataType as anything useful: it has to be a FixedWidthInteger to be meaningful. More helpfully, it must be initiable via Int(value), have basic arithmetic capabilities (support basic math operators) must have .max and .min. I can't seem to define the type in any way that will give me these capabilities without a Self requirement.

Either I try to make define DataType as conforming to FixedWidthInteger, in which case I can't return -> DataType or DataType.Type (Protocol 'DataType' can only be used as a generic constraint because it has Self or associated type requirements). Or I don't define it as a FixedWidthInteger and then the actual readDataPoints function is extremely limited in its capabilities.

Any ideas?

Edit: I managed to get the code as simple as the following, which is much more satisfying than before.

let fileExtension = filepath.suffix(3)

switch fileExtension {
case "ui8":
  print(UInt8.getAllData(from: datafile))
case "si8":
  print(Int8.getAllData(from: datafile))
case "i16":
  print(Int16.getAllData(from: datafile))
default:
  fatalError("Unsupported File Type: \(fileExtension)")

}
extension FixedWidthInteger {
     static func getAllData(from dataFile: DataFile) -> ...
}

It still feels weird to have to perform the call three separate times. Is there no way to return, say, a FixedWidthInteger and just do the call once? At the moment the code isn't doing much but I have concerns that this won't be very extensible or maintainable as written..

let type = getType(fileExtension)
type.getAllData(...) // is this ever possible if type has Self requirements?

func getType(_ fileExtension: String) -> FixedWidthInteger {
    switch fileExtension {
    case "si8": return Int8.self
    case "ui8": return UInt8.self
    case "i16": return Int16.self
    }
}

You could factor the code so that you pass the type into a common local function that invokes the generic interface:

func getData(from datafile: File) -> Data {
  func doIt<T: FixedWidthInteger>(_ type: T.Type) -> Data {
    return type.getAllData(from:datafile)
  }
  switch datafile.suffix(3) {
  case "ui8": return doIt(UInt8.self)
  case "si8": return doIt(Int8.self)
  ...
  }
}
2 Likes