Any news on reflection in Swift 2.0?

Some much boilerplate to be removed if only swift 2.0 has reflection!

Answered by Dave in 7273022

I don't know enough about MirrorType's capabilities to say if it's completely the same, but the parts I want still don't work (at least not in the playground, running Xcode 7.0 beta (7A120f) on 10.10.3):

import Cocoa
let str = "Hello, playground"
let strReflect = reflect(str)
let works = str.dynamicType()//creates a new string
let doesNotWork1 = strReflect.value.dynamicType()// 'Any' cannot be constructed...
let doesNotWork2 = strReflect.valueType() // same


If there's something in particular you want tested, I'll be happy to try it and let you know what happens.

Accepted Answer

I don't know enough about MirrorType's capabilities to say if it's completely the same, but the parts I want still don't work (at least not in the playground, running Xcode 7.0 beta (7A120f) on 10.10.3):

import Cocoa
let str = "Hello, playground"
let strReflect = reflect(str)
let works = str.dynamicType()//creates a new string
let doesNotWork1 = strReflect.value.dynamicType()// 'Any' cannot be constructed...
let doesNotWork2 = strReflect.valueType() // same


If there's something in particular you want tested, I'll be happy to try it and let you know what happens.

Thanks Dave. In particular I wanted to know if enums with associated values (i.e. union types) have reflection support.


I use SwiftyStateMachine and my code base is filled with the type of boilerplate shown below. Enumerating enum case labels and getting textual representation of the label names is time consuming, error prone and increases code size considerably. I'd obviously like full reflection capabilities like .NET or Java, with full assembly metadata... looks like we'll have to keep banging out the boilerplate. Roll on Swift 3.0!


// MARK: StoryBookState DOTLabelable extension
extension StoryBookState: DOTLabelable {
    public static var DOTLabelableItems: [StoryBookState] {
        let items: [StoryBookState] = [.Welcome(nil), .Praise(nil, nil), .InvitationToRepeat(nil, nil), .TurnToNextPage(nil), .End]

        // Trick: switch on all cases and get an error if you miss any.
        // Copy and paste the following cases to the array above.
        for item in items {
            switch item {
            case .Welcome, .Praise, .InvitationToRepeat, .TurnToNextPage, .End: break
            }
        }
   
        return items
    }

    public var DOTLabel: String {
        switch self {
        case .Welcome: return "Welcome"
        case .Praise: return "Praise"
        case .InvitationToRepeat: return "InvitationToRepeat"
        case .TurnToNextPage: return "TurnToNextPage"
        case .End: return "End"
        }
    }
}

They showed simple enum type names printing properly during the What's New In Swift session that can be streamed from WWDC.


In Swift 2 / Xcode 7 beta, it seems that enums imported from ObjC still have issues, and only simple Swift enum types are printable.


let strX = "this is \(NSRoundingMode.RoundPlain)"  // "this is C.NSRoundingMode"


enum TestBasicEnum
{
    case Option1
    case Option2
}
let strY = "this is \(TestBasicEnum.Option1)"  // "this is TestEnum.Option1"


enum TestRawEnum: String
{
    case Option1 = "text1"
    case Option2 = "text2"
}
let strW = "this is \(TestRawEnum.Option1)"  // "this is TestEnum.Option1"


enum TestRichEnum
{
    case Option1 (String)
    case Option2 (String)
}
let opt = TestRichEnum.Option1("text")
let strZ = "this is \(opt)"  // "this is TestRichEnum"

Strange


enum MyEnum
{
  case SomeValue( associatedValue: Int )
}
print( "\(MyEnum.SomeValue(associatedValue: 42) )" )


Outputs: "MyEnum.SomeValue(42)"


So it does support associated types, too.

enum MyEnum
{
    case SomeValue( associatedValue: Int )
}
print( "\(MyEnum.SomeValue(associatedValue: 42))" ) // "MyEnum.SomeValue(42)"


enum MyEnum2
{
    case SomeValue( associatedValue: Int )
    case AnotherValue( associatedValue: Int )
}
print( "\(MyEnum2.SomeValue(associatedValue: 42))" ) // "MyEnum2"


enum MyEnum3
{
    case SomeValue( associatedValue: String )
}
let x = "text"
print( "\(MyEnum3.SomeValue(associatedValue: x))" ) // "MyEnum.SomeValue("text")"


Apparently, it only supports enums with associated types if they just have a single case.

Sorry, meant to come back to this thread days ago...


As others have noted, Swift 2 doesn't seem to support what you want it to do. I see a workaround, though... There's still boilerplate code, but it seems less error-prone to me


First, we need a way to iterate over the enum cases (and provide their default values):

class StoryBookStateGenerator : AnyGenerator<StoryBookState> {
    var optcase:StoryBookState? = nil
    override func next() -> Generator.Element? {
        if let cas = optcase {
            switch cas {
            case .Welcome: optcase = .Praise(nil, nil)
            case .Praise: optcase = .InvitationToRepeat(nil, nil)
            case .InvitationToRepeat: optcase = .TurnToNextPage(nil)
            case .TurnToNextPage: optcase = .End
            case .End: optcase = nil
            }
        } else {
            optcase = .Welcome(nil)
        }
        return optcase
    }
}


