6

I am implementing a function called ofType which filters out all the elements of the given type.

Here are my codes:

class Animal {}
class Mammal: Animal {}
class Monkey: Mammal {}
class Pig: Mammal {}
class Human: Mammal {}

extension Array {
    func ofType<T>(_ metatype: T.Type) -> [T] {
        return flatMap { type(of: $0) == metatype ? $0 as? T : nil }
//      return flatMap { $0 as? T } // This is not working as the T is always the static type of the parameter, which is Animal in this example.
//      return flatMap { $0 as? metatype } // This is not working either because of the grammar restriction.
    }
}

let animals = [Monkey(), Pig(), Human(), Mammal(), Animal()]
func animalType() -> Animal.Type {
    return Mammal.self
}
animals.ofType(animalType()).count // returns 1, expect to be 4.

In Objc, I can use isKindOf() to check whether an object is an instance of the class or the subclass. There are similar operations in swift is and as, but the type after them should be a static type, not a dynamic type value (e.g. I can write is Mammal, but not is Mammal.self).

I cannot use the type parameter T either because, in this example, the T equals to Animal, which is not what I want.

Do you have any idea about how to implement this function?

Bing
  • 351
  • 3
  • 12
  • Do you not know at compile time what type to filter out? – Sweeper Jan 31 '18 at 06:54
  • @Sweeper No. The type is only determined at run-time. – Bing Jan 31 '18 at 06:59
  • Then how can the compiler possibly know what `T` is? – Sweeper Jan 31 '18 at 07:04
  • @Sweeper The compiler only know the static type of the parameter, in this example, `T` is `Animal`. – Bing Jan 31 '18 at 07:13
  • Exactly! So what I'm saying is that returning a `[Mammal]` is impossible. – Sweeper Jan 31 '18 at 07:13
  • I don't understand your use case. It would help us to provide you with a better solution if you let us know how and where you're intending to use this. There is probably a better and Swiftier way of achieving the result you are looking for, without going so deep into the swift type system. – Sam Jan 31 '18 at 09:07
  • @MartinR By converting the object to `AnyObject`, I can call the function `isKind(of:)` to check whether the instance is a kind of a dynamic type. – Bing Jan 31 '18 at 15:23
  • @Sam I agree there is probably a better and Swiftier way, but I want to try the direct way first. – Bing Jan 31 '18 at 15:25

4 Answers4

5

This works. Just use as? inside flatMap. If the animal can be cast, it will be returned otherwise nil is returned and flatMap throws it away

class Animal {}
class Mammal: Animal {}
class Monkey: Mammal {}
class Pig: Mammal {}
class Human: Mammal {}

extension Array {
    func ofType<T>() -> [T] 
    {
        return flatMap { $0 as? T }
    }
}

let animals = [Monkey(), Pig(), Human(), Mammal(), Animal()]
let monkeys: [Monkey] = animals.ofType() // A one element array
let mammals: [Mammal] = animals.ofType() // A four element array

If you explicitly type the output array, the compiler can infer T from the context, otherwise you pass T's type as a parameter but don't use it in the function.


If you want to be able to dynamically check the type i.e. you don't know the type to filter at compile time, you can use mirrors as it turns out. Here's a solution which is a bit clunky but it does work:

class Animal
{
    func isInstance(of aType: Any.Type) -> Bool
    {
        var currentMirror: Mirror?  = Mirror(reflecting: self)
        while let theMirror = currentMirror
        {
            if theMirror.subjectType == aType
            {
                return true
            }
            currentMirror = theMirror.superclassMirror
        }
        return false
    }
}
class Mammal: Animal {}
class Monkey: Mammal {}
class Pig: Mammal {}
class Human: Mammal {}


let animals = [Monkey(), Pig(), Human(), Mammal(), Animal()]

for aType in [Animal.self, Mammal.self, Monkey.self]
{
    let result = animals.flatMap { $0.isInstance(of: aType) ? $0 : nil }
    print("\(result)")
}

Prints:

[__lldb_expr_12.Monkey, __lldb_expr_12.Pig, __lldb_expr_12.Human, __lldb_expr_12.Mammal, __lldb_expr_12.Animal]
[__lldb_expr_12.Monkey, __lldb_expr_12.Pig, __lldb_expr_12.Human, __lldb_expr_12.Mammal] 
[__lldb_expr_12.Monkey]

Edit Following Sam's suggestion in the comments, it occurred to me that the above method is best put in a protocol extension.

protocol TypeCheckable {}

