How to extend float3 or any other built-in type to conform to the Codable protocol?
Asked Answered
F

4

6

In trying to serialize an array of float3 objects with the basic JSONEncoder, it's revealed that float3 does not conform to the Codable protocol, so this cannot be done.

I tried to write a basic extension as suggested in Encoding and Decoding Custom Types as seen below, but the error 'self' used before all stored properties are initialized is rendered for each of the assignment lines in the init. I assume this is because the compiler is not sure that Float.self is defined prior to the initialization of float3, but I'm not sure how to resolve this.

Further, the end of the init says Return from initializer without initializing all stored properties which I assume means there are float3 properties in addition to x, y, and z, but I'm wondering if there is a way to default/ignore these, and/or how to find the full list of properties aside from digging through all the float3 extensions in simd.

If you have any thoughts about how to go about doing this, sharing them would be much appreciated. Thanks!

import SceneKit

extension float3: Codable {
    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        x = try values.decode(Float.self, forKey: .x)
        y = try values.decode(Float.self, forKey: .y)
        z = try values.decode(Float.self, forKey: .z)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(x, forKey: .x)
        try container.encode(y, forKey: .y)
        try container.encode(z, forKey: .z)
    }

    enum CodingKeys: String, CodingKey {
        case x
        case y
        case z
    }
}

Here is the (I think complete) float3 definitions from the simd file:

/// A vector of three `Float`.  This corresponds to the C and
/// Obj-C type `vector_float3` and the C++ type `simd::float3`.
public struct float3 {

    public var x: Float

    public var y: Float

    public var z: Float

    /// Initialize to the zero vector.
    public init()

    /// Initialize a vector with the specified elements.
    public init(_ x: Float, _ y: Float, _ z: Float)

    /// Initialize a vector with the specified elements.
    public init(x: Float, y: Float, z: Float)

    /// Initialize to a vector with all elements equal to `scalar`.
    public init(_ scalar: Float)

    /// Initialize to a vector with elements taken from `array`.
    ///
    /// - Precondition: `array` must have exactly three elements.
    public init(_ array: [Float])

    /// Access individual elements of the vector via subscript.
    public subscript(index: Int) -> Float
}

extension float3 : Equatable {

    /// True iff every element of lhs is equal to the corresponding element of
    /// rhs.
    public static func ==(lhs: float3, rhs: float3) -> Bool
}

extension float3 : CustomDebugStringConvertible {

    /// Debug string representation
    public var debugDescription: String { get }
}

extension float3 : ExpressibleByArrayLiteral {

    /// Initialize using `arrayLiteral`.
    ///
    /// - Precondition: the array literal must exactly three
    ///   elements.
    public init(arrayLiteral elements: Float...)
}

extension float3 : Collection {

    /// The position of the first element in a nonempty collection.
    ///
    /// If the collection is empty, `startIndex` is equal to `endIndex`.
    public var startIndex: Int { get }

    /// The collection's "past the end" position---that is, the position one
    /// greater than the last valid subscript argument.
    ///
    /// When you need a range that includes the last element of a collection, use
    /// the half-open range operator (`..<`) with `endIndex`. The `..<` operator
    /// creates a range that doesn't include the upper bound, so it's always
    /// safe to use with `endIndex`. For example:
    ///
    ///     let numbers = [10, 20, 30, 40, 50]
    ///     if let index = numbers.index(of: 30) {
    ///         print(numbers[index ..< numbers.endIndex])
    ///     }
    ///     // Prints "[30, 40, 50]"
    ///
    /// If the collection is empty, `endIndex` is equal to `startIndex`.
    public var endIndex: Int { get }

    /// Returns the position immediately after the given index.
    ///
    /// The successor of an index must be well defined. For an index `i` into a
    /// collection `c`, calling `c.index(after: i)` returns the same index every
    /// time.
    ///
    /// - Parameter i: A valid index of the collection. `i` must be less than
    ///   `endIndex`.
    /// - Returns: The index value immediately after `i`.
    public func index(after i: Int) -> Int
}

extension float3 {

    /// Vector (elementwise) sum of `lhs` and `rhs`.
    public static func +(lhs: float3, rhs: float3) -> float3

    /// Vector (elementwise) difference of `lhs` and `rhs`.
    public static func -(lhs: float3, rhs: float3) -> float3

    /// Negation of `rhs`.
    prefix public static func -(rhs: float3) -> float3

    /// Elementwise product of `lhs` and `rhs` (A.k.a. the Hadamard or Schur
    /// vector product).
    public static func *(lhs: float3, rhs: float3) -> float3

    /// Scalar-Vector product.
    public static func *(lhs: Float, rhs: float3) -> float3

    /// Scalar-Vector product.
    public static func *(lhs: float3, rhs: Float) -> float3

    /// Elementwise quotient of `lhs` and `rhs`.
    public static func /(lhs: float3, rhs: float3) -> float3

    /// Divide vector by scalar.
    public static func /(lhs: float3, rhs: Float) -> float3

    /// Add `rhs` to `lhs`.
    public static func +=(lhs: inout float3, rhs: float3)

    /// Subtract `rhs` from `lhs`.
    public static func -=(lhs: inout float3, rhs: float3)

    /// Multiply `lhs` by `rhs` (elementwise).
    public static func *=(lhs: inout float3, rhs: float3)

    /// Divide `lhs` by `rhs` (elementwise).
    public static func /=(lhs: inout float3, rhs: float3)

