UInt8 suspected behaviour

I try to play with MIDI, which likes UInt8 type.

I made UInt8 extension for intervals:

public extension UInt8 {
    static var C_ = 0
    static var C_Sharp = 1
    static var D_Flat = 1
    static var D_ = 2
    static var D_Sharp = 3
    // etc. all notes ...
}

and dictionary:

var ScaleNotes: [String : UInt8] {
    ["C" : .C_,
     "C♯": .C_Sharp,
     "D♭": .D_Flat,
     "D" : .D_,
     "D♯": .D_Sharp,
     /// all notes names and intervals
    ]
}

Compiler shouts on me on red background on each line:

Member 'C_' in 'UInt8' produces result of type 'Int', but context expects 'UInt8'

What could be a reason to shout on me?

The default type for integer literals is Int. You need to explicitly specify the type for those properties:

    //...
    static var C_: UInt8 = 0
    //...
5 Likes

If the constants are defined in a separate file where every use of integer literals is meant to be UInt8, then you could put this at the top-level of the file:

private typealias IntegerLiteralType = UInt8

Then all integer literals in the file will default to UInt8 instead of Int.

11 Likes

The static properties on UInt8 should also be let properties because they shouldn't change:

public extension UInt8 {
    static let C_: Self = 0
    static let C_Sharp: Self = 1
    static let D_Flat: Self = 1
    static let D_: Self = 2
    static let D_Sharp: Self = 3
}
4 Likes

I can’t remember the details, but I’m pretty sure at one point it was determined that static computed properties are more efficient than static lets. Something about storage or lazy initialization or some such.

1 Like

i'd recommend an explicit enumeration type:

play(.a3) // ok
play(57) // compilation error

example
enum Note: UInt8, CaseIterable {
    case a3 = 57
    case a3_sharp
    case b3
    // ...
}

// MARK: frequencies
extension Note {
    var frequency: Double {
        switch self {
        case .a3: return 220
        case .a3_sharp: return ...
        case .b3: return ...
        // ...
        }
    }
}

// MARK: names
extension Note {
    var name: String { // *
        switch self {
        case .a3: return "A3"
        case .a3_sharp: return "A#3"
        case .b3: return "B3"
        // ...
        }
    }
}

extension Note {
    init?(name: String) {
        guard let note = (Note.allCases.first { $0.name == name }) else {
            return nil
        }
        self = note
    }
    // instead of ScaleNotes["A3"] call Note("A3")
}

(*) wish swift had an ability to generate enum constant names when raw value is taken for something else. without it you have to generate those names explicitly.

1 Like

static and global let and var properties are already lazily initialized. Why would computed properties be more efficient? The value has to be computed each time they're accessed.

There is also a Swift wrapper around CoreMIDI to make your life a bit easier:

Although Notes and pitches are not defined as constants/enumerations. However, MIDI messages are represented as enums with associated values and it is therefore really easy to read and write all kinds of messages in a type safe way.

Contributions are welcome If you want to add constants/enumerations for notes and pitches :slight_smile:

1 Like

Again, I don’t recall the details, but in general computing is faster than loading memory.

it could be nice, but there is exactly 127 midi notes from 0 to 127. I use names just for any octave, and use some struct Tone with note 0..11 and octave number... UInt8 absoluteNoteNumber % 12 gives me note in octave, absoluteNoteNumber / 12 gives me octave (+2)

It probably does not matter that much in that case. The compiler should inline both. Maybe it is a bit more likely to get inlined if it is a constant.

Edit: to be clear, it should definitely not be a var. it should either be a let or a computed property. Otherwise it always needs to be loaded because it could be modified.

1 Like

Is it better to keep using a dictionary instead of doing linear search with .first(where:)? Probably not matter much especially if number of cases are small. But for large case numbers, it does matter?

// Since enum cannot contain stored properties, so use a global. Is there better way?
let nameToNote: [String: Note] = {
    var result = [String: Note]()
    for note in Note.allCases {
        result[note.name] = note
    }
    return result
}()

extension Note {
    init?(name: String) {
        guard let note = Note.allCases.first(where: { $0.name == name }) else {
            return nil
        }
        self = note
    }
    // instead of ScaleNotes["A3"] call Note("A3")


    init?(viaMap: String) {
        guard let note = nameToNote[viaMap] else {
            return nil
        }
        self = note
    }
}

I think that’s a slightly dangerous type of thinking. :slight_smile:

I regularly study the machine code produced on my platform (Arduino/AVR) by various swift statements. I can tell you that generally static let global constants will be used efficiently. For example what you actually usually want is the value turned into something like an immediate load in assembly and this is what happens in fully compiled and optimised code.

So I think it’s probably rarely true that “computed properties are more efficient than static let”. I can certainly say in my experience of studying the assembly produced on my platform that’s NOT usually true. :slight_smile:

I think the best approach is generally make something that’s semantically correct and give the compiler the best information possible to help it reason and generate efficient code. It often knows best. Happy to chew over assembly language generated that proves me wrong!

3 Likes

I think the behaviours of ints can seem surprising and hard when you first come to swift. I think what it’s fundamentally doing is always forcing you to think about the edge cases and what ifs.

For your example. It might be quite natural to create an enum like...


public enum Note: UInt8 {
    Case C_ = 0
    Case C_Sharp = 1
    Case D_Flat = 1
    Case D_ = 2
    Case D_Sharp = 3
    // etc. all notes ...
}

(Typed on a phone so needs correcting!)

Then you could use it like
let openingNote: Note = .C_Sharp
Anywhere that needs the unsigned integer 8 value can use openingNote.rawValue and the compiler will know exactly how it all works. Further, you can write your own functions like...
func nextNoteUp(note: Note) -> Note? { Note(note.rawValue + 1) } that are strongly typed.

1 Like

Adding the philosophical bit in a separate post as you might not be interested! The reason for swift being apparently “difficult” comes when you think a bit around the edges. You’re probably compiling on a Mac or linux box that has a 64 bit processor? In which case uint8 will likely get passed around in registers etc where it is stored no more efficiently. So it’s ok that the default type is Int. But the API is answering what if questions.

So what if you define all those constants then need another octave? You add low_C’ or middle_C` etc. 15 semitones to an octave. I guess you’ll probably never run out of numbers in 0-255.

But the compiler is allowing for it. Say you coded a constant with 260. When you pass that to the API on some languages it will make a “best guess” how to convert it to 0-255. In swift it forces you to say.

If you use UInt8(note) then it will trap (crash) if you pass a number out of range. Or there’s more complex approaches you can use that avoid crashing. The purpose is that swift forces you to explicitly say what you’re going to do with these cases.

2 Likes

I’m not sure what you’re referring to as a “line of thinking”. I was recounting my recollection of something that had previously been determined as fact, and I stated up front that I could not recall the details.

Having looked through past threads, it appears that there was in fact, as I recalled, an objective and measurable optimization benefit to using computed, rather than stored, static constants.

This thread both documents the problem, and the fact that it was subsequently solved in a newer version of Swift: Static let vs static computed property, optimization differences

This thread talks about the extra work required by the compiler to prove that an expression resolves to a constant, and can thus be optimized away: Static computed property vs static let constant?

Thus, for more complex calculations involving function calls, it is conceivable that the compiler would not be able to prove constancy, and so would need to store the value for a let (and recalculate it each time for a computed var).

It seems that today, in the particular case of literal constants, the Swift compiler is smart enough to optimize away both stored and computed static properties. It did not used to be able to, as the first link shows, and for more complex expressions it may still not be able to, as the second link implies.

nameToNote should be a static property of Note. Dictionaries are generally faster at checking for membership.

1 Like

First, I want to make absolutely clear I'm not criticising anyone. I'm just talking about my thoughts on how to use Swift, basically so that people reading this thread can think "it's OK, I don't have to worry about this"! :slight_smile:

What I mean by "a slightly dangerous type of thinking" is this...

I have worked in a lot of languages over 30 years, swift is just my most recent. Often people try to "game the compiler", i.e. developers say "although they look the same and in theory these two constructs should do exactly the same thing, you should always use this form X not that form Y because the compiler will compile it better".

The problem is, you are trying to second guess an extraordinarily complex software system (all of the 100 or 200 optimiser passes that run on your code at various levels during lowering, SIL transforms, IR transforms, MC emission, etc.)

And it's a moving target. In one specific case, on the first thread, with one specific version of the compiler on one platform, there was a path where two identical looking approaches happened to optimise differently. The fact that the developer was returning a Bool constant true or false would absolutely have been important too... constant transformations would probably apply (to be sure you would have to reproduce the exact behaviour then turn on detailed optimiser debugging to detail exactly what transforms were done on each code path... this would be something like a few weeks work done by a normal person). It could easily be that if they had returned a float, or if they had added two constants together, or returned a string, then the result could have gone the other way... the static let would have optimised better and the computed property would have been slightly slower. Or most likely they would have been the same. This is almost certainly not as simple as "if you use the Swift 5.1.3 compiler then computed vars are faster than static lets in all cases, they fixed the issue later".

In the second thread it's even more cut and dried. The OP admits that the two ways to write the code produce exactly the same SIL, therefore exactly the same IR and the same machine code when optimisations are turned on.

There is a bit of discussion by Jordan about what happens in the complicated case where a static computed property and a static let are exposed from a library. But again, this is a very complex case. How much can be optimised will depend on what's public, whether the library has been built resilient and if the properties are @inlinable, etc. and I'm sure he'd agree that it's not as simple as "computed vars are more efficient".

I would say the general rules for static let vs computed properties (in the context of the OP's question) are simple:

  • using either will produce the correct behaviour
  • if you are not crossing module boundaries, i.e. if the code for the static let or computed var properties are all in the same module (they will usually be for most normal iOS apps you write for example) and if optimisations are turned on (usually most release builds in Xcode will have optimisations turned on) then the two approaches will produce identical code in almost all cases and be equally "efficient"
  • in the (rare) cases where a particular combination of compiler version and code produces a difference in speed, it could just as easily be static let that's faster and not computed var. You are not going to be able to predict it and you probably shouldn't try. A slight change in code or compiler version might reverse the result and make it a slower approach.
  • even if you happen to get it right, the differences are likely to be so small that you'd never notice

So, in summary, the reason I call this type of thinking "slightly dangerous" is because 1) it leads to "cargo cult" programming (see Richard Feynman) where people do things but they don't know really why, 2) it will often produce distractions from simple, understandable code that is readable and makes sense, for no benefit, 3) in some cases it might make it slightly faster or slower but either way it almost certainly won't be reliable and probably won't be worth it.

I hope everyone takes this in the spirit in which it is meant... as reassurance that you don't need to worry and can write what seems like elegant code... and not as a criticism of someone's work!

p.s. Just to give a little context, while I am no expert. I've written swift code since version 1.0, contributed small patches to the compiler and I make my own variant of swift and the standard library for Swift for Arduino. This is how I've seen a lot of the details of optimisations in practise... with many, many hours of studying the AVR machine code output at the end of the process! :slight_smile:

4 Likes
Terms of Service

Privacy Policy

Cookie Policy