Cleaner way of writing functions for multiple types

OK, my pitch is to raise the point of finding a cleaner solution to this problem, not necessarily via disjoint types. If everything will eventually work via BinaryFloatingPoint that'd be equally nice, even though the name will bit every beginner (the "Binary" part I mean).

For example in stdlib/simd you see the file starts like:

floating_types = ['Float','Double']

and later on it's being used like:

%for type in floating_types:

Now I know that simd is an extremely complicated system with all the crazy 3 level nested for loops, so it's not really used only for this, but I do find it hypocritical that beginner Swift users are required to understand a type system which requires the graph you linked, while the core stdlib is simply using %for type in floating_types:

What’s being specified twice?

I mean the most reasonable solution, as of today is to specify a function twice, once for Float and once for Double.

1 Like

It depends on what you mean by “everything” working eventually.

There are runtime overhead costs to generics that, if not optimized away, would be unacceptable for SIMD; nor, of course, is it the case that SIMD can be made generic over any binary floating-point type.

It’s true that writing generic versions of algorithms isn’t easy, but in truth if you’re actually trying to create something to work with Float and Double, it’s quite possible that you’re doing something for which performance does matter, and starting with concrete types would be advisable.

1 Like

By everything I meant math functions in the stdlib, not including simd. For example pow.

I have no knowledge about compilers, but why would a generic pow have any runtime penalty? The type is known at compile time, so it can just be compiled with the substituted type, couldn't it?

Your pitch as I understand it, is to suggest that a generic type parameter T can be generic over a disjunction (ORing) of a finite (hopefully) set of (potentially unrelated) types, such as Float and Double. That's called a disjunctive type. It's to be contrasted with a conjunctive type, which is a conjunction (ANDing) of a finite set of potentially unrelated types, which can be spelt (for example) as Hashable & Comparable in Swift.

Both conjunctive and disjunctive types only make sense in a generic context, when specifying the type of a parameter or return value of a function. They do not make sense as types themselves.

For example, to replace the use of GYB for stamping out Float/Double, (U)Int(|8|16|32|64) couldn't be replaced as a hypothetical

struct Int | Int 8 | Int 16 | Int32 | Int64 | UInt | UInt8 | UInt16 | UInt32 | UInt64 { ... }

Because it would be rediculous to specify differing behaviour on a case basis.

func + (lhs: Self, rhs: Self) -> Self {
    // I need to call 1 of 10 different LLVM built-ins, depending on the type of Self.
    // How the hell do I specify that? something like this?
    let t = true._getBuiltinLogicValue()
	
    switch (lhs, rhs) {
    
	case let (l as Int8, r as Int8): 
		return Builtin.uadd_with_overflow_Int8(l._value, r._value, t)
	case let (l as Int16, r as Int16): 
		return Builtin.uadd_with_overflow_Int16(l._value, r._value, t)
	case let (l as Int32, r as Int32): 
		return Builtin.uadd_with_overflow_Int32(l._value, r._value, t)
	case let (l as Int64, r as Int64): 
		return Builtin.uadd_with_overflow_Int64(l._value, r._value, t)
	case let (l as Int, r as Int): 
		return Builtin.uadd_with_overflow_Int64(l._value, r._value, t) //assuming Int = 64 bits
        
	case let (l as UInt8, r as UInt8): 
		return Builtin.uadd_with_overflow_Int8(l._value, r._value, t)
	case let (l as UInt16, r as UInt16): 
		return Builtin.uadd_with_overflow_Int16(l._value, r._value, t)
	case let (l as UInt32, r as UInt32): 
		return Builtin.uadd_with_overflow_Int32(l._value, r._value, t)
	case let (l as UInt64, r as UInt64): 
		return Builtin.uadd_with_overflow_Int64(l._value, r._value, t)
	case let (l as UInt, r as UInt): 
		return Builtin.uadd_with_overflow_Int64(l._value, r._value, t) //assuming UInt = 64 bits

    // Yuck!
    }
}

Thus, even if disjoint types were available (at great complexity and speed costs, to the compiler), they wouldn't be particularly useful for solving the issues GYB addresses.

I'm not saying that GYB should be changed everywhere, but it'd be really nice to have some clean way of allowing users to write functions which work on multiple types.