    /// Scales `lhs` by `rhs`.
    public static func *=(lhs: inout float3, rhs: Float)

    /// Scales `lhs` by `1/rhs`.
    public static func /=(lhs: inout float3, rhs: Float)
}
Folberth answered 29/1, 2018 at 1:45 Comment(6)
Where is your float3 struct declaration? Are those x, y, z the only properties declared in your custom type? Note that it is Swift naming convention to name your classes, structures, enumerations and protocols starting with an uppercase letter CamelCaseShoestring
@LeoDabus I think the float3 struct must be a left-over from obj-c—it's not something I defined, but seems to be defined in the simd file.Folberth
Do you need to import and framework ?Shoestring
Yeh, importing SceneKit to get, sorry will add that to sample.Folberth
try adding self.init() before let values ...Shoestring
omg... thank you! I thought I had tried that but realized I tried to initialize the super class (obv. can't on struct)Folberth
R
11

You can solve the compiler error by instead of trying to directly assign the decoded values to the fields of your type, storing the decoded values in local variables, then calling a designated initializer of float3.

As Rob mentions in his answer, the cause of the issue has to do with x, y and z being computed properties rather than stored ones, so they cannot be directly written during initialization.

extension float3: Codable {
    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let x = try values.decode(Float.self, forKey: .x)
        let y = try values.decode(Float.self, forKey: .y)
        let z = try values.decode(Float.self, forKey: .z)
        self.init(x, y, z)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(x, forKey: .x)
        try container.encode(y, forKey: .y)
        try container.encode(z, forKey: .z)
    }

    private enum CodingKeys: String, CodingKey {
        case x,y,z
    }
}

You can test both encoding and decoding in a Playground using below code:

let vector = float3(3, 2.4, 1)
do {
    let encodedVector = try JSONEncoder().encode(vector)
    let jsonVector = String(data: encodedVector, encoding: .utf8) //"{"x":3,"y":2.4000000953674316,"z":1}"
    let decodedVector = try JSONDecoder().decode(float3.self, from: encodedVector) //float3(3.0, 2.4, 1.0)
} catch {
    print(error)
}

If you prefer more concise code, the init(from decoder:) method can be shortened to:

public init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    try self.init(values.decode(Float.self, forKey: .x), values.decode(Float.self, forKey: .y), values.decode(Float.self, forKey: .z))
}
Rattlebrain answered 29/1, 2018 at 2:8 Comment(5)
You can use a single try before the initializer try self.init(values.decode(Float.self, forKey: .x), values.decode(Float.self, forKey: .y), values.decode(Float.self, forKey: .z))Shoestring
@LeoDabus thanks for the tip, included the shorter version in my answer, but also kept the original, since some people might prefer readability over concise code.Rattlebrain
you are welcome. I would also change the enum to case x, y, z and make it privateShoestring
The solution is good, but this isn't due to designated initializers. Structs don't have designated initializers or convenience initializers. The problem is that x, y, and z are computed properties on float3, not stored properties, so you can't write them during init.Esmerolda
@RobNapier you're completely right, I missed the fact that float3 is actually a struct, so your explanation is the correct one indeed.Rattlebrain
E
5

Dávid is correct on how to fix the problem, but it's worth understanding why it's a problem (it's not actually designated vs convenience initializers; that only applies to classes).

If we created our own version of float3, your code would work fine with an extension:

struct float3 {
    var x: Float
    var y: Float
    var z: Float
}

extension float3: Codable {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        x = try values.decode(Float.self, forKey: .x)
        y = try values.decode(Float.self, forKey: .y)
        z = try values.decode(Float.self, forKey: .z)
    }
    // ...
}

So that seems strange. Why doesn't our implementation of the simd type work the same as the simd type? Because simd doesn't have an x stored property (or y or z). Those are all computed properties.

The real interface is defined in simd.swift.gyb. If you study that code, you'll see all the SIMD vector types use a generic _vector storage:

public var _vector: Builtin.${llvm_vectype}

And then there are computed properties defined for each letter (component is ['x','y','z','w']):

% for i in xrange(count):
  public var ${component[i]} : ${scalar} {
    @_transparent
    get {
      let elt = Builtin.${extractelement}(_vector,
        (${i} as Int32)._value)

      return ${scalar}(_bits: elt)
    }
    @_transparent
    set {
      _vector = Builtin.${insertelement}(_vector,
        newValue._value,
        (${i} as Int32)._value)
    }
  }
% end

So if we were building our own float3 (without fancy builtins), it'd be something like this:

struct float3 {
    private var vector: [Float]
    var x: Float { get { return vector[0] } set { vector[0] = newValue } }
    var y: Float { get { return vector[1] } set { vector[1] = newValue } }
    var z: Float { get { return vector[2] } set { vector[2] = newValue } }
    init(x: Float, y: Float, z: Float) {
        vector = [x, y, z]
    }
}

And if you write your extension against that, you'll get the same error.

Esmerolda answered 29/1, 2018 at 2:55 Comment(0)
S
3

I think that it is worth mentioning that you can simply use an unkeyed container to encode/decode your float3 properties as an array:

extension float3: Codable {
    public init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        try self.init(container.decode([Float].self))
    }
    public func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try container.encode([x,y,z])
    }
}
Shoestring answered 29/1, 2018 at 3:52 Comment(0)
W
0

to anyone coming here to make their SIMD vectors Codable, it looks like swift already supports this out of the box now

Wire answered 8/7, 2024 at 4:20 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.