Second, we need a way to parse the source code and extract the case labels. For some reason, I couldn't get String(contentsOfFile) to work. Fortunately, someone on stackoverflow was kind enough to post a class that'll read each line of a file into a [String] (thanks, Martin R!). I'm not sure I should post somebody else's code, even though it's publicly posted, but it's the first answer here: http://stackoverflow.com/questions/24581517/read-a-file-url-line-by-line-in-swift


By using Martin R's StreamReader, and making a few assumptions about code structure, writing a simple parser is fairly easy:

class SimpleEnumParser {
    let labels: [String]

    init(file:String = __FILE__, line:Int = __LINE__) {
        if let file = StreamReader(path: file) {
            defer { file.close() }
            let lineTextArr = Array(file)
            var caseDeclStartLine:Int = 0
            var caseDeclEndLine:Int = 0
            var foundStartOfCaseDecl = false

            func checkLine(line:String) -> Bool {
                let trimmed = line.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
                return !(trimmed == "" || trimmed.rangeOfString("//")?.startIndex == trimmed.startIndex)
            }

            parseLoop: for lineNum in line ..< lineTextArr.count {
                let lineText = lineTextArr[lineNum]
                switch foundStartOfCaseDecl {
                case false:
                    if let _ = lineText.rangeOfString("case") {
                        caseDeclStartLine = lineNum
                        foundStartOfCaseDecl = true
                    }
                case true:
                    if let _ = lineText.rangeOfString("case") {} else {
                        if checkLine(lineText) {
                            caseDeclEndLine = lineNum - 1
                            break parseLoop
                        }
                    }
                }
            }
            labels = Array(lineTextArr[caseDeclStartLine...caseDeclEndLine])
                .map {$0.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())}
                .filter {checkLine($0)}
                .map {$0.componentsSeparatedByCharactersInSet(NSCharacterSet.alphanumericCharacterSet().invertedSet)[1]}
        } else {
            labels = []
        }
    }
}


And this is how you'd use them to, um, "refactor" your boilerplate:

public enum StoryBookState {
    // This is why Generators are good
    public static let DOTLabelableItems = Array(StoryBookStateGenerator())

    /******************************************
    * Make sure that the SimpleEnumParser is   *
    * instantiated IMMEDIATELY (except for     *
    * newlines & comments) before you list all *
    * your cases, otherwise it won't work.     *
    ********************************************/
    static let parser = SimpleEnumParser()
    // It'll start parsing the lines when it finds one that contains "case"
    case Welcome(String?)// Comments can go here...
    // ... or here. And they're discarded, so you can say things like "case case case case" without messing anything up
    case Praise(String?, String?)// Whitespace-only lines are ok, too, if you want to deliminate groups of cases:

    case InvitationToRepeat(String?, String?)
    case TurnToNextPage(String?)
    case End

    // It'll stop parsing when it gets to anything that doesn't start with "case" or "//"
    func cases() {} // <- starts with "func"

    /******************************************
    * This can go anywhere EXCEPT between the  *
    * SimpleEnumParser instantiation and cases *
    ********************************************/
    var fauxHashValue:Int {
        switch self {
        case .Welcome:              return 0
        case .Praise:               return 1
        case .InvitationToRepeat:   return 2
        case .TurnToNextPage:       return 3
        case .End:                  return 4
        }
    }

    public var DOTLabel:String {
        return StoryBookState.parser.labels[self.fauxHashValue]
    }
}


It seems like their ought to be a way to programmatically generate fauxHashValue, but I can't think of one that doesn't involve custom build rules.


Speaking of which, if you can figure out how to call a custom preprocessor, you could probably automate the writing of StoryBookStateGenerator pretty easily, since it was copy/pasted from the output of this:

func generatorWriter(name:String, cases:[(label:String, defaultValue:String?)]) -> String {
    guard cases.count > 0 else { return "" }
    var text = ""
    text += "class \(name)Generator : AnyGenerator<\(name)> {\n"
    text += "\tvar optcase:\(name)? = nil\n"
    text += "\toverride func next() -> Generator.Element? {\n"
    text += "\t\tif let cas = optcase {\n"
    text += "\t\t\tswitch cas {\n"
    for i in 0 ..< (cases.count - 1) {
        text += "\t\t\tcase .\(cases[i].label): optcase = .\(cases[i+1].label)"
        if let value = cases[i+1].defaultValue {
            text += "(\(value))\n"
        } else {
            text += "\n"
        }
    }
    text += "\t\t\tcase .\(cases.last!.label): optcase = nil\n"
    text += "\t\t\t}\n"
    text += "\t\t\t} else {\n"
    text += "\t\t\t\toptcase = .\(cases.first!.label)"
    if let value = cases.first!.defaultValue {
        text += "(\(value))\n"
    } else {
        text += "\n"
    }
    text += "\t\t\t}\n"
    text += "\t\treturn optcase\n"
    text += "\t}\n"
    text += "}\n"
    return text
}

let labels = ["Welcome", "Praise", "InvitationToRepeat", "TurnToNextPage", "End"]
let defaultValues:[String?] = ["nil", "nil, nil", "nil, nil", "nil", nil]
print(generatorWriter("StoryBookState", cases:Array(zip(labels, defaultValues))))


Anyway, I hope that helps! :-)

Thanks Dave. I'm just sorry I didn't see this reply earlier.


Very interesting!

love the generatorWriter

Any news on reflection in Swift 2.0?
 
 
Q