3

We have a case where we're being handed an object of type Array<Any> which we need to convert to an Array<Codable>. If any of the items in the original array don't adhere to Codable, then we want the entire thing to abort and return nil.

Or current approach is to manually loop over everything, testing along the way, like so...

func makeCodable(sourceArray:Array<Any>) -> Array<Codable>?{

    var codableArray = Array<Codable>()

    for item in sourceArray{

        guard let codableItem = item as? Codable else {
            return nil
        }

        codableArray.append(codableItem)
    }

    return codableArray
}

However, I'm wondering if there's an easier way to do this with the map command, but it would require it to short-circuit if any of the elements can't be mapped. That's what I'm not sure or not is possible.

For instance, this pseudo-code...

func makeCodable(sourceArray:Array<Any>) -> Array<Codable>?{

    return sourceArray.map({ $0 as? Codable});
}

Is this possible, or is our original way the correct/only way?

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • Certainly the `for` approach is most efficient. It only iterates at most one time and it fails as soon as it can. Good old loops are not a bad thing just because Swift containers offer other convenience methods. – rmaddy Dec 09 '17 at 01:27
  • You can also check if your array contains an object not Codable, return nil if true or map the elements. try`return sourceArray.contains(where: {!($0 is Codable)}) ? nil : sourceArray.map { $0 as? Codable}` – Leo Dabus Dec 09 '17 at 01:32
  • @LeoDabus This is where my efficiency comment comes in. Any other approach requires up to two iterations of the array. – rmaddy Dec 09 '17 at 01:38
  • But it doesn't map the elements and it breaks it at the first non Codable element if it exists – Leo Dabus Dec 09 '17 at 01:39
  • But doesn't it? It has to check if it's mappable, then check a second time when actually doing the map. Two iterations over the same loop doing the same check twice. I'm starting to think I should just create my own `mapCompletely` extension to do exactly this kind of thing. – Mark A. Donohoe Dec 09 '17 at 01:45
  • @LeoDabus Right but the `contains` will iterate the loop. If everything is codable, it then does a 2nd loop in the `map`. – rmaddy Dec 09 '17 at 01:46
  • The problem is we have a 'core' team here who won't give us access to the common source code, and they insist on defining all of their APIs with type 'Any', especially when handling JSON responses from micro services. We get Array or [String:Any] which is painful. We'd rather just have the raw JSON so we can use the JSONEncoder/Decoders in Swift4. So... I'm writing a generic function to take their 'Any'-based objects, convert them to Codable, then run them through the JSONEncoder, then back through the JSONDecoder, but with the correct types. (continued...) – Mark A. Donohoe Dec 09 '17 at 02:05
  • That way we don't have to manually write the encoders/decoders as we do now, which is just tedious/wasteful. Yes, this means we're technically going from JSON -> Array -> JSON -> Concrete type (and similar for dictionaries), but if we ever win the battle and just have them give us back the raw JSON, we can eliminate the middle two steps and we can delete the code we're talking about here. It's just a battle between us and a 'core' team who doesn't know how to work with their clients, or write good APIs. – Mark A. Donohoe Dec 09 '17 at 02:07

2 Answers2

3

Here's one solution using map and throws.

func makeCodable(sourceArray: [Any]) -> [Codable]? {
    enum CodableError: Error {
        case notCodable
    }

    let res: [Codable]? = try? sourceArray.map {
        guard let codable = $0 as? Codable else {
            throw CodableError.notCodable
        }

        return codable
    }

    return res
}

let res = makeCodable2(sourceArray: [5, 6.5, "Hi", UIView()])
print(res) // nil

Here's a variation that makes makeCodable throw and return a non-optional array:

enum CodableError: Error {
    case notCodable
}

func makeCodable(sourceArray: [Any]) throws -> [Codable] {
    let res: [Codable] = try sourceArray.map {
        guard let cod = $0 as? Codable else {
            throw CodableError.notCodable
        }

        return cod
    }

    return res
}

do {
    let res = try makeCodable(sourceArray: [5, 6.5, "Hi"])
    print(res) // prints array
    let bad = try makeCodable(sourceArray: [5, 6.5, "Hi", UIView()])
    print(bad)
} catch {
    print(error) // goes here on 2nd call
}
rmaddy
  • 314,917
  • 42
  • 532
  • 579
0

As @rmaddy shows, you can utilise the fact that map(_:) can accept a throwing closure, and will stop mapping upon an error being thrown, propagating the error thrown back to the caller (which you can then absorb with try?).

One slight variation on this would be to define your own throwing cast(_:to:) function to call in the transformation closure:

struct TypeMismatchError : Error {
  var expected: Any.Type
  var actual: Any.Type
}

func cast<T, U>(_ x: T, to _: U.Type) throws -> U {
  guard let casted = x as? U else {
    throw TypeMismatchError(expected: U.self, actual: type(of: x))
  }
  return casted
}

func makeCodable(sourceArray: [Any]) -> [Codable]? {
  return try? sourceArray.map { try cast($0, to: Codable.self) }
}

Although we're completely ignoring the error thrown in this case, I have found it occasionally useful in other cases to have a throwing casting function about (you could of course also propagate the error by making makeCodable a throwing function and using try).

However, that all being said, note that your resulting [Codable]? really isn't too useful in its current form. You can't decode stuff from it because you don't have any concrete types to hand, and you can't directly encode it as protocols don't conform to themselves (i.e Codable doesn't conform to Encodable, so you can't just hand off a Codable or [Codable] to JSONEncoder).

If you actually wanted to do some encoding with your [Codable], you'd need to wrap each of the elements in a Encodable conforming wrapper, for example:

struct AnyEncodable : Encodable {

  var base: Encodable

  init(_ base: Encodable) {
    self.base = base
  }

  func encode(to encoder: Encoder) throws {
    try base.encode(to: encoder)
  }
}

func makeEncodable(sourceArray: [Any]) -> [AnyEncodable]? {
  return try? sourceArray.map {
    AnyEncodable(try cast($0, to: Encodable.self))
  }
}

Now [AnyEncodable] is something you can pass off to, for example, JSONEncoder.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • Pasting your code in a playground, I'm getting 'use of unresolved identifier 'cast''. Have you tried this yourself? (I'm using Xcode 9.1's playground feature) – Mark A. Donohoe Dec 09 '17 at 17:54
  • @MarqueIV Compiles fine for me in Xcode 9.2. The second example uses the `cast(_:to:)` function from the first example though, sounds like you tried to compile the second example without copying that over. – Hamish Dec 09 '17 at 18:45
  • Oops! You're right. The 'cast' is your function, not the library's. That's on me. – Mark A. Donohoe Dec 09 '17 at 18:53
  • Question though... why specify 'T' in that function? You don't use it anywhere. Couldn't you instead have just done cast and made X of type Any? – Mark A. Donohoe Dec 09 '17 at 18:55
  • @MarqueIV There's no real difference; `Any` would indeed also be perfectly acceptable. Generic functions can benefit from specialised implementations as an optimisation though, whereas an `Any` parameter cannot. But that probably shouldn't be factor here anyway, as the function will probably just get inlined. – Hamish Dec 09 '17 at 19:06