Objective Cでnilと向き合う

最近のObjective Cでは、クラスやプロトコルの定義をするときに、メソッドの引数・返り値やプロパティがnilになるかどうかを書くことができるようになっています。

@interface Person : NSObject

@property (nonatomic, nonnull) NSString *name;
@property (nonatomic, nullable) NSString *address;

@property (nonatomic, nonnull, readonly) NSString *displayName;

+ (nonnull instancetype)personWithName:(nonnull NSString *)name;

@end

namenilになりませんが、addressnilのことがありますし、personWithName:を呼ぶときにはnamenilを渡してはいけませんし、返り値はnilになりません。Nullabilityのアノテーションがついていれば、マニュアルを見たりソースコードを見たりしなくても、これだけのことがわかります。便利ですね。

このNullabilityの注釈が特に重要なのはSwiftとの相互互換性を考えたときですが、それはひとまず置いておきます。今日の話題は、この情報がObjective Cの中でほとんどまったく活用されないことです。

Person *person = [Person personWithName:@"Taro"];

person.name = nil;  // ここは警告が出る

NSString *name = nil;
person.name = name;  // ここは出ない

メソッド呼び出しの引数にあからさまにnilを渡した場合には警告が出ますが、一度変数に代入すると素通ししてしまいます。変数にNullabilityを書いてみるとどうでしょうか。

NSString * _Nullable name = nil;
person.name = name;

これでも警告は出ません。Nullabilityの注釈は単純に無視されます。メソッドの返り値も警告は出ません。

- (nonnull NSString *)displayName {
  return nil;  // 明らかに nonnull ではない……
}

これでは既存のクラスをnonnullにしようと思いたったとしても、作業が困難です。雑にnonnullを付けて回ると、nilが返るのにnonnullと主張する嘘つきメソッドが量産されることになります*1。せっかくNullabilityが書けるようになっているのに、これでは役に立たないと言っても過言ではないでしょう。

この問題をなんとかするためのツールを作りました。

github.com

インストールと使い方

Homebrew (brew tap)になっているので、適当にインストールしてください。

$ brew tap soutaro/nullarihyon
$ brew install nullarihyon

Clangとかダウンロードはじめるので不穏な感じがするかもしれませんが、Nullarihyonだけしか見ないところにコピーするので既存のなにかを壊すことはないはずですし、バイナリ配布を使うのでインストールが死ぬほど遅いということもありません。

インストールしたら、Xcodeプロジェクトのビルド設定を変更します。Build Phaseのコンパイルの後の段階に、新しくRun Script Phaseを追加してください。

if which nullarihyon >/dev/null; then
  nullarihyon xcode
fi

これでプロジェクトをビルドすれば、ソースコード中のNullabilityが間違っているところを全部見つけて、警告としてXcodeに表示します。

  • 毎回すべてのソースコードを検査すると時間がかかるので、変更されたファイル(今回のビルドでコンパイルされたファイル)のみを検査します
  • 上の事情と、Frameworkとかいろいろ面倒を見ないといけないものがあるので、コンパイルよりも後の段階で実行しないといけません
  • ARMだと上手く動かないので、iOSプロジェクトの場合はシミュレータを対象にビルドしてください

検査の結果をフィルタリングする

ソースコードの歴史の長さとか量にもよりますが、実行してみると何千件と警告が出ると思います*2。これは多すぎて修正するのが大変、というかふつーに心が折れると思いますので、検査の結果をフィルタリングできるようになっています。.xcodeprojと同じディレクトリにnullfilterというファイルを作って、そこにクラス名を書いてください。

$ cat nullfilter
Person
PersonViewController # コメントが書ける
# PersonAddressViewController

これでPersonPersonViewController以外に関するの警告は表示されなくなります。「〜に関する警告」とはなんなのかという話ですが、

  1. クラスの実装で発見された警告
  2. クラスが定義しているメソッドの引数に関する警告
  3. クラスが定義しているメソッドの返り値に関する警告

です。

ワークフロー

具体的な作業の方針としては、次のようになると思います。

  1. どのクラスを直すか決める
  2. nullfilterを作る
  3. NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_ENDを、直すクラスの.h.mに書く
  4. 警告がなくなるまで、修正→検査を繰り返す

一度に出てくる警告を数十個に減らせれば、まー少し気合いを入れれば直せます。

検査の内容

簡単に言えば、nonnullな変数にnonnullでない式を代入しようとすると警告になります。派生で、

  • メソッドの引数も検査
  • returnする返り値も検査

しています。ブロックの型の引数・返り値の型も検査しているので、void(^)(NSString * _Nonnull)^(NSString * _Nullable x){}を渡そうとすると警告が出ます。賢い!