Maybe the solution is not a generic over disjoint types, but something which allows writing DRY code and gets processed in a preprocessor stage (although I understand Swift doesn't have a classic preprocessor stage).

I renamed my post, as it's not strictly about using disjoint generic types.

As I already showed (in your duplicate post), that's already possible.

The deficiency there isn't a language one, it's a deficiency in the standard library (in that pow is handled heterogeneously by Darwin or Glibc, with no unified interface). NumericAnnex shows that it's possible to make a library to fix that, and I expect it'll be used as a reference to guide the development of the standard library in that direction.

There's no need for a preprossessor for this.

2 Likes

“Just” is not the word. When the type is known at compile time, the method can be specialized for performance. There’s no guarantee of it, though, and you pay for it in compile time. Sometimes severely. I wrote a generic integer division algorithm and broke Swift continuous integration with 15 lines of test code that takes 20 minutes to compile.

3 Likes

I see both of your points. But many performance critical functions already support generics, so there clearly has to be a way to eventually make it work without performance penalty.

Say, 5 years from now, I'd bet that there'll be a nicer way of using pow, without performance penalty.

As has been pointed out before, you can already do this by using your own protocol until the standard library is more mature in this respect. So for example the following will work:

import Darwin

protocol StandardFloatingPoint : BinaryFloatingPoint {
    func toThePowerOf(_ p: Self) -> Self
    // And any other functionality that you need but is currently not
    // generically available would be added here of course ...
}
extension Float : StandardFloatingPoint {
    func toThePowerOf(_ p: Float) -> Float { return powf(self, p) }
}
extension Double : StandardFloatingPoint {
    func toThePowerOf(_ p: Double) -> Double { return pow(self, p) }
}

extension StandardFloatingPoint {
    func removingSrgbGamma() -> Self {
        let c1: Self = 1.0 / 12.92
        let c2: Self = 1.0 / 1.055
        if self <= 0.04045 {
            return self * c1
        } else {
            return ((self + 0.055) * c2).toThePowerOf(2.4)
        }
    }
    func applyingSrgbGamma() -> Self {
        if self <= 0.0031308 {
            return self * 12.92
        } else {
            return 1.055 * self.toThePowerOf(1.0 / 2.4) - 0.055
        }
    }
}

this is essentially an argument against using generics at all.

related discussion,, may be relevant:

It certainly is a caveat against using generics when using concrete types will do. The same saying about premature optimization can fairly be applied here to premature genericization, as you're pessimizing compilation time and/or runtime performance as a trade-off for...what?

Take the original example, for instance. What do you gain by writing such a function to be generic over all FloatingPoint types, depriving yourself and the compiler of opportunities to take advantage of the additional constraints and semantics of more refined protocols or concrete types? How would you even test that your implementation is fully correct? You can't: there are no extant types that conform to FloatingPoint but not BinaryFloatingPoint (Foundation.Decimal, for instance, does not and cannot conform to either protocol--in part because it does not support the representation of infinity).

In summary, the response to the criticism that it's difficult for a beginner to write generic floating-point code in Swift is--and I'm not being flippant about this--that I would not recommend beginners or anyone else to write generic floating-point code in Swift unless there's a specific reason why concrete code can't deliver the intended result. I say this as someone who's written several thousand lines of it. Because many concrete methods are intrinsics, the generic versions can be easily hundreds or thousands of times more slow, and even as some of this can be optimized away at the cost of more compilation time, not all of it can because generic code must not rely on the peculiarities of specific floating-point types.

1 Like

The main reason for writing generic code is to avoid duplicating logic. Duplication means there will be multiple places that you need to change when you want to update or fix an algorithm.

It would be really nice if compiler did a better job of quickly compiling generic code and being able to specialize it successfully so that we would not have such a ridiculous performance gap between concrete and generic code.

Floating-point code.

As @taylorswift has suggested, I think it would be reasonable to add an Angle type with trig functions and pow, etc. to FloatingPoint to make numeric programming easier. These additions would be a solution to many problems like the one that started this thread.

On a related point, and already raised elsewhere, ExpressibleByFloatLiteral should also be renamed so that it is obvious to people that it is only applicable to BinaryFloatingPoint.

This has been pitched before, and the core team has stated that their preference is for these to be addressed in a third party library that can eventually, if it matures and gains traction, potentially become a core library (but not part of the standard library). So I wrote one.

Redesigning numeric literals is a much bigger question than just renaming, and it is tracked by SR-920. There's a lot of design work to be done, and also a lot of unanswered implementation questions. It is true that FloatingPoint cannot refine ExpressibleByFloatLiteral in its current state, but it's by no means settled that this is the intended final design.

(On the other hand, I've myself tried to implement useful generic algorithms using FloatingPoint and found that it doesn't actually provide enough semantics to do much of anything. Even some of the standard library's own methods, which we initially thought could be generic over FloatingPoint, cannot in fact be implemented without BinaryFloatingPoint. If anything, issues like the one presented here and the ones we encountered in the standard library itself actually suggest very strongly that we ought to delete FloatingPoint altogether. If decimal types ever become part of the standard library, DecimalFloatingPoint, BinaryFloatingPoint, and BinaryInteger can each individually refine Numeric.)

2 Likes

big scenario:

i am writing some kind of scientific or geometry tool or library

i originally write the whole thing in Swift’s default Double

now some graphics programmer wants it to be in Float, because that’s the maximum precision GPU’s are built for. Now they have to copy and paste all my source code into their own project and they have to do find and replace throughout the whole source code to swap out Double with Float.

now some scientist wants it to be in some arbitrary-precision superfloat type. Now they have to copy and paste all my source code into their own project, and replace Float with their own FloatingPoint type.

this is literally the same story that motivates all other generics. Floaty types are no exception.

it already does. to get it through the module boundary you can use @_specialize(T == Concrete)

So, I actually wrote such a scientific library. You’ll notice that the final code uses only concrete types. I started using Double everywhere, then moved to generic methods everywhere, then scrapped everything and used Float.

If you’re creating or using one of these libraries, what's a reasonable slowdown for a few values quickly becomes unacceptable for any significant amount of data. Optimized routines such as those in Accelerate simply don’t exist for arbitrary floating-point types, and (as you mention) preserving the option of using GPU acceleration also limits your options greatly.

Within your library, some very carefully written code might be agnostic to Float versus Double, but you don't have to go very far before you find that only one specific floating-point type can be used for this or that nontrivial feature and that the overhead of even the most optimized Float-to-Double conversion kills your performance.

So yes, floating-point types are exceptional in this way. Let me put it this way: knowing what I know now, I would not vend, nor would I use, a mathematical or scientific library that uses generic floating point; it's almost certainly not fit for purpose.

If you have to use @_specialize to make your library work, then you're doing something wrong: it's not a supported language feature and isn't meant for public consumption.

I’ve also written such a module (spherical voronoi tessellation), and I ended up writing it in Float because of lack of generic protocols. However at some point I want it to support statically generated hi-res maps (that you would save to a file and load at runtime, as opposed to computing low-res maps dynamically), which would at the minimum need Double as the base floating type. At the end of the day, we have different precision floating types for a reason, but without generic protocols to abstract over them, having those types is pointless, because you have to settle on “one canonical float format”.