arraysswiftgenericscastingunsafemutablepointer

Swift 5: Merging Two Generic Functions with <T: BinaryInteger> & <T:BinaryFloatingPoint> with Output Type Specified


What I want to Work

My end goal is to have Array Data such as [Int], [UInt], [Float] passed through a generic function that converts any of these types and outputs the [UInt8] 0-255 values for pixel data. This is so I can make a Pointer to this UInt8 data, create a CFData (built-in type) out of this pointer, then finally give the CFData to a CGDataProvider and have this as the input to generate a grayscale Image from CGImage.init(). (This process could be simpler I think, but this is how I figured it out.)

My function I will have for reading looks like this:

    mutating func fromfileBI<T : BinaryInteger>(count : Int = 1) -> [T]?  {
    let byteSize = MemoryLayout<T>.stride
    var swiftTypeOut : [T] = []
    if count > 1 {
        swiftTypeOut = [T].init(repeating: 0, count: count)
        for i in 0...count-1 {
            swiftTypeOut[i] = data.subdata(in: off + i * byteSize..<off + (i + 1) * byteSize ).withUnsafeBytes{ $0.load(as: T.self )}
        }
        off += count * byteSize
    }
    else if count == 1 {
        swiftTypeOut =  [ data.subdata(in: off..<(off+1) * byteSize ).withUnsafeBytes{ $0.load(as: T.self )} ]
        off += byteSize
    }
    else if count == 0 {
        
        return []
    } else if count < 0 {
        print("Warning, fRead.int8Read( count : Int = 1) called with a negative count, returning empty array.")
        return []
    }
    return swiftTypeOut
}

However there is also an identical bodied function for BinaryFloatingPoint (BF)

mutating func fromfileBF<T : BinaryFloatingPoint>(count : Int = 1) -> [T]?

And the functions for formating the image data (This last step is all I care about... All the data will make it to the Image without any need of sharing the original type of the data (data will be [UInt8] returning from this function)).

Here is the one for BinaryInteger conforming types.:

func formatArrayDataforImage<T>(dataSet : Array< T >, count:  Int, numpyTypeName : String = "") -> [UInt8]! where T: BinaryInteger {



var f32Data = [Float](repeating: 0, count: count)
let obj = f32Data as NSObject // Object to synchronise
var UInt8BytesOut = [UInt8](repeating: 0, count: count)

DispatchQueue.concurrentPerform(iterations: count){ index in
    synchronized(obj) {
        let dataSetValue = dataSet[index]
        f32Data[index] = Float( dataSetValue )
    }
} // This DispatchQueue thing was necessary when I was dealing with pointers as input, Now I dont know.

guard let max = f32Data.max() else { fatalError("Find max fail.")}
guard let min = f32Data.min() else { fatalError("find min fail.")}

for i in 0..<f32Data.count {
    f32Data[i] -= min
    f32Data[i] =  255 * f32Data[i] / max
    UInt8BytesOut[i] = UInt8( f32Data[i] )
    if i<100 {
        print(UInt8BytesOut[i])
    }
}

return UInt8BytesOut

} I guess the other formatArrayDataforImage<T: BinaryFloatingPoint> will simply have the difference where Float(dataSetValue) which should work too, I think it's fine for Float32 to be cast to Float and Float8 etc. But the function call was demanding BinaryInteger conformance so we'll see.

RAMBLE:(skip it's only for context)–––––––

I have fixed a function that reads bytes and outputs them as the type desired, I've hardcoded all the way to getting one dataset loaded as type [UInt16], and this works. But I knew from the start I would have to learn about how to make the handling of types completely automatic. (The project code is somewhat embarrassing... I know I have to switch on a String value read from the source file to determine Swift datatype once.. but I ended up needing this switch often when reading, and right now I may have to hardcode the switch again.. how am I going to get the Swift data type known by whatever function is calling it? I don't want to use separate cases like switching on a datatype then casting var example_variable = dataWhosTypeIKnowOnly_FromRawString as [Int32] // <--I know your type from hardcoding the switch I suspect the answer to my question to be buried somewhere in this answer–C# generics: cast generic type to value type But given I'm dealing with wanting my generic-typed array specified for the creation of a pointer, I'm not sure how that could change things.

––––––––.

END Hope to learn once and for all how to become clear on proper Swift Type handling.


Solution

  • If I understand you correctly, you just want formatArrayDataforImage to accept any array of BinaryInteger or BinaryFloatingPoint without rewriting the whole thing.

    To do that, write it for floating point, and then call that from the integer version.

    I believe this is your floating point version:

    func formatArrayDataforImage<Element>(dataSet: [Element]) -> [UInt8]
    where Element: BinaryFloatingPoint {
        guard let max = dataSet.max() else { fatalError("Find max fail.")}
        guard let min = dataSet.min() else { fatalError("find min fail.")}
    
        // Note this is a little dangerous since it it crash if
        // min and max aren't in the range 0 - 1. I'd probably add
        // assertions at least.
        return dataSet.map { UInt8(255 * ($0 - min) / max) }
    }
    

    There's no particular reason to convert this to Float. It works fine for any BinaryFloatingPoint.

    And then to call it for a BinaryInteger, you would just need to map the values to a floating point type (like Float), like you're currently doing.

    func formatArrayDataforImage<Element>(dataSet: [Element]) -> [UInt8]
    where Element: BinaryInteger  // <=== Note different `where` clause
    {
        // Since this creates a [Float] it will call the other function
        formatArrayDataforImage(dataSet: dataSet.map(Float.init))
    }
    

    As a note, your concurrentPerform is trying to do an illegal operation in parallel (you cannot modify an array on multiple threads simultaneously, not even if you are modifying different indexes). It turns out not to matter because your synchronization just makes the concurrentPerform serial again. All that code is equivalent to my dataSet.map(Float.init).