あまりに自明なfalse positiveとか、まー常識的に考えてそこは気にしないみたいなものは、できるだけ見なかったことにしたいので、

  • Nullabilityの指定がないローカル変数の宣言に初期値がある場合は、初期値のNullabilityを使う
  • ifの条件から明らかなものはthen節ではnonnullにする
  • selfnonnullだと思う
  • ループの変数はnonnullだと思う
  • allocinitclassの返り値は、Nullablityの指定がない限りnonnullだと思う

などしています。あと、どうしても明示的なキャストを書くのが増えるので、そこで間違えないようにするというのも注意しています。うっかり間違えてキャストを書かないように「_Nonnullな型に変更するキャストはクラスを変える場合警告。ただしidからのキャストは例外」などしていますし、逆に作業途中で書いたキャストがいらなくなることもあるので「_Nonnullから_Nonnullのクラスを変更しないキャストは警告」などもしています。もりもりキャスト書いてok。

これを使うべきなの?

使ってみるとわかるのですが、基本的にとても費用対効果が悪いのでなかなか微妙なところです。多分あなたが検査している対象のプログラムは今動いているものなので、型システム的には間違いがあっても、その範疇を超えたところで大体上手く動いているんですよね……そこで「isEqualToString:の引数はnonnullだから!」とか得意げに言われても、実はあまり嬉しくなかったりします。もちろん、Swiftで言うところのStringString?を混同して使っていると言うことなので、まずいのは確かなのですが。

今はかなり厳格に検査しているので(Nullabilityが書かれていないものはnullableになるのでいっぱい警告が出る)、もう少しマイルドなもの、例えば

  • Nullabilityが書かれているものだけ検査する
  • 組み込みライブラリに関する警告は無視する

とかした方が実用的なのかもしれません。

とりあえず私の手元にあるObjective Cのプロジェクトではなんかそれっぽい結果がでるようにはなっていますが、全然雑に作っているので、なにか上手く動いていない感じとかありましたら、ぜひ教えてください。

*1:実際の経験

*2:ユビレジの場合の例をいうと、500個ソースコードがあって2500個警告が出ました

Cのauto

Unix考古学を読んでいて、ちょっと面白い記述を見つけた。

たとえば、Dennis Ritchieのコンパイラは、局所的な自動変数をautoという型で定義したり、あるいは変数定義を省略することを許していましたが、型を明確に宣言するという今日のスタイルに変更されました。

Unix考古学 Truth of the Legend

Unix考古学 Truth of the Legend

これだけを読むと、

auto count;

のような宣言が許されていたように思える。

これってもしかして、C++っていう言語が2011年に獲得した機能のことでは……???これ多分1970年代の頭の話なので、MLができたのが1973年で型推論アルゴリズムが1969年とかで*1、その前後にCには型推論があったってことか*2。すごいな!

そんなことはない

検索したら当時のCのマニュアルっぽいものが見つかったので読んでみた*3

In a declaration inside a function, if a storage class but no type is given, the identifier is assumed to be int; if a type but no storage class is indicated, the identifier is assumed to be auto.

なんのことはない、型を省略するとintになるのだった。関数宣言が見つからないときとかそうだった気がするので、まあ一貫性がある。

*1:Wikipediaによる

*2:多相型も関数もないので、全然制限されているけど

*3:1975年のものと書いてある

LINTを見直した話

僕は基本的にLINTが大嫌いで、理由は

  • いくつかのLINTはあまり上手く実装されていない
  • いくつかのよくあるルールは、プログラミング言語の設計者がわざと導入したルールを制限するものである

という二つが主である。(既に使われているLINTにわざわざ文句は言わないし、開発チームで決めたことには従いますが。)一番ばかばかしいと思うルールの一つに、

  • リストの最後のカンマを入れるか入れないか

がある。これなんかは、設計者がわざわざ導入した自由を逆に制限するものであり、しかも見た目が揃う以外の利点がなにもないルールなので、こういうルールを有効にするのは止めたほうが良いと真顔で発言できる。

で、ずっとLINT嫌いの立場を貫いてきたんだけど、実はLINTって便利じゃないかと思ったことがあるので、書いておきたい。

OCLintのCoveredSwitchStatementsDontNeedDefaultを知って、もしかしたらLINTって便利なものなのかもしれないと、不明を恥じたのだった。これは、Objective-Cプログラムのswitchが網羅的な場合にdefaultがあったら警告するもので、つまり無駄な空のdefaultを書かなくて良くなる効果がある。

switch (enumValue) {
case SomeCase:
  ...
  break;
case AnotherCase:
  ...
  break;
default:
  // Unreachable
  NSAssert(false);
  break;
}

