読者です 読者をやめる 読者になる 読者になる

swift解体新書 その2 Enum RawValue

Hack XCode Cocoa swift2

Swift.org - Documentationを読んでて、swiftEnumJavaなみに変態仕様になっていたのはチェック済みだったんだけど、いろいろ隠れ仕様があるみたいなのでそのメモ。

 

まずは手始めにこんなのを書いてみた

 

気になっていたのが、The Swift Programming Language (Swift 2.2): Enumerationsの次の記述。

Implicitly Assigned Raw Values

When you’re working with enumerations that store integer or string raw values, you don’t have to explicitly assign a raw value for each case. When you don’t, Swift will automatically assign the values for you.

 

 つまり、明確な型を指定しなくても、swiftcで良きに計らってなにかしらのサインとして、型付けするよと。

これの何が嬉しいかというと、Planet_whoAreYouみたいにrawValueほったらかしていても、switch-case文書くときには何も気にしなくていい。

とにかく.MercuryはPlanet_whoAreYouのMercuryというサインでしかないと。

これは、全てをオブジェクトで表現しようというOOP上正しい。

 

そもそも、K&R時代のenum値でアーキテクチャのWORD型を割り当てていたのは、Cという抽象表現から機械語に翻訳する上でそうするほか無かったからだという解釈もできるわけで。

本当なら、コーダーはenum値が具体的にIntで1だろうが、Stringで"Mercury"だろうがそんなな気にせず、Planet_whoAreYou.Mercuryという抽象的なモノとして扱えと。

こうしておくことで、具体値が何かの都合で突如IntになろうがStringになろうがDoubleになろうが.Mercuryは.Mercuryという概念でしかなくなり、概念上のシステムバウンダリから向こう側はそっから先で勝手に考えてくれれば、それで必十条件を満たすので、みんな幸せということにできる。

とはいえ具体値は気になるということで、それを確認しているのが各for内の

    print(x)

    // -> Earth

    let strA = String(x)

    print(strA)

    // -> Mercury

    print("\(x) = \(x.rawValue)")

    // -> Jupiter = 5

 

のあたり。どうやら2.2時点ではrawValue関係なく、Stringで内部に持っているらしい。正確には、String型でインスタンスを生成できるといったところか。

 

ただし、Planet_...で次のようにはできない。

swtch x {

    case "Mercury" : .....

"型と値はちっげーよ、勘違いすんなカス"と怒られる。

 

あと、

let y = x as! String

とかで強制キャストすると、Warningで"それ...やってもいいけどダメだと思うよ。その型xと関係ねーし。わけわからん!"といわれる。

で、実際実行してみるとInstructionエラー。機械語に展開したときに変なオペコードでも吐いてるのか?これはx86だからかも知れない。ARMでやったらnilスルーするのかも。 

 

でも、

swtch String(x) {

    case "Mercury" : .....

はPlanet_...全てで通るし、想定通りに実行される。

 

Stringの生成に関して、ニーモニック上は手前で実装されたmethodの呼び出しに続いて、"__...NSString_class"なんていかにもバックコンパチな外部シンボルを呼んでいるにもかかわらず、"...fixStringForCoreDataP11objc_object"なんて、名前を見る限り不穏なローカルルーチンに何度も何度もオフセットかけて正気の沙汰とは思えないくらいぐっるぐる飛び倒す。これ...CStringの残党引っ張りだすためなんじゃね?とすら思える。

ちなみにこの解析アプローチは、レジスタの書き換えを肉眼では追い切れず、もはや人間の読むフローではないので、あきらめた。

 

 

じゃぁということで、ひとつ疑問が生じる。"enum Planet_String: String..."って結局内部保持してるから二重に宣言するだけ無駄じゃね?Stringじゃないほうがお得かもって。。。

なんとなくここから次のソリューションをひらめく人もいるかもしれない。

例えば、 rawValueはIntだろうが、DoubleだろうがswitchのネタをString()インスタンスにしてしまえば、IntとString両方の値が持てるのでは?

つまり、caseラベルは"Mercury"などenum値を文字列表現したもの、rawValueはInt型だとすると、

func next_prev(x:Planet_Int, isNext:Bool) -> Planet_Int! {

    switch String(x) {

    case "Jupiter":

        if isNext {

            return Planet_Int(rawValue: (x.rawValue + 1))

        } else {

            return Planet_Int(rawValue: (x.rawValue-1))

        }

    // something else ....

    default:

        return Planet_Int(rawValue: 1)

    }

}

なんて、このコードは例としては良くないが、要はStringでcase分岐かけつつ、Int値演算で次or前のenum値を得ることができる。そして実際実行可能だ。

enumにclassを内包させたりして結構苦労する.Neptuneの次は.Mercuryみたいに、循環enumも割と簡単にできてしまうし、なんなら、regexやら遅延評価で超技巧的なこともできそうだ。

 

しかし、これ実はOSやコンパイラ配布側(つまりApple)からすると相当筋の悪いソリューションで、思いついて"自分天才ですから"なんて言ってちゃいけない(と思う、個人的には)。

理由は上記で出てきた、

...Planet_whoAreYou.Mercuryという抽象的なモノとして扱え ...

 というのがその全て。

これ、抽象的に扱うというのは、具象的な扱いは全て配布側で握るよ、気が向いたら内部表現なんて全然違うモノにするよん。といっている(ようにも聞こえる)。

つまり、ある日突然xをStringでインスタンス化したコードは全く想定外の動きを示し、実は人間の読めないハッシュ値になることすら否定はできない。例えば、swift3ではenumが直で階層構造持てるようになって、.Jupiter.NaturalSAT.Ganimede, .Mercury.None, .Earth.MTSAT.HimawariVIIIとか(妄想)。

今の仕様で、これをやろうとすると結構めんどくさい。主張点がぼけるからかもしれないが、Appleが出しているトランプの例示が、別々に宣言したenumの組み合わせでしかできないことを示しているのかと。確かにpropertyは委譲できないけど、関数やらstruct/class宣言は内包できるんで、がんばればできるのかとは思う。

 

なんかいろいろ議論が混線しちゃったけど、言いたいことは、enumにこんな特徴見つけたが、たとえお客から数時間後に緊急納品ねじ込まれたとしても、よい子はマネしちゃだめということ。

いや、たいていはチーム内の誰かがアーキの意見を聞かず、色気を出して技巧に走り、自分の守備範囲ではないEnum値をIntで順序ありきとかに勝手に書き換えてシステム前提ぶっ壊す。あげく褒め殺してアピールをしてくるというのが現実なんだけど...

だからJavaなんかは、禁則コードが描きやすくなってるんだよね。Appleはその辺あんまり興味ないみたいだけど。

"やられて困ることは、はなからできなくしろ" これ鉄則。

マーフィーの法則の失敗する余地を残したシステムは、いつか誰かが必ずやらかすってやつ。