extension TypeCheckable  
{
    func isInstance(of aType: Any.Type) -> Bool
    {
        var currentMirror: Mirror?  = Mirror(reflecting: self)
        while let theMirror = currentMirror
        {
            if theMirror.subjectType == aType
            {
                return true
            }
            currentMirror = theMirror.superclassMirror
        }
        return false
    }
}

Then you can add the capability to any Swift type by making it conform to the protocol.

class Animal: TypeCheckable { ... }

extension String: TypeCheckable {}
JeremyP
  • 84,577
  • 15
  • 123
  • 161
  • The answer of @Hamish is quite good. You can see it here: https://gist.github.com/hamishknight/5bf45b025fcde4c2e8f9c67fce9fbe7e – Bing Jan 31 '18 at 15:19
  • I liked your approach. To simplify it, I would move the `isInstance(of aType: Any.Type)` method to a class of its own and make Animal subclass it. – Sam Jan 31 '18 at 17:59
  • @Sam I almost agree with you. In fact, the best approach is to create a protocol and then put the method in a protocol extension. That way, you can add the functionality to any type by using an extension. – JeremyP Feb 01 '18 at 10:15
  • The protocol extension implementation is awesome! – Bing Feb 01 '18 at 10:33
  • @JeremyP I didn't know you could extend protocol. That seems really nice! – Sam Feb 01 '18 at 15:25
3

Personally, I think @JeremyP's suggestion to use Mirror is the best; though I would make a couple of tweaks to it:

/// Conditionally cast `x` to a given dynamic metatype value, taking into consideration
/// class inheritance hierarchies.
func conditionallyCast<T, U>(_ x: T, to destType: U.Type) -> U? {

  if type(of: x) is AnyClass && destType is AnyClass { // class-to-class

    let isCastable = sequence(
      first: Mirror(reflecting: x), next: { $0.superclassMirror }
    )
    .contains { $0.subjectType == destType }

    return isCastable ? (x as! U) : nil
  }

  // otherwise fall back to as?
  return x as? U
}

Here we're using sequence(first:next:) to create a sequence of metatypes from the dynamic type of x through any superclass metatypes it might have (probably the first use of the function I've seen that doesn't look awful :P). In addition, we're falling back to doing an as? cast when we know we're not doing a class-to-class cast, which allows the function to also work with protocol metatypes.

Then you can simply say:

extension Sequence {
  func ofType<T>(_ metatype: T.Type) -> [T] {
    return flatMap { conditionallyCast($0, to: metatype) }
  }
}

protocol P {}
class Animal {}
class Mammal: Animal {}
class Monkey: Mammal, P {}
class Pig: Mammal {}
class Human: Mammal, P {}

let animals = [Monkey(), Pig(), Human(), Mammal(), Animal()]

let animalType: Animal.Type = Mammal.self
print(animals.ofType(animalType)) // [Monkey, Pig, Human, Mammal]

print(animals.ofType(P.self)) // [Monkey, Human]

Another option, assuming you're on an Apple platform (i.e have access to the Objective-C runtime), is to use the the Objective-C metaclass method isSubclass(of:) in order to check if a given metatype is equal, or is a subclass of another:

import Foundation

/// Conditionally cast `x` to a given dynamic metatype value, taking into consideration
/// class inheritance hierarchies.
func conditionallyCast<T, U>(_ x: T, to destType: U.Type) -> U? {

  let sourceType = type(of: x)

  if let sourceType = sourceType as? AnyClass,
     let destType = destType as? AnyClass { // class-to-class

    return sourceType.isSubclass(of: destType) ? (x as! U) : nil
  }

  // otherwise fall back to as?
  return x as? U
}

This works because on Apple platforms, Swift classes are built on top of Obj-C classes – and therefore the metatype of a Swift class is an Obj-C metaclass object.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • I still prefer your second answer, although not as enlightening as the first one. :P – Bing Feb 01 '18 at 10:22
  • 1
    I find `isKind(of:)` is available for `AnyObject`. So this line `let sourceType = type(of: x)` can be omitted and just check `(x as? AnyObject)?.isKind(of: destType)`. – Bing Feb 01 '18 at 10:27
  • @Bing Yup, you could use `isKind(of:)` instead; though bear in mind that a cast to `AnyObject` always succeeds, as non Obj-C compatible things get boxed in an opaque Obj-C compatible box. So to check if `x` really is an instance of a class, you need to say `type(of: x) is AnyClass`, and I found by that point you might as well just use `as?` and `isSubclass(of:)` with the metatypes :) – Hamish Feb 01 '18 at 13:11
2

You might use the reflection to find all the items that are compatible with the metatype, doing so:

class Animal { }
class Mammal: Animal {}
class Monkey: Mammal {}
class Pig: Mammal {}
class Human: Mammal {}