みたいなプログラム、皆さん書きますよね。現時点ではdefaultはいらないんだけど、将来にenumのcaseが増えたことを考えると、defaultを書いておいてせめて実行時にはエラーになって欲しい。でもこのdefaultとか明らかに無駄なので、できればソースコードから無くしてしまいたい。

この葛藤から逃れられるのであれば、LINTは使うべきだなあと思ったのでした。

これは、止めて欲しいと思うルールとなにが違うかというと、

  • 止めて欲しいのは、ある種のプログラムを書くことを禁止するルール
  • 良いと思うのは、コンパイラの賢さが足りないせいで保守的にならざるを得なくてこれまで書けなかったプログラムを、書けるようにするルール

と言える気がする。

なお、OCLintのCoveredSwitchStatementsDontNeedDefaultについて言えば、最近のClangはNS_ENUMとかの新しい構文を使って定義したenumであれば網羅性の検査をしてくれるので、特にOCLintを使う必要はありません。(Cの素朴なenumを書くのを禁止するルールがないかなーと思って眺めてたら見つけました。)

長い文字列をNSDecimalNumberに変換するとおしりが0になる

なにかの金額を入力する画面などで、ユーザーに数字を入力してもらうUIを作ることがある。金額なので、NSDecimalNumberで受けることになる。このとき大きな数字を入力すると、入力とは違う値になることがある。*1

NSDecimalNumber(string: "999999999999999999999999999999999999999").stringValue
// => "999999999999999999999999999999999999990"

これよりも長い文字列を入力すると、0が続く。

NSDecimalNumber(string: "9999999999999999999999999999999999999999").stringValue
// => "9999999999999999999999999999999999999900"

これは、NSDecimalNumberの内部表現による制限である。NSDecimalNumberは、128bitの符合なし整数と32bitの符合つき整数の組で表現されている。(下の定義では_mantissa_exponent。)

struct NSDecimal {
    var _exponent: Int32
    var _length: UInt32
    var _isNegative: UInt32
    var _isCompact: UInt32
    var _reserved: UInt32
    var _mantissa: (UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16)
    init()
    init(_exponent _exponent: Int32, _length _length: UInt32, _isNegative _isNegative: UInt32, _isCompact _isCompact: UInt32, _reserved _reserved: UInt32, _mantissa _mantissa: (UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16, UInt16))
}

例えば、123.45であれば、12345 * 10^-2として、12345と-2の組で表現する。*2

つまり、_mantissaに対応する値は、128bitで表現できる範囲でなくてはならない。

Rubyで試してみると999999999999999999999999999999999999999を表現するには130bit必要とのことなので、128bitでは足りずに切り捨てられたということがわかる。

> 999999999999999999999999999999999999999.bit_length
=> 130
> 99999999999999999999999999999999999999.bit_length
=> 127

つまり、999999999999999999999999999999999999990というのは、

  • _mantissa = 9999999999999999999999999999999999999
  • _exponent = 1

になっているはず。

*1:UIとしては現実的な桁数に入力を制限するべきである。

*2:多分こうだし、本質的には同等の表現なことはかなり自信があるが、まったく同じ値かどうかはわからない。

NSDecimalNumber(floatLiteral:)は使ってはいけない

私がSwiftで気に入っている改善として、NSDecimalNumberインスタンスがちょう簡単に作れることがあります。

let d : NSDecimalNumber = 15.97

すばらしい。Objective Cだとこうなって、辛い。

NSDecimalNumber *d = [NSDecimalNumber decimalNumberWithString:@"15.97"];

このSwiftの機能はFloatLiteralConvertibleというプロトコルを使って実装されていて、floatのリテラルの型がFloatLiteralConvertibleなクラス(構造体もできる?)のときに、適当なイニシャライザを呼んで変換してくれるというものです。

init(floatLiteral: Double)

わーかっこいい。かっこいいのですが、ところがしかし型をよく見ると一回Doubleが作られています。NSDecimalNumberDoubleから作るのは安全なのかと聞かれて、一瞬悩んでしまったのでした。とりあえず、NSDecimalNumberについて言えば明らかにダメです。

let d : NSDecimalNumber = 15.97
_ = d.stringValue     // => "15.970000000000004096"

15.97とは似ても似つかない値になりました。Stringから作る場合と見比べれば明らかですね。

let d = NSDecimalNumber(string: "15.97")
_ = d.stringValue     // => "15.97"

やはり、NSDecimalNumberを作るときには、整数か文字列を与えるのが安全なようです。

さて、これはなぜでしょう?「浮動小数点はそんなもんだよ」と言われて、一瞬納得してしまいそうになったのですが、やっぱりよく考えた結果、これはNSDecimalNumberが壊れているというのが私の現時点の理解です。

浮動小数点に関しては、僕は全然経験がないので、ここから先まったく間違ったことを言うかもしれません。 自信はありません。 間違っていたら、教えてもらえると嬉しいです。

Floatのリテラルとは

0.1を10回足しても1.0にならないことはよく知られていると思います。

let a = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1
_ = (a as NSNumber).stringValue // => "0.9999999999999999"

ソースコード0.1と書いても、それは実数の0.1ではありません。Doubleで表現できる中で一番実数の0.1に近い数です。この辺りの感覚があると、15.97が15.970000000000004096になると言われても納得しても良さそうな気がする感じもします。

ところが、0.1を一度Doubleにしてから文字列表現に直すと、"0.1"が得られます。0.1ではないのに。少し不思議な気がしますね。これは、Doubleを表現する10進数の中に0.1よりも近いものが他にないことが理由です(多分)。Floatから文字列への変換をどういうアルゴリズムで実装するかによって違いが生まれますが、SwiftとかNSNumberではそうなっているようです。Swiftで試すと、0.1に対応するFloatの文字列表現は"0.1"ですし、15.97に対応するFloatの文字列表現は"15.97"です。

FloatをNSDecimalNumberに変換するというのは10進数に変換するのと同じことですから、NSDecimalNumberDoubleを与えるとおかしくなるというのは辻褄が合わない話に思えます。

もちろん、変な数を考えるとうまく行かなくなることはあります。

let x = 0.09999999999999986
let y = 0.09999999999999987
_ x == y  // => true

ここで、xyは区別できないので、文字列表現も元には戻りません。

_ = (x as NSNumber).stringValue   // => "0.09999999999999987"
_ = (y as NSNumber).stringValue   // => "0.09999999999999987"

まあ仕方ないですね。NSDecimalNumberにすればこの二つは区別できるようになります。

let x = NSDecimalNumber(string: "0.09999999999999986")
let y = NSDecimalNumber(string: "0.09999999999999987")

_ = x.stringValue   // => "0.09999999999999986"
_ = y.stringValue   // => "0.09999999999999987"

15.970000000000004096

それでは、15.970000000000004096とはなんなのでしょうか。15.97は15.970000000000004096だったのでしょうか。

let s = 15.97
let t = 15.970000000000004096
_ = s == t   // => false

もちろん違います。この二つは別々の数として認識されています。15.9715.970000000000004096の間の数すら、Doubleは表現できます。

let u = (s + t)/2
_ = s < u && u < t  // => true

15.970000000000004096は、意味がわからない謎の数字なのです。同じ例は、0.09999999999999986でも見ることができます。

let x : NSDecimalNumber = 0.09999999999999986
_ = x.stringValue  // => "0.09999999999999983616"

0.09999999999999983616はどこから出てきたのでしょうか。ちなみに0.099999999999999860.09999999999999983616の間には0.09999999999999985があります。

正しい実装の例

さて、DoubleNSDecimalNumberに変換する正しいコードは次のようになります。この関数にFloatの15.97を与えると15.97を指すNSDecimalNumberが得られます。0.099999999999999860.09999999999999987のような場合に注意は必要ですが、それは仕方ないでしょう。

ちょっと難しいかもしれませんが、がんばって読んでみてください。

func decimalNumberWithDouble(double: Double) -> NSDecimalNumber {
    let number = double as NSNumber
    return NSDecimalNumber(string: number.stringValue)
}

参考にしたもの

直接この問題について理解するというよりは、ぼんやり眺めて浮動小数点の周りの空気がわかった気がした。

特定のMacでビルドできないXcodeプロジェクト

Xcodeに登録されているファイルの名前とファイルシステム上の名前が大文字小文字で違っている場合、Apple Storeから買ってきたばかりのあなたのMacではビルドできるかもしれませんが、Case Sensitiveファイルシステムに設定しているMacではビルドできません。

f:id:soutaro:20160314100916p:plain

この画像でUBTypedJSONDictionary.hとなっているソースコードは、ファイルシステム上はUBTypedJsonDictionary.hでした。なんでこうなったかというと、コードレビューで「NSJSONSerializationがあるのだからここはJSONであるべき」と指摘されて、なるほどと思ってXcodeでRenameしたらファイルシステムがついてこなかったからです。

注意しましょう。

注意したくない人は、Xcodeプロジェクトに登録されたソースコードをスキャンして、Case Sensitiveファイルシステムで問題になりそうな場合に警告するプログラムを作ったので、CIで実行したら良いと思います。

github.com

CI完了を待ってPRをマージするbotを作るためのライブラリを公開しました

CIを待って自動でPRをマージするbotを作ると便利だった - soutaroブログで、紹介した、CIが完了したときに自動でPRをマージするbotですが、皆さんが簡単に実装できるようnpm packageにしました。

github.com

動くと良いですね。(npm packageはまだ試してない。)