adazacom
adazacom

Reputation: 453

How can several related struct types share an init(...) method without defining read-only properties with var?

I find myself very often forced to use var properties in Swift even when properties will only be assigned once.

Here's an example: The only way I have found to have several types share an init(...) is to put the init(...) in a protocol extension. However if I do this, the properties of the struct must be assigned a dummy value before the body of the init(...) in the protocol extension is run, when it will get its "real" value.

The example below runs without error. How can color be a let property but still be assigned in Piece's init(...)?

protocol Piece {
    var color: Character {set get}   // "w" or "b"
    init()
}

extension Piece {
    init(color c: Character) {
        self.init()
        color = c
        // Some complex logic here all Pieces share
    }
}

struct Knight: Piece {
    var color: Character = "_" // annoying dummy value
    internal init(){}
}

// ... the other Piece types here ...

let knight = Knight(color: "w")

To make this clearer, hopefully, this is what I would like instead: (This does not compile, because of let color in struct Knight.)

protocol Piece {
    let color: Character {get}   // "w" or "b"
}

extension Piece {
    init(color c: Character) {
        color = c
        // Some complex logic here all Pieces share
    }
}

struct Knight: Piece {
    let color: Character
}

// ... the other Piece types here ...

let knight = Knight(color: "w")

Edit (after I have found an answer, see below): Another way to state the the subject line question: How can several struct types share initialization logic, while allowing read-only properties to be let?

2nd Edit Made clear that second code example doesn't compile.

3rd Edit Made clear which let color.

Upvotes: 2

Views: 844

Answers (4)

Shadowrun
Shadowrun

Reputation: 3857

Out of interest a completely alternative approach:

First we define a curry function - this one taken from https://github.com/pointfreeco/swift-prelude:

public func curry<A, B, C>(_ function: @escaping (A, B) -> C)
  -> (A)
  -> (B)
  -> C {
    return { (a: A) -> (B) -> C in
      { (b: B) -> C in
        function(a, b)
      }
    }
}

Now let's suppose we have a piece that has a Role, which is the type of piece. Role can change because you can promote pawns. Here we'll use String for Role:

struct Piece {
    let color: Character
    var role: String
}

To share the init that we want white pieces and black pieces, we curry the init function:

let curryPieceInit = curry(Piece.init(color:role:))

and make two partially-applied init functions that bake in either w or b for the color:

let white = curryPieceInit("w")
let black = curryPieceInit("b")

And now we can finish the partial application by passing along the remaining argument to fully instantiate a chess piece:

let wBish = white("bishop")
let wQueen = white("queen")
let blackPawns = (1...8).map { black("pawn\($0)") }

Now make Role not a string but some custom type, an enum, encapsulating the logic representing the differences between the various chess pieces.

Make the Piece.init(color:role:) private, expose the curried versions.

No protocols needed.

Upvotes: 0

Soumya Mahunt
Soumya Mahunt

Reputation: 2782

With some refactoring, you can achieve minimum code duplication. Here is my solution thinking about your scenario with a different perspective:

  1. First, I noticed that you are taking only "w" or "b" as your color property value. Since you have only two (or let's say minimal) variations of inputs you can make color part of type definition itself by using protocol with associated types and generics, rather than having it as a property. By doing this you don't have to worry about setting property during initialization.

    You can create a protocol i.e. PieceColor and create a new type for each color, i.e. Black, White and your Piece protocol can have an associated type confirming to PieceColor:

    protocol PieceColor {
        // color related common properties
        // and methods
    }
    
    enum Black: PieceColor { // or can be struct
        // color specific implementations
    }
    
    enum White: PieceColor { // or can be struct
        // color specific implementations
    }
    
    protocol Piece {
        associatedtype Color: PieceColor
    }
    

    This approach also provides safety guarantees as you are now restricting user input to only values your code is designed to handle, instead of adding additional validation to user inputs. Also, this helps you implementing specific relations between pieces depending on their color group, i.e. only opposite colors can kill each other etc.

  2. Now for the main part of your question, instead of trying to customize initializer you can create a static method that initializes your piece and does some shared complex handling on it:

    protocol Piece {
        associatedtype Color: PieceColor
        init()
        static func customInit() -> Self
    }
    
    extension Piece {
        static func customInit() -> Self {
            let piece = Self()
            // Some complex logic here all Pieces share
            return piece
        }
    }
    
    struct Knight<Color: PieceColor>: Piece {
        // piece specific implementation
    }
    
    let wKnight = Knight<White>.customInit()
    let bKnight = Knight<Black>.customInit()
    

Upvotes: 1

adazacom
adazacom

Reputation: 453

After discovering Charles Srstka's answer to How to use protocols for stucts to emulate classes inheritance. I have built a solution. It isn't the prettiest but it does allow several struct types to share initialization logic while allowing properties that should be read-only, to be defined with let.

This works:

typealias PropertyValues = (Character) // could be more than one

protocol Piece {
    var color: Character {get}   // "w" or "b"
}

extension Piece {
    static func prePropSetup(color c: Character) -> PropertyValues {
        // logic all Pieces share, that needs to run
        //   BEFORE property assignment, goes here
        return (c)
    }
    func postPropSetup(){
        // logic all Pieces share, that needs to run
        //   AFTER property assignment, goes here
        print("Example of property read access: \(color)")
    }
}

struct Knight: Piece {
    let color: Character
    init(color c: Character){
        (color) = Self.prePropSetup(color: c)
        postPropSetup()
    }
}

struct Bishop: Piece {
    let color: Character
    init(color c: Character){
        (color) = Self.prePropSetup(color: c)
        postPropSetup()
    }
}

// ... the other Piece types continue here ...

let knight = Knight(color: "w")
let bishop = Bishop(color: "b")

Upvotes: 0

Rob C
Rob C

Reputation: 5073

Get-only protocol properties are cool because conforming types have a lot of flexibility in how they define the corresponding properties. All that's required is that the property is gettable.

So if you define your protocol with a get-only property:

protocol Piece {
    var color: Character { get }
}

The conforming types can define the color property as a stored variable, a let constant, or a computed property.

Stored variable:

struct Queen: Piece {
    var color: Character
}

Computed property:

struct King: Piece {
    var color: Character { return "K" }
}

Let constant:

struct Knight: Piece {
    let color: Character
}

Each of the above satisfies the gettable property requirement imposed by the protocol.

Regarding initialization. Recall that swift automatically creates default initializers for structs, and those initializers have parameters that correspond to each of the struct's stored properties. Swift may create an initializer for Queen and Knight that looks like this:

init(color: Character) {
    self.color = color
}

So if you want color to be a let constant and you want to be able to configure it with an initializer, the above definitions for Piece and Knight are sufficient. You don't need to do any additional work.

You can instantiate a Knight like so:

let knight = Knight(color: "w")

Upvotes: 0

Related Questions