extension Array {
    func ofType<T>(_ metatype: T.Type) -> [T] {
        return flatMap { item in
            var mirror:Mirror? = Mirror(reflecting: item)
            while let currentMirror = mirror {
                mirror = currentMirror.superclassMirror
                if currentMirror.subjectType == metatype {
                    return item as? T
                }
            }
            return nil
        }
    }
}

let animals = [Monkey(), Pig(), Human(), Mammal(), Animal()]
func animalType() -> Animal.Type {
    return Mammal.self
}
let result = animals.ofType(animalType())
print(result) // returns 4 items: Monkey, Pig, Human, Mammal

Alternatively, with the following code I am using the operator is and I am passing directly Mammal.self to the function ofType:

class Animal {}
class Mammal: Animal {}
class Monkey: Mammal {}
class Pig: Mammal {}
class Human: Mammal {}

extension Array {
    func ofType<T>(_ metatype: T.Type) -> [T] {
        return flatMap { $0 is T ? $0 as? T : nil }
    }
}

let animals = [Monkey(), Pig(), Human(), Mammal(), Animal()]
let result = animals.ofType(Mammal.self)
print(result) // returns 4 items: Monkey, Pig, Human, Mammal
mugx
  • 9,869
  • 3
  • 43
  • 55
  • I know this works. I just cannot pass in a literal class type, so the compiler cannot infer the exact type. – Bing Jan 31 '18 at 09:53
  • @AndreaMugnaini Why `String(describing: type(of: item)) != String(describing: T.self)`? I cannot get what you mean... – Bing Jan 31 '18 at 14:26
  • @Bing I found a proper solution – mugx Jan 31 '18 at 21:35
  • 1
    @AndreaMugnaini Some others have also found the similar solution. Thanks for answering the question! – Bing Feb 01 '18 at 10:30
1

The isKindOf() method is also available in Swift, as it is a method of the NSObjectProtocol. So what you really need to do is subclass NSObject for your declaration of Animal.

NOTE: The is kind of method is renamed to isKind(of: Type) in swift.

should be as simple as

class Animal: NSObject {}

Now, all that is left, is to get around the problem that not all arrays will have elements that are a subclass of NSObject or conform to NSObjectProtocol.

To fix that we add a where clause in the declaration of the swift extension.

It should now look like

extension Array where Element: NSObjectProtocol 

Putting it all together, the final code should be similar to

class Animal: NSObject {}
class Mammal: Animal {}
class Monkey: Mammal {}
class Pig: Mammal {}
class Human: Mammal {}

extension Array where Element: NSObjectProtocol {
    func ofType<T: NSObjectProtocol>(_ metatype: T.Type) -> [T] {
        return flatMap { $0.isKind(of: metatype) ? $0 as? T : nil }
    }
}

let animals = [Monkey(), Pig(), Human(), Mammal(), Animal()]
func animalType() -> Animal.Type {

    return Mammal.self
}

print(animals.ofType(animalType()).count)
Sam
  • 649
  • 5
  • 13
  • Thanks for your answer. I really don’t want to add this constraint because it will restrict the function only used on classes implementing NSObjectProtocol. – Bing Jan 31 '18 at 07:21
  • I believe there is no simple way of doing that in Swift, without going back to the code from the Obj-C days. Doing this without subclassing `NSObject` is more effort than its worth. We usually don't subclass `NSObject` in swift. It does not mean that its against the "Swift way", but rather that we don't need to, in most cases. This is one of those rare cases where we would need to subclass `NSObject` and its not a bad thing. – Sam Jan 31 '18 at 07:36
  • ...as the saying goes, "Code That Doesn't Exist Is The Code You Don't Need To Debug." – Sam Jan 31 '18 at 07:37
  • 1
    You can write this to not require inheritance from `NSObject`: https://gist.github.com/hamishknight/5bf45b025fcde4c2e8f9c67fce9fbe7e. Although because its still relying on the Obj-C runtime, it will only work on platforms with Obj-C interop (i.e Apple platforms). – Hamish Jan 31 '18 at 13:33
  • @Sam Haha, I like your philosophy. – Bing Jan 31 '18 at 14:29
  • @Hamish Your resolution is great. That is exactly what I want. Thanks very much! – Bing Jan 31 '18 at 14:54
  • @Hamish Maybe you can post your answer as an Answer, so that I can accept it as the correct one? – Bing Jan 31 '18 at 15:33
  • @Bing Sure, give me a couple of mins :) – Hamish Jan 31 '18 at 15:58
  • @Bing Posted an answer, though I do think Jeremy's approach of using `Mirror` is best as it's more cross-platform than `isSubclass(of:)`. – Hamish Jan 31 '18 at 17:52