UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: How to use generics to simplify overloaded functions

Forums > Swift

I need a little help updating some overloaded funcs such that I can use generic functions instead.

I am writing an application where I produce a grayscale image from some raw data. The raw data (typically terrain data) contains data points of Doubles or Ints, depending on the source dataset that I am using.

I have created some custom "shaders" that convert the raw data value to a grayscale pixel value, but I have to create two overloaded functions for every shader I need.

func shaderLinear(_ v: Double, limits: ClosedRange<Double>, scalar: Int, isInverse: Bool) -> UInt8 {
  return UInt8(normaliseValue(v, limits: limits, isInverse: isInverse) * Double(scalar))
}
func shaderLinear(_ v: Int, limits: ClosedRange<Double>, scalar: Int, isInverse: Bool) -> UInt8 {
  return UInt8(normaliseValue(v, limits: limits, isInverse: isInverse) * Double(scalar))
}

// ... other shaders, code not provided...

func normaliseValue(_ value: Double, limits: ClosedRange<Double>, isInverse: Bool) -> Double {
  let lower = limits.lowerBound
  let upper = limits.upperBound
  let v = min(max(value, lower), upper) - lower
  let span = upper - lower
  return (isInverse) ? 1.0 - (v / span) : v / span
}
func normaliseValue(_ value: Int, limits: ClosedRange<Double>, isInverse: Bool) -> Double {
  return normaliseValue(Double(value), limits: limits, isInverse: isInverse)
}

This works okay but I was hoping to replace the overloaded shader functions with something like ...

func shaderTest<T: Numeric>(_ v: T, limits: ClosedRange<Double>, scalar: Int, isInverse: Bool) -> UInt8 {
  return UInt8(normaliseValue(Double(v), limits: limits, isInverse: isInverse))
}

If this works, I would only need one normaliseValue function and a single generic function for each shader I want to implement. But I get an error Initializer 'init(_:)' requires that 'T' conform to 'BinaryInteger'. So I guess Numeric is not the correct generic to use in my case.

Is my approach simply wrong? Suggestions on a better way to use generics instead of my overloaded functions would be very welcome. Kieran

2      

Double.init(_:) requires that the value conform to BinaryInteger, which Double does not. So if you try to call your shaderTest function with a Double, that's an error.

AFAIK, you have to do something like this:

func shaderTest<T: Numeric>(_ v: T, limits: ClosedRange<Double>, scalar: Int, isInverse: Bool) -> UInt8 {
    let val: Double
    switch v {
    case let i as Int: val = Double(i)
    case let d as Double: val = d
    default: fatalError()
    }
    return UInt8(normaliseValue(val, limits: limits, isInverse: isInverse))
}

2      

@roosterboy, TY! That'll do the trick.

For my implementation, I would need to include the switch block in every shader, so I pulled that out into a separate function. Also added a little future proofing, in case I have to hande Floats.

func dblFromNumeric(_ n: any Numeric) -> Double {
  let val: Double
  switch n {
  case let i as Int: val = Double(i)
  case let f as Float: val = Double(f)
  case let d as Double: val = d
  default: fatalError()
  }
  return val
}

func shaderLinear<T: Numeric>(_ v: T, limits: ClosedRange<Double>, scalar: Int, isInverse: Bool) -> UInt8 {
  let val = dblFromNumeric(v)
  return UInt8(normaliseValue(val, limits: limits, isInverse: isInverse) * Double(scalar))
}

So that makes it pretty easy to update all my shaders (another 10 of them). Thanks again.

2      

Rather than a free function dblFromNumeric you could do it as an extension on Double:

extension Double {
    init<T:Numeric>(_ v: T) {
        let val: Double
        switch v {
        case let i as Int: val = Double(i)
        case let d as Double: val = d
        case let f as Float: val = Double(f)
        default: fatalError()
        }
        self = val
    }
}

And then call it like so:

let val: Double = .init(v)

Heck, you could even lose the intermediary variable:

return UInt8(normaliseValue(.init(v), limits: limits, isInverse: isInverse) * Double(scalar))

Personally, I find this better since I wouldn't have to remember the existence of dblFromNumeric and could just initialize the Double from the given value.

2      

Awesome, I like it. So you fixed my orginal problem and helped me improve my code.

TYVM

2      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.