Struct initializer in extension

This is a dumb question but after some googling, l haven't been able to find an answer.

In Vapor there's a struct:

public struct PageMetadata: Codable {
    public let page: Int
    public let per: Int
    public let total: Int
}

I wanted to create one of these from values, so I created an initializer for it:

extension
PageMetadata
{
	init(page inPage: Int, per inPer: Int, total inTotal: Int)
	{
		self.page = inPage
		self.per = inPer
		self.total = inTotal
	}
}

But Swift doesn't like this. for each of the assignments, it says 'let' property 'page' may not be initialized directly; use "self.init(...)" or "self = ..." instead.

This seems like a reasonable thing to want to do, but I'm not sure how to do it.

Every struct type in Swift necessarily has at least one init. If the primary struct declaration does not contain at least one init, then the compiler generates a member-wise init with internal access level that simply takes in the values of all properties of the struct. Even though struct types don't have the notion of designated and convenience initializers, I highly recommend thinking in these terms to make it easier to reason about the initialization logic. For instance, in your case, it looks like your extension-based initializer serves as the "designated" initializer for the struct, so it's a good idea to put it in the struct definition itself. Or, if it isn't, then the best course of action is to simply call the auto-generated member-wise initializer from this one.

I can't define it in the struct itself because that's in a separate package I don't own. I tried to call the auto-generated initializer (it's the first thing I tried), but got:

let pm = PageMetadata(page: 1, per: 1, total: 1)
                     --
Extra arguments at positions #1, #2, #3 in call
Missing argument for parameter 'from' in call

Maybe there's a version mismatch/the PageMetadata type you have in scope is something else? There is an example here of using the same initializer which presumably compiles fine.

Oh, hmm, I don't know. Not clear to me how to find out, either. :slight_smile:

If this struct is in another package, then it’s no wonder that you can’t call it: the member-wise init has internal access level. Your only hope is to find at least one public init on the struct which you can call from your init. If you can’t find one, then this was either an oversight on the library author’s part or it was a deliberate choice to limit the instantiation of the struct.

7 Likes

To make @technogen's example concrete, it's considered source-compatible to add a new property to a public struct (say sortOrder), and your initializer would have to initialize that property. That's why you can't initialize the properties one by one from a separate module, and have to rely on an initializer explicitly declared as API in the original module.

I've filed a diagnostic bug for this: SR-13133 Add special-case diagnostic for initializing a type with only synthesized public initializers.

The compiler should be able to explain the issue and save you a bunch of head-scratching. :slight_smile:

3 Likes

You could create one from JSON :stuck_out_tongue:

1 Like

So, I'm struggling to contrive an example where this kind of restriction would be a good thing. It seems like I should be able to create one of these objects, and in fact I could if I chose to do so with init(from:).

Can anyone give me an example where it would make sense to limit the instantiation of an otherwise public struct?

When the values have important relationships (e.g. that per <= total). If you filled in any values you want, you’d break things left and right and debugging it would be a nightmare.

The author has the ability to make their data members publicly mutable if they want. This particular author has decided not to.

Oh and FWIW, you’re not supposed to just populate your instance with decoded data without validating it. Notice that the initialiser you mentioned can throw.

2 Likes

One technique that I like to use that involves an init-restricted .struct is to design a robust API that involves temporary objects with strict life-time limitations (like some sort of transaction or some temporary accessor to something). Take, for example this API:

public final class DataBase {
    // ...
    public func withTransaction<Success>(call function: @escaping (Transaction) throws -> Success) -> Success {
        // ...
    }
}

In a normal scenario, the Transaction object can be either easily copied (if it is a struct) or retained (if it is a class), which is not what you’d want, given that the transaction has a very specific semantic attached to its life-time (for instance, the same transaction object might be reused for nested transaction-wrapped code, which would coalesce them into a single transaction, which, in turn, would commit the transaction at the end of the outermost call):

func foo() {
    myDataBase.withTransaction { transaction in
        // The `transaction` object was just created (a transaction has begun).
        transaction.changeSomething()
        bar()
    }
}

func bar() {
    myDataBase.withTransaction { transaction in
        // The `transaction` object is the same as the one in `foo`.
        transaction.changeSomething()
    }
}

In this scenario, the transaction either has to be a class object or a struct that wraps a class object. If we simply pass around the class object itself, then foo or bar could accidentally escape the reference to it, thus disrupting the transaction logic and causing the transaction (and all subsequent transactions) to hang indefinitely.

One could argue that it’s a programming error to escape the reference, but it’s easy to accidentally retain it in a closure and it’s incredibly hard to track down the cause of the bug.

So, in order to avoid this problem, I’d make Transaction a struct that stores a weak reference to a class object that actually implements the transactional functionality. I’d then pass around that wrapper struct instead of the object, thus, guaranteeing that the transaction will live exactly as long as we need it to. Any “escaped” references will always be weak, so at the end of the transaction, all references will expire and the worst possible outcome is a force-unwrapping crash, instead of a mysterious hang...

Naturally, you’d make all inits of the struct non-public, so that the only way to get it would be to actually start a trabsaction.

1 Like

Right, both of those arguments make sense, so that's enough for me. In this particular instance I was able to work around the problem by letting the API build it for me (I was mis-using it in my earlier approach that led me to want to create the struct), and it worked out in the end.

I feel like there should be a more explicit way to say "Only I can create instances of this struct" than providing an internal initializer that by its existence suppresses the compiler-generated initializers. But that's a separate conversation.

1 Like

The compiler generated initialisers are internal. This is intentional so framework authors have to intentionally declare a public init to expose to other modules, if they want to.

4 Likes