タグ「プログラミング」の 記事 143 件中 101 ~ 143 件を表示しています。

面倒なのでできれば相手をしたくないが

さて、前回Service のプロセスがアプリと同一の場合、つまり通常の場合に Binder を用いて双方向でやり取りする方法に関して考察したが、今回は止むを得ない事情でプロセスを分けなければならなくなった場合に Messenger を用いてプロセス間通信を行う為の実装例に関して記載する。

Service 側の実装に関してコード例を示す:

companion object {

    /** クライアント側からの replyTo 受信. */
    val MESSAGE_RECEIVING_REPLY_TO = 1

    /** クライアントへのメッセージ送信. */
    val MESSAGE_SEND = 2
}

/** サービス側の処理を行うハンドラ. */
private class MyHandler : Handler() {

    /** クライアントの Messenger. */
    var clientMessenger: Messenger? = null

    override fun handleMessage(msg: Message?) {
        when (msg?.what) {
            MESSAGE_RECEIVING_REPLY_TO -> clientMessenger = msg.replyTo
            else -> super.handleMessage(msg)
        }
    }
}

/** ハンドラ. */
private val mHandler = MyHandler()

/** サービス側のメッセンジャ. */
private val mMessenger = Messenger(mHandler)

override fun onBind(intent: Intent?): IBinder = mMessenger.binder

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    // サービス実行から 5 秒後にコールバック実行
    Handler().postDelayed({
        val bundle = Bundle().apply { putString("message", "ほげふが") }
        mHandler.clientMessenger?.send(Message.obtain(null, MESSAGE_SEND, bundle))
    }, 5000)
    return Service.START_STICKY_COMPATIBILITY
}

こちらでもプロセスが同一の場合と同様に onBind()IBinder を返却しているが、こちらは Messenger に対応する Binder である (後で Messenger に復元可能な Binder) ところが異なる。 mMessenger というフィールドを持っているが、これは Handler インスタンスを Messenger クラスでラップしたものである。 Handler クラスの方を拡張することによって相手から投げられたメッセージをどう処理するかを実装する。

上記の実装例では msg.replyTo をフィールドに持っているが、これは Service からみてどのクライアントの Messenger でメッセージを送信すれば良いのかを判別するためのものである。 つまり Service 側の任意のタイミングでクライアント側にメッセージを送信したい場合は以下の手順を踏む必要がある:

  1. Service の接続確立時にクライアント側から replyTo を教えるためのメッセージを Service 側の Messenger を使って送信
  2. Service 側の送信したいタイミングでフィールドに持っている replyTo を使用してクライアントに送信

上記コードでは onStartCommand() 内で 5 秒後に実際にクライアントに対してメッセージを送信している。

Activity / Fragment 側のコード

/** クライアント側の処理を行うハンドラ. */
private class MyHandler(val activity: MainActivity) : Handler() {
    override fun handleMessage(msg: Message?) {
        when (msg?.what) {
            MyService.MESSAGE_SEND -> activity.button.text = (msg.obj as Bundle).getString("message")
            else -> Log.w(javaClass.simpleName, "Unknown what: ${msg?.what}")
        }
    }
}

/** サービスとの bind 時の接続を行う. */
private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mServiceMessenger = Messenger(binder)

        // サービス側に replyTo を介してクライアント側の Messenger を教える
        val message = Message.obtain(null, MyService.MESSAGE_RECEIVING_REPLY_TO)
        message.replyTo = mClientMessenger
        mServiceMessenger?.send(message)
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        mServiceMessenger = null
    }
}

/** サービス側の Messenger. */
private var mServiceMessenger: Messenger? = null

/** クライアント側の Messenger. */
private val mClientMessenger = Messenger(MyHandler(this))

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // サービスを bind する
    val intent = Intent(applicationContext, MyService::class.java)
    startService(intent)
    bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
}

override fun onDestroy() {
    unbindService(mConnection)
    val intent = Intent(applicationContext, MyService::class.java)
    stopService(intent)
    super.onDestroy()
}

Service との接続が確立した時に渡ってきた IBinder を逆に Messenger のコンストラクタに渡すことで Service 側の Messenger を復元している。 それを使用することで Service 側にメッセージを送信することができる。 先ほど記載したように、メッセージの replyToActivity, Fragment 側の Messenger を詰めて送信しておく。

後は Service 側と同様にこちら側でも Handler クラスを拡張して Service から渡ってきたメッセージを処理する為のコードを書く。

まとめ

  1. ServicebindService() する
  2. Service とのコネクション確立時に ServiceMessenger を使用して Service に対しクライアント側の MessengerreplyTo に詰めて送信することで教える
  3. Service 側で Handler の拡張クラスの実装によって渡ってきた replyTo を格納しておく
  4. replyTo を使用して任意のタイミングでクライアントに対しメッセージ送信
  5. クライアント側の Handler の拡張クラスでの実装で UI の表示処理などを行う

手順をしっかり理解しないと間違えそうな内容なので、やはり手軽ではないと思う。

プロセスが同一の場合の例

大抵の場合アプリケーション内に Service を置く場合は必要に迫られない限りアプリケーションと同一プロセスとすると思われる。 この場合 Fragment 側から Service を操作したい場合は bindService を使用し Binder 経由で操作を行うのが一般的だ。

あまり意味のない例で恐縮であるが、例えば以下のような「現在時刻のタイムスタンプを返すサービス」を実装したものとする:

/**
 * 現在時刻を返すサービス.
 */
class MyService : Service() {

    inner class MyBinder : Binder() {
        fun getTime() = Date().time
    }

    /** Binder インスタンス. */
    private val mBinder = MyBinder()

    override fun onBind(intent: Intent?): IBinder {
        Log.d(javaClass.simpleName, "onBind start.")
        return mBinder
    }

    override fun onUnbind(intent: Intent?): Boolean {
        Log.d(javaClass.simpleName, "onUnbind start.")
        return super.onUnbind(intent)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d(javaClass.simpleName, "onStartCommand start.")

        // TODO 本当は何か UI に関係ない部分で待機するような処理が入るはず

        return super.onStartCommand(intent, flags, startId)
    }

    override fun onCreate() {
        super.onCreate()
        Log.d(javaClass.simpleName, "onCreate start.")
    }

    override fun onDestroy() {
        Log.d(javaClass.simpleName, "onDestroy start.")
        super.onDestroy()
    }
}

上記の場合 onBind() でサービス内で定義されている MyBinder を返却しているが、クライアント (Fragment) 側からこのインスタンスを使用してサービスの機能を利用する。 よって、例えば「ボタンを押したらサービスからデータを取ってきて画面に表示する」といった実装を行う場合は以下のようなコードになる:

/** サービスとの bind 時の接続を行う. */
private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mBinder = binder as MyService.MyBinder
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        mBinder = null
    }
}

/** Binder. */
private var mBinder: MyService.MyBinder? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // サービスを bind する
    val intent = Intent(applicationContext, MyService::class.java)
    bindService(intent, mConnection, Context.BIND_AUTO_CREATE)

    // ボタン押下でサービスからデータ取得し表示する
    button.setOnClickListener { text.text = mBinder?.getTime()?.toString() }
}

override fun onDestroy() {
    unbindService(mConnection)
    super.onDestroy()
}

この例だと bindService() しかしていないので Service 側としては onCreate()onBind() は呼ばれるが onStartCommand() はコールされないことに注意する。 onStartCommand() はあくまで startService() した時に呼ばれる処理となっている。

ServiceConnection.onServiceConnected() 内で Service 側で定義したクラスである MyService.MyBinder にキャストしているが、これが可能なためにこの MyService.MyBinder に対し自由にメソッドやフィールドを追加することにより Service に対して好きに処理を行うことができるようになる。

Service から任意のタイミングでクライアントに処理を行わせたい場合

上記の Binder の例はあくまでクライアント側から任意のタイミングで Service に対し処理を行わせたい場合に使用できる。 逆方向である Service からクライアントに対し処理を行わせる場合であるが、以下の 3 通りが考えられる:

  • Binder を介してクライアント側からコールバックを渡す
  • EventBusRxJava などのライブラリを使用して任意のイベントを飛ばす
  • BroadcastReceiver をクライアント側に実装して Service からの sendBroadcast() を受け取る

今は EventBus などのライブラリを使用すると任意の位置からイベントを伝搬できるのでとても便利だ。 ここでは 1 番目の Binder を用いて実現する方法に関して書く。

例えばサービスが起動された 5 秒後にクライアント側で実装されたコールバックを実行する例を示す。 サービス側の実装として、まずクライアント側で実装すべきコールバックのインターフェースを開示する:

class MyService : Service() {

    /** サービスで 5 秒後に実行されるコールバック. */
    interface Callback {
        fun doIt()
    }

    inner class MyBinder : Binder() {
        fun getTime() = Date().time

        // クライアント側からコールバックを渡す為のメソッドを追加
        fun set(callback: Callback?) {
            mCallback = callback
        }
    }

    /** Binder インスタンス. */
    private val mBinder = MyBinder()

    /** クライアント側で実装されたコールバック */
    private var mCallback: Callback? = null

    override fun onBind(intent: Intent?): IBinder = mBinder

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // サービス実行から 5 秒後にコールバック実行
        Handler().postDelayed({
            mCallback?.doIt()
        }, 5000)
        return Service.START_STICKY_COMPATIBILITY
    }
}

後は以下のようにクライアント側でコールバックの実装を行い、サービスのコネクションが張れたタイミングでコールバックを Binder を通してサービス側に渡してやれば良い:

private val mCallback = object : Callback {
    override fun doIt() {
        button.text = "サービス側から呼ばれた"
    }
}

/** サービスとの bind 時の接続を行う. */
private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mBinder = binder as MyService.MyBinder
        mBinder?.set(mCallback)  // サービス側に定義したコールバックを渡す
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        mBinder?.set(null)
        mBinder = null
    }
}

/** Binder. */
private var mBinder: MyService.MyBinder? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // サービスを bind する
    val intent = Intent(applicationContext, MyService::class.java)
    startService(intent)
    bindService(intent, mConnection, Context.BIND_AUTO_CREATE)

    // ボタン押下でサービスからデータ取得し表示する
    button.setOnClickListener { text.text = mBinder?.getTime()?.toString() }
}

override fun onDestroy() {
    unbindService(mConnection)
    val intent = Intent(applicationContext, MyService::class.java)
    stopService(intent)
    super.onDestroy()
}

これで 5 秒後にボタンに「サービス側から呼ばれた」という文字列が表示される。

プロセスを分けるとどうなるか

さて Android の場合同一アプリ内であっても別のプロセスを使用した Service を定義することができる。 これは AndroidManitest.xml に以下のように定義を加えることにより実現できる:

<service
    android:name=".MyService"
    android:process=":remote"/>

android:process=":remote" の部分であるが、:remote の箇所は別に :hoge などの他の名前でも良い。 このようにして先ほどのコードを実行してみると ServiceConnection.onServiceConnected()MyService.MyBinder にキャストしている箇所で ClassCastException がスローされるようになる:

private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mBinder = binder as MyService.MyBinder  // 駄目. binder は BinderProxy が渡ってくる!!
    }
}

デバッグ実行で見てみると分かるが Service 側で渡した Binder の参照がそのまま渡ってくるのではなく BinderProxy というプロキシクラスのインスタンスが渡ってくるようになる。 その為このようにキャスト前提の実装にしてしまうと失敗する。 また EventBus などのライブラリもプロセス間通信には使用することができない。

この場合は公式にある通り Messenger を使用するわけだが、長くなったのでそのコード例は次回とする。

Professional は高い

私は Python は仕事で使用しているわけではないのだが、趣味のプログラミング、特に今はこの Blog を開発する言語として使用している。 Python を日常的に書く方はエディタや IDE は何を使用しているのだろうか。 Vim で書いているという方も多いと思うが、私が愛用しているのは PyCharm である。

PyCharm の元となっている IntelliJ IDEA は言語ごとにそれぞれのエディションがあり、私が仕事でよく使用しているのが PHP 向けの IDE である PhpStorm である。 この PhpStorm を使う前は Eclipse の PHP プラグインなど使用していたと思うが、この PhpStorm が便利すぎて今となってはこれがないと仕事に支障が出るし、これなしで PHP を書きたくないと思うほどだ。

PhpStorm には Community 版などというものはないし何より重要な仕事道具の為特に疑問も持たず毎年お金を払うようにしている。 しかしこれがちょっと使うだけの場合は悩んでしまうくらい高い。 初年度 89 ドル、次年度 71 ドル、3 年目以降 53 ドルとなっている。 そして PyCharm のサブスクリプション金額も同様である。

ただ PyCharm の方には上記の通りのお金がかかる Professional と無料版の Community というのがある。 Web アプリなど関係なく Python を学習したいというだけであれば Community 版で十分だろう。 私は Web アプリ開発で使用しているので Professional 版が好ましいのだが何となくお金が惜しいのでしばらく Community 版で作業してみたがいろいろと辛い場面はあった。 以下それを列挙しようと思う。

Django サポートがない

Professional には Django のサポートがあるのだがこれがすごく便利で、画面右上の再生アイコンから Django のテストサーバーの起動・停止ができるしデバッグ実行もできてしまう。 PHP の場合ブレークポイントで止めるには Xdebug の設定など必要で面倒 (なので大抵は var_dump などで変数を出力してデバッグする) なのだが PyCharm ならば何もしなくても普通にブレークポイント張ってデバッグ実行すればちゃんとその位置で実行が止まる。 Community にはこれがないのでテストサーバ起動はコンソールを叩く (manage.py runserver)。

一番作業効率に差が出そうだと思ったのは Django テンプレートのサポートで、Django テンプレートの記法 {{ variable }}{% for x in xs %}などを書いているところでもコード補完が利く。 コード補完が効かないと適宜フレームワークのドキュメントを眺めながら書いていかなければならず非効率である。

Web 系のコードがすべて補完が効かない

PyCharm の Community には Web development の機能がない。 これは TypeScript や LESS, SASS などのモダンな言語だけでなく JavaScript, CSS, HTML の補完も効かないことを意味する。 これも前述と同様で、補完が効かない場合適宜ネットで検索して必要なフィールド、メソッドを探してから書かなければならない場面が多く辛い。

ただ、この 1 点だけであれば PhpStorm を既に持っているならばそこだけ PhpStorm の手を借りるという手もあるが、それも少し面倒な話だ。

IDE 上からデプロイができない

PhpStorm もそうだが Professional には SFTP などのプロトコルでリモートサーバにデプロイする機能がある。 同期したくないファイルを除外するルールなども書けるし、本番反映したい場合は IDE 上からメニューを選択するだけでいいので便利だ。

Community はこれがないので WinSCP を入れて自分で必要なファイルを選択して同期するか Cygwin 上で rsync コマンドを叩く必要がある。

データベースサポートがない

DB のサポートがないので Community でデータベースを見る場合はやはりコンソールでコマンドを叩いて見ることになる。

「とりあえずできる」と「快適にできる」は大きな差

別に Community であったとしても工夫次第で Django を使用した Web アプリの開発は可能なのだが「困ったらコンソールでコマンドを叩け」ではそもそも何故 IDE を使用しているのか、という本質を見誤りそうになる。 仕事で使用するのであれば迷いもなく購入するのだが、こういうわけで PyCharm に関しては未だに迷い続けている。 仕事も Python になってくれれば一件落着なのだが……と今でも少し思う。

あと Android Studio が無償になっているのは間違いなく IntelliJ 系 IDE の普及に貢献できていると思う。 私などは Android Studio から入り PhpStorm を試して止められなくなったクチだ。

描画が遅くなってきたのでページングを実装した

当 Blog のタイトル一覧ギャラリーであるが、最初のうちは別に件数も少なかったので常に全件表示する感じでも全くストレスを感じなかった。 だがこの Blog は毎日欠かさず何かしら書いているため最近になると記事の件数は 600 件を超え、画像に関しては 300 件近くになってしまった。 その為さすがに全件表示してしまうと描画にそこそこ時間がかかるようになってきた。 そこで Web アプリでよくあるページング処理を実装してみたわけだが、大抵の Web フレームワークには Pagination の実装が内蔵されているが当 Blog で使用している Django にもあったので割と簡単に実装することができた。

データ取得処理の差し替え

例えば変更前のデータ取得コードが以下のようになっているとする:

context['posts'] = Post.objects.filter(**params)

これを Paginator オブジェクトでラップして必要なページを取得すれば良い:

paginator = Paginator(Post.objects.filter(**params), 100)  # 1 ページ 100 件とする
number_of_page = request.GET.get('page') if request.GET.get('page') else 1  # GET パラメータで来たページインデックス
context['posts'] = paginator.page(number_of_page)

これだけで GET パラメータで page=2 などと渡せば正しく 101 件目から 200 件目を描画してくれる。

ページング UI

後はページングの為の UI を描画する処理をテンプレートに書く。 公式ドキュメントにも書いてあるが PaginatorPage オブジェクトにはそれぞれページングの UI を描画するために使える値が含まれている。

paginator.count  # 検索対象総件数 (例えば 732 件中 101 から 200 件を表示している場合の「732」)
paginator.num_pages  # 総ページ数 (例えば 732 件で 100 件ずつの表示ならば「8」)
paginator.page_range  # [1, 2, 3, 4, 5, 6, 7, 8] などといった全ページ番号のリスト. これをテンプレート側で for 文で回して使う

page.has_next()  # 次ページがあるか
page.has_previous()  # 前ページがあるか
page.has_other_pages()  # 他のページがあるか (使うのか?)
page.next_page_number()  # 次ページ番号
page.previous_page_number()  # 前ページ番号
page.start_index()  # 開始件数 (例えば 732 件中 101 から 200 件を表示している場合の「101」)
page.end_index()  # 終了件数 (例えば 732 件中 101 から 200 件を表示している場合の「200」)

page.number  # ページ番号
page.paginator  # Page インスタンスから Paginator インスタンスを取得する. なので Paginator を Template 側に渡す必要はない

これを使って当 Blog では以下のようにページングを実装してみた (posts は View 側から渡された Page インスタンスである):

<div class="pagination-count">
    {{ posts.paginator.count }} 件中 {{ posts.start_index }} ~ {{ posts.end_index }} 件を表示しています。
</div>
{% if posts.paginator.num_pages > 1 %}
<div class="pagination">
    {% for i in posts.paginator.page_range %}
        {% if i == posts.number %}
    <em>{{ i }}</em>
        {% else %}
    <a href="?page={{ i }}{% if tag_id %}&tag_id={{ tag_id }}{% endif %}">{{ i }}</a>
        {% endif %}
    {% endfor %}
</div>
{% endif %}

ページング程度であればすべて自分で実装することもそこまで大変ではないが、フレームワークで用意されているページングの実装を使うとやはり記述するコードが減って楽だ。

さて、本日で MediaPad M3 Lite 10 を購入して 2 週間ほど経過した。 やはり継続して使ってくると使い始め当初とはまた違った視点で見えてきたこともあった。 大体のことはファーストインプレッションの方に書いてしまったのでもし MediaPad M3 Lite 10 の情報を探しておられるのであればそちらも参照いただきたいのだが、ここには追加で感じたことを書く。

指紋認証はやはり便利

Nexus 9 まではロック解除時に毎回パターンを入力していたのだが、この端末は指紋認証を使うことができるし認証精度もとても良いので便利だ。 尚、スマートフォンは持つスタイルが大体決まっているので 1 つの指 (多くの方は右手人差し指) のみを登録すればいいのだが、タブレットの場合いろいろな持ち方をして使うので直感的に指を伸ばすとその指が指紋認証登録されていなかったりしてもどかしさを感じる。 多少面倒でも最初に複数の指 (左手と右手の親指・人差し指・中指) を登録してしまったほうが良さそうだ。

解像度 1920x1200 は別に問題ない

この端末の解像度は 1920x1200 であり、最近の 7 ~ 8 インチタブレットはおろか高解像度スマートフォンよりも解像度が低いのだが、確かにスマートフォンを持つ距離で見るとドットがはっきり見えるのだが正直実運用ではこの解像度で十分である。 解像度が低いことで消費電力が少なくなりバッテリーの持ちが良くなっていると思うので悪いことばかりではない。

ただ、この端末の下位モデルである MediaPad T3 10 の 1280x800 だと明らかに画質が荒い。 個人的には不満なく Web ブラウジング、Kindle や写真、動画を楽しみたいならば 1920x1200 が最低ラインだと思う。

電池持ちは異常にいいが残量表示は若干眉唾

この端末の電池持ちは異常に良く、体感として同じく電池持ちが良い Mate 9 と比べても遥かに良いと感じる。 ちなみに私が使用しているのは LTE モデルなので条件的には同じはずである。

ただこの端末の電池残量表示に関してはどうも信用出来ないところがあり 30 分ほど使用しても残量が 100% のままだったりすることがある。 先ほど電車内で 1 時間ほど使っても残量 98% だったが流石にそれもおかしい気がする。 車のガソリン残量表示みたいに、残量が少なくなってきたら表示上の減りが速くなるという感じだろうか。

イヤホンジャックの位置が最悪

これがこの端末で一番辛いと思った点だった。 この端末は多くはカバーを付けて立てて使うであろう大型のタブレットなのにも関わらず、イヤホンジャックが長い方の側面に付いている。 これだと風呂フタ型のカバーを付けてテーブルの上に乗せて浅く横方向で使おうとした時にイヤホンジャックが下に来てしまい、そのままイヤホンを刺そうとするととても不格好な事になってしまう。 また、縦で持って使おうとした時もイヤホンジャックがちょうど手に持ちたい位置にある為イヤホンの端子が邪魔で快適に持つことが出来ない。

ちなみに iPad はどうか、と思い調べてみたが、やはり短い方の側面に付いていた。 兄弟機の MediaPad M3 の方の位置も調べてみたが、こちらも短い方の側面に付いていた。 絶対にこちらの方が良い。 MediaPad M3 Lite 10 の位置は設計ミスレベルだと思うし、何故 MediaPad M3 で出来ていたことが出来ないのか……。

充電端子も今時 Micro USB

MacBook から始まった USB Type-C であるが Mate 9 などの最近のスマートフォンやタブレットでは旧来の Micro USB ではなく USB Type-C が増えてきている。 しかしながらこの端末は Micro USB である。 これだと Mate 9 と一緒に持ち歩く場合 Micro USB と USB Type-C のケーブルを両方持っていかなければならない。 更に USB Type-C の方が給電速度が速いので Micro USB 且つバッテリー容量が多いこの端末の場合満充電まで時間がかかる。 ただ、前述の通りこの端末はそうそう電池が減らないのでその点ではあまり困らない。

カバーは 1,000 円近辺のもので十分

これは本体のレビューとは違うのだが、今はタブレットのカバーは Amazon などで 1,000 円近辺でいくつも出ている。 その中の 1 つを購入して使っているが品質上全く問題ない。 風呂ブタを閉じればちゃんとスリープ状態になるし便利。 1,000 円ほどの投資で綺麗に使えて処分するときも高く買い取ってもらえるのだから積極的に活用するのがいいだろう。

Android Studio に warning を出されるようになった

Kotlin で以下のような構文を書くと Android Studio に warning を出されるようになった:

if (value?.isEmpty() ?: false)  // Equality check can be used instead of elvis
if (value?.isEmpty() == true)  // これなら OK

この場合は elvis 演算子 ?: ではなく == で代用できるというわけである。 何故私が上の elvis 演算子 を使った構文を使ってしまっていたかというと、紛れもなく Java では false は基本型 boolean であるという事実が頭にあったからであり Java の概念に引きずられて if (value?.isEmpty() == true) の方の例が些か直感的でないように感じてしまったからだ。 以下どういうことなのか考察する。

Java では

Java では以下のような比較は syntax error となる:

if (null == false)  // syntax error. false はオブジェクトではない!!
if (null == Boolean.FALSE)  // これなら OK

Java は歴史的な経緯にて全てのフィールドやメソッドがオブジェクトというわけではなく基本型 (プリミティブ型) とオブジェクト型に分類される。 そもそも基本型が null になることはなく基本型に対する == 比較は基本型且つ型が一致していないと行えない。

基本型はオブジェクトではないので基本型に対するメソッド呼び出しを行うことはできない。 メソッド呼び出しを行いたい場合は対応するラッパー型 (オブジェクト) に変換した上でメソッド呼び出しを行う。 上記の例の場合 boolean のラッパー型は Boolean なので false に対しメソッド呼び出しを行いたいならば Boolean.FALSE を使えば良い。

Boolean.TRUEBoolean.FALSE はそれぞれ java.lang.Boolean クラスに対するインスタンスである。 一方 Java における == 演算子の役割はオブジェクトの参照が等しいことの比較なので以下のような null との比較は false となる:

System.out.println(null == Boolean.TRUE);  // false
System.out.println(null == Boolean.FALSE);  // false

一方 Boolean.equals() の実装も「比較対象が Boolean クラスでない場合 (null も含む) 常に false となる」となっているので以下も合点がいく:

System.out.println(Boolean.TRUE.equals(null));  // false
System.out.println(Boolean.FALSE.equals(null));  // false

まとめると Java では nulltrue でも false でもない第三の値である。 一方以下のように nullnull の同値比較は true となる事に注意する:

System.out.println(null == null);  // true

Kotlin に話を戻す

話を戻すが Kotlin では以下だとコンパイルが通らない:

if (value?.isEmpty())  // 駄目. value が null の場合戻り値が null となるので評価値が Boolean? となる

Kotlin は null-safety な言語なので上記のような書き方をすると valuenull だった場合に NullPointerException がスローされずに isEmpty() メソッド呼び出しが行われないという挙動になる。 よって null の場合を ?: で付け加えるのは自然に見える。

以下再掲する:

if (value?.isEmpty() == true)  // なんとなく null == true がマズそうに見える

Kotlin では全てのローカル変数はオブジェクトの為 null == true の評価が可能である。 しかし ==null オブジェクトに対する equals() のシンタックスシュガーに見えるので何となく null に対するメソッド呼び出しに失敗しそうに見えるのだが、Kotlin 公式に以下のように記載があった:

Structural equality is checked by the == operation (and its negated counterpart !=). By convention, an expression like a == b is translated to a?.equals(b) ?: (b === null)

つまり Kotlin においては前述の if 文は以下と等価である:

if (value?.isEmpty()?.equals(true) ?: (true === null))

これは比較対象の左辺が null だった場合左辺と右辺が共に null の時のみ true となる、と読み解ける。 つまり Java における「nulltrue とも false とも一致しないし null == null = true である」という原則を生かしつつ、常にそのまま第三の値として == で比較できるように言語の文法レベルで配慮が効いているということだろう。

議事録作成

私はよく会議で議事録を作成しているのだが、ノート PC を持ってくるにしても結構重量があって負担があるので Android タブレットに外部キーボードを繋いで同じように使えないかと考えた。 Happy Hacking Keyboard Professional JP は会社に置いてあるので micro USB - USB 変換アダプタさえあればいけそうだ。 変換アダプタは Amazon で格安で手に入れることができた。

少々使い勝手は慣れが必要だが悪くない

メモ帳としては普段使用している Google Keep を使用した。 ローマ字打ちしか選択できないが最近はなるべく打つようにしているのでそんなに詰まらずに打てる。 各種ショートカット (F7 ~ F10 や Ctrl + A, V など) も Windows と共通になっているようで同じ感覚でいけそうだ。

1 つ、入力メソッドの切り替えに関しては半角 / かなキーを押すか Win + スペースでないといけなかった。 私は入力メソッドの切り替えを多用する打ち方をしているので慣れが必要である。 今手元に英語キーボードが無いので試せないのだが英語キーボードの場合は Win + スペースのみだろうか。

会社で打つ場合はこれでもいけそうだが、旅行・ツーリング先で打つ場合にわざわざ比較的コンパクトとはいえそれなりに重量もある HHKB を持っていくのはどう見てもスマートではない。 また、アダプタで接続できるとはいえキーボードの線が伸びているのも不格好だ。 今後手元に届くであろう「かえうち」が使えないという難点はあるが、それと比較してもコンパクトで安価な Bluetooth キーボードがあれば最高な気がする。 ちょっと検討してみようかと思う。

早速購入した HUAWEI MediaPad M3 Lite 10 をデバッグで使用してみた。 やはり画面が広い端末だと机に置いた状態でとても見やすくていい感じだ。 ただ最初の状態では Mate 9 と同様に Logcat にアプリ側で出力しているデバッグログ (例えば Log.d("HOGE", "FUGA")) が出力されない。 幸いにも Mate 9 と同様の対処方法で出力することができた。 Ascend Mate 7 も同様の方法のようであるし、多くの HUAWEI 端末は全てこの方法でいけるのではないかと思われる。

念の為ここにその方法を書き残しておく:

  1. 端末の電話アプリを起動
  2. *#*#2846579#*#* とダイヤルする
  3. 何やら黒い画面が開くので「1. Background Settings」を選択
  4. 「3. LOG Settings」を押下
  5. 「AP Log」「Charge Log」「Sleep Log」にチェックを入れる (AP Log だけでもよい?)

何と 10 インチタブレットである MediaPad M3 Lite 10 には電話アプリが入っているのである。 ということは恐らく音声通話 SIM を挿せばちゃんと携帯電話として使用できるということなのだろう。

ちなみに上記の方法で設定しても再起動をした時など設定が解除されている時がある。 その時は面倒だがまた同様の手順でデバッグログを ON にする。 何度もやっていると*#*#2846579#*#* も記憶してしまうので今になってみるとそれほど不都合はない。 開発者でない多くの人に対してはわずかとはいえ電力消費をするであろうデバッグログは無用の長物であるので、初期状態で出力が OFF になっているという HUAWEI 端末の配慮は悪くないと思う。

今日注文していた MediaPad M3 Lite 10 が届いた。 ショップの発送がとても早かった。 本来は数日使ってからインプレを書くわけであるが Nexus 9 LTE でも書いた通り私はタブレット端末に関してはこれまで何台も使ってきているので数時間触れば何となく長所・短所が見えてきたのでファーストインプレッションとして書き留めておくことにする。

10 インチ Android タブレットにしては軽い

最近ではこの程度は当たり前なのかもしれないが 10 インチで 460 g と iPad 2017 年モデル並である。 最初に持った時「軽い」と感じた。以前使っていた 10 インチタブレットは 600 g 超えだったのでそれに比べると随分軽く感じる。 勿論人によっては「やっぱりちょっと重い」と思うかもしれないが、正直技術の進歩に驚かされた。

スペックの低さはあまり気にならない

この端末は発売時期が最近なのに対し CPU が非力だと言われているが、確かに Mate 9 のようなハイエンドモデルと比べるとモタツキを感じるものの不快に思うほどの遅さは感じず必要十分だと思った。 MJ モバイルなどプレイしてみたが特に問題は感じられなかった。 勿論 YouTube 視聴や Kindle など全く問題ない。 少なくとも Nexus 9 よりはキビキビと動作する。

唯一みんゴルは元々処理が重いせいか少しコマ落ちのような形になりプレイし辛さは感じた。 ゲームを主目的にする場合は注意が必要だろう。

プリインストールアプリが少ないのがいい

HUAWEI の端末はどれもプリインストールされているアプリが少ない上簡単にアンインストールすることができるのが好印象なのだが、この端末でもしっかりそれが踏襲されていた。 また HUAWEI 独自のランチャーである EMUI は相変わらず Android デフォルトのようなシンプルな見た目で良い。

物理キーをナビゲーションボタンにすることができる

この端末は指紋認証用のセンサーが前面下部に付いているのだが、そのセンサー部分を使用して Android の「戻る」「ホーム」「履歴」のナビゲーションボタンの機能をもたせることができる。 シングルタップで「戻る」、長押しで「ホーム」、左右スワイプで「履歴」といった感じだ。 これが意外と使いやすい。 Android デフォルトだと画面下の領域にこのナビゲーションボタンが配置されてしまうのだが、それが無くなるので表示領域も少し広くなる。

勿論 Android デフォルトのボタン配置にすることもできるので使いやすい方を選べば良い。

音は良いことは良いが過度の期待をしてはいけない

この端末はスピーカーが上下 4 箇所についているのでさまざまなレビューサイトで「音がいい」と紹介されている。 確かにそれは間違いではなかったのだがあくまで「タブレットにしては音がいい」というだけであるので過度の期待は禁物である。 納得できる部分だったのでそこまで不満はない。

値段に対する満足度がそこそこ高い

最後に、この端末は Wi-Fi モデルで 3 万円を切り、LTE モデルでも 3 万 5 千円を切るというミドルレンジモデルの価格帯である。 だが私が今まで使ってきたタブレットは 5 万円若しくはそれ以上するものでも動作に突っかかりを感じたり重量があったりして少なからず不満があったのも確かだ。 このタブレットはこの価格で以前感じたような不満をほとんど感じない。

ただ iOS が許容できるのであれば Wi-Fi モデルであればもう少し出せば iPad 2017 年モデルが買えるのでそちらがいいだろう。 このタブレットはどうしても Android の 10 インチ級の端末が使いたい人向けだと思った。 ゲームなどの処理が重い用途でなければ恐らく十分満足できるだろう。

後日改めてレビューした

2 週間使った感想として後日 MediaPad M3 Lite 10 改めてレビューを書いたのでそちらも参照頂ければ幸いである。

普段であれば楽天スーパーセールまで待つところなのだが、楽天は少し高いのと、早めに買っておけば月末に行く予定の旅行でも使えそうなので結局 LTE モデルを kakaku.com の最安でポチった。 合わせて amazon の方で 1,000 円くらいのケースを購入。 もっと良さそうな 10 インチのモデルが出たら買い換えるつもりなのでケースに入れて綺麗に使っておく (ヤフオクの買取価格が上がる)。 まぁこの手のガジェットは常に処分するときの事を考えて綺麗に使うところではある。

ちなみにデータ SIM であるが LINE モバイルではなくイオンモバイルにした。 月 518 円 (税込) と 22 円安いというのは誤差程度としても、キャンペーンで入会金が 1 円だったのが大きいとみた。 また、イオンモバイルは最低契約期間がなくいつ解約しても違約金が発生しないらしいのもお手軽でいい。

SIM は一週間くらいかかるようなので旅行には間に合わないかもしれないが、別に SIM が無くても Wi-Fi モデルだと思って使えばどうということはない。 この MediaPad M3 Lite 10 は Wi-Fi モデルと LTE モデルの価格差がそれほど無いのが魅力だ。 iPad (2017) だとこうはいかない。

10 インチタブレットという誘惑

私は昔から 10 インチ若しくは 9 インチ級のタブレットを度々変えては使用してきた。 このあたりの系譜は Nexus 9 LTE に書いた通りだ。 ただ、実は Nexus 9 に関しては Mate 9 を購入した際に手放してしまい、しばらくタブレット無しの生活を送っていた。 Mate 9 は 5.9 インチとなかなかの画面サイズなのでタブレット無しでもいけるだろう、という打算もあった。

今年になって HP Spectre x360 という 2-in-1 ノート PC を購入したのだが、購入当初これが「タブレット PC のように (Android や iOS タブレットの代わりとして) 使用できるだろう」と思っていた。 2-in-1 なのでキーボード部分を 360 度折りたたんでタブレットのようにして使用することが出来るからだ。 ただ、この使用方法は 1 回だけ試してみて「これは無いな」と思ってしまった。 本体が 1.3 kg もあるのでタブレットスタイルで持つと全く快適ではないし Windows 10 自体 Android と比べるとタブレットモードがあるとはいえまだまだタッチ操作の最適化が進んでいるとは言い難いところはあった。 あと、ノート PC は綺麗な液晶で見たいというのに慣れてしまっていて、指で触ってしまうと指紋が残ってしまうのが気になるところでもあった。

HUAWEI の 10 インチタブレットミドルレンジモデル

Mate 9 と合わせるわけではないが今回気になったのも HUAWEI の 10.1 インチタブレット MediaPad M3 Lite 10 である。 Lite という名前が付いているが MediaPad M3 10 Plus などという製品はない。 ただ、これの下位に位置するモデルとしては MediaPad T3 10 がある。 だがこの下位モデルの方はスペックも相当低く、液晶も 1280x800 と粗いもので全く購入対象とするには至らなかった。

この MediaPad M3 Lite 10 は Wi-Fi モデルは 3 万円を切るし LTE モデルであったとしても 3 万 5 千円弱という手頃な価格が魅力と言える。 ただその代わりスペックが低めで各種ベンチマークも振るわない結果となっており、数値上は Nexus 9 よりちょっと劣る程度しか出ない端末らしい。 流石にそれはないんじゃないか、と思い 2ch のスレッドなども漁ったが、そもそも Nexus 9 があのベンチマーク結果とは思えないくらい処理が緩慢な端末だったらしい。 私は Nexus 9 を使っていてそんなに不満には思わなかったので、恐らくこの MediaPad M3 Lite 10 でもいけるだろうと踏んだわけだ。 個人的には 5 万円くらい出してももう少し高スペックの端末が良かったのだが、現状そういうものが存在しないので仕方がない。 10 インチ級のタブレットは作ってもあまり売れないのだろう。

ちなみに MediaPad M3 というモデルは 8.4 インチのもっと高スペックのタブレットなのであるが、8.4 インチと少し小さいのが気になったので泣く泣く選択肢から外した。 Mate 9 と釣り合いをとるには 10 インチくらいないと気になるところだ。

使用用途としては Mate 9 で唯一画面が小さすぎて使う気にならなかった Kindle や PDF (技術書) 閲覧、そして Amazon ビデオや d アニメなどの動画、MJ モバイルといったところだ。 このタブレットを購入した場合 Mate 9 には引き続きバイクのナビとして活躍してもらうつもりだ。

Nexus 9 を使用していた時に挿していたデータ SIM は解約してしまったのだが、もし使うとしたら LINE モバイルあたりの 1G プランにするだろう。 月額 540 円ととても安い。 良い時代になったものだ。

素の AsyncTask は冗長過ぎる

例えば「重い処理をワーカースレッドで行い戻ってきた文字列を Toast で表示する」といった処理を AsyncTask で共通化せずに無名クラスで実装すると以下のようになる:

object : AsyncTask<Unit, Unit, String>() {
    override fun doInBackground(vararg p0: Unit?): String {
        Thread.sleep(3000)
        return "めっせーじ"
    }

    override fun onPostExecute(result: String?) {
        Toast.makeText(context, result, Toast.LENGTH_LONG).show()
    }
}.execute()

Kotlin かつちょっとした処理を書こうとしているだけなのにえらく冗長なコードになっている。 この問題点は AsyncTask が 3 つも raw type (ジェネリクスの型パラメーター) を要求する抽象クラスになっていることに加え、Kotlin の場合無名クラスを定義するのに object : (継承元クラス) といった割と仰々しい構文を使わなければならないことだ。

このような無名クラスを使うのではなく、Kotlin のシンプルなラムダ式を渡せばいいようにしたい。

SimpleAsyncTask

以下、内部的には AsyncTask を使用するが各種メソッドの override が必要な時のみラムダ式を渡せばいいように実装してみた:

class SimpleAsyncTask<R>(private val background: () -> R,
        private val pre: () -> Unit = {}, private val post: (R) -> Unit = {},
        private val cancelled: () -> Unit = {}) : AsyncTask<Unit, Unit, R>() {
    override fun onPreExecute() = pre()
    override fun doInBackground(vararg p0: Unit?): R = background()
    override fun onPostExecute(result: R) = post(result)
    override fun onCancelled() = cancelled()
}

AsyncTask が持つ経過表示を行うための機能 onProgressUpdate() はほとんど使用しないのでバッサリ切った。 また、パラメータを AsyncTask().execute(param) のように渡せる機能も共通化しないのであれば必要ないので、今回即時実行するケースのみにターゲットを絞り削った。 これで以下のように使える:

// 一番最初の AsyncTask の例と等価のコード
SimpleAsyncTask(background = {
    Thread.sleep(3000)
    "めっせーじ"
}, post = { Toast.makeText(context, it, Toast.LENGTH_LONG).show() }).execute()

あとすこし改善

かなりシンプルにはなったが AsyncTaskexecute() をコールし忘れて発火されないという凡ミスがよくある。 どうせパラメータを渡さないのだから execute() を書く必要はない。 そこで、以下の関数をトップレベルに配置する:

/**
 * SimpleAsyncTask を使用し非同期で実行する.
 *
 * @param R 非同期実行スレッドの戻り型
 * @param background 非同期実行処理
 * @param pre 非同期実行前処理 (未セットならば処理を行わない)
 * @param post 非同期実行後処理 (未セットならば処理を行わない)
 * @param cancelled 非同期実行キャンセル時処理 (未セットならば処理を行わない)
 */
fun <R> doAsync(background: () -> R, pre: () -> Unit = {}, post: (R) -> Unit = {}, cancelled: () -> Unit = {})
    = SimpleAsyncTask<R>(background, pre, post, cancelled).execute()

これで以下のように書ける:

doAsync(background = {
    Thread.sleep(3000)
    "めっせーじ"
}, post = { Toast.makeText(context, it, Toast.LENGTH_LONG).show() })

Kotlin の型推論が強力なおかげで <R> を明示する必要はない。 とてもシンプルで素晴らしい。

onSaveInstanceState 後に表示しようとするとクラッシュする DialogFragment

DialogFragment は例えば以下のように表示できる:

// MainActivity 内のコード
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    MyDialogFragment().show(supportFragmentManager, null)  // 表示処理
}

class MyDialogFragment : AppCompatDialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog
            = AlertDialog.Builder(activity).setMessage("Hello World").create()
}

通常はこれで問題ないのだが、例えば以下のようにして画面を開いた直後に「スリープ状態にする」「画面を閉じる」など行うとクラッシュする:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // 5 秒待ってから表示. この間にスリープやバックグラウンドに回すとクラッシュ!!
    Handler().postDelayed({ MyDialogFragment().show(supportFragmentManager, null) }, 5000)
}

class MyDialogFragment : AppCompatDialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog
            = AlertDialog.Builder(activity).setMessage("Hello World").create()
}

上記のように数秒待ってからダイアログ表示することなどそうそうないと思われるかもしれないが、昨今は通信を伴ったアプリの開発が当たり前であり通信の結果やエラー表示をダイアログで行っていたりするとよくこのエラーを目にすることとなる。

クラッシュの内容は java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState といった感じで恐らく Fragment を使っている人であればよく見ているのではないかと思う。 要するに Activity や Fragment の onPause() から onResume() 間には Fragment (DialogFragment) を操作できないというわけであるが、これの回避が思ったより難しい。

onPause() から onResume() 間に操作しようとしている DialogFragment の show()dismiss()ouResume() の後に pending するという方法でうまく回避できたのでここに記しておく。

前提

今回は前提として私が Qiita に以前書いた DialogFragment の共通化処理を入れているものとする。 例えば以下のような MyDialogFragment が用意されているものとする:

class MyDialogFragment : AppCompatDialogFragment() {

    /** コールバック */
    private val mCallback: Callback by lazy {
        if (targetFragment is Callback) {
            targetFragment as Callback
        } else if (activity is Callback) {
            activity as Callback
        } else {
            throw UnsupportedOperationException()
        }
    }

    /** リクエストコード */
    private val mRequestCode: Int
        get() = if (arguments.containsKey("request_code")) arguments.getInt("request_code") else targetRequestCode

    /**
     * MyDialog で何か処理が起こった場合にコールバックされるリスナ.
     */
    interface Callback {

        /**
         * MyDialog で positiveButton, NegativeButton, リスト選択など行われた際に呼ばれる.
         *
         * @param requestCode MyDialogFragment 実行時 mRequestCode
         * @param resultCode DialogInterface.BUTTON_(POSI|NEGA)TIVE 若しくはリストの position
         * @param params MyDialogFragment に受渡した引数
         */
        fun onMyDialogSucceeded(requestCode: Int, resultCode: Int, params: Bundle) = Unit

        /**
         * MyDialog がキャンセルされた時に呼ばれる.
         *
         * @param requestCode MyDialogFragment 実行時 mRequestCode
         * @param params MyDialogFragment に受渡した引数
         */
        fun onMyDialogCancelled(requestCode: Int, params: Bundle) = Unit

        /**
         * MyDialog で multiChoice した際に呼ばれる.
         *
         * @param requestCode MyDialogFragment 実行時 mRequestCode
         * @param position チェック位置
         * @param checked チェックされたか否か
         */
        fun onMyDialogMultiChoiceClicked(requestCode: Int, position: Int, checked: Boolean) = Unit

        /**
         * MyDialog で dateSet した際に呼ばれる.
         *
         * @param requestCode MyDialogFragment 実行時 mRequestCode
         * @param year 年
         * @param month 月
         * @param date 日
         */
        fun onMyDialogDateSet(requestCode: Int, year: Int, month: Int, date: Int) = Unit
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val listener = DialogInterface.OnClickListener { _, which ->
            dismiss()
            mCallback.onMyDialogSucceeded(mRequestCode, which, arguments.getBundle("params"))
        }
        val isProgress = arguments.getBoolean("is_progress")
        val title = arguments.getString("title")
        val message = arguments.getString("message")
        val positive = arguments.getString("positive")
        val negative = arguments.getString("negative")
        val items = arguments.getStringArray("items")
        val multiChoiceItems = arguments.getStringArray("multi_choice_items")
        val multiChoiceItemValues = arguments.getBooleanArray("multi_choice_item_values")
        val date = arguments.getSerializable("date") as Date?
        isCancelable = arguments.getBoolean("cancelable")

        if (isProgress) {
            return ProgressDialog(activity).apply { message.let { setMessage(it) } }
        } else if (date != null) {
            val cal = Calendar.getInstance(Locale.getDefault())
            cal.time = date
            val picker = DatePickerDialog(activity, { _, year, month, date ->
                mCallback.onMyDialogDateSet(mRequestCode, year, month, date)
            }, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DATE))
            title?.let { picker.setTitle(it) }
            message?.let { picker.setMessage(it) }
            return picker
        } else {
            val builder = AlertDialog.Builder(activity)
            title?.let { builder.setTitle(it) }
            message?.let { builder.setMessage(it) }
            positive?.let { builder.setPositiveButton(positive, listener) }
            negative?.let { builder.setNegativeButton(negative, listener) }
            items?.let { builder.setItems(items, listener) }
            multiChoiceItems?.let {
                builder.setMultiChoiceItems(multiChoiceItems, multiChoiceItemValues) {
                    _, position, checked ->
                    mCallback.onMyDialogMultiChoiceClicked(mRequestCode, position, checked)
                }
            }
            return builder.create()
        }
    }

    override fun onCancel(dialog: DialogInterface?) {
        mCallback.onMyDialogCancelled(mRequestCode, arguments.getBundle("params") ?: Bundle())
    }
}

通常のタイトルやメッセージを持ったダイアログ表示に加えて、複数のアイテムから 1 つを選択するものや複数チェックできるもの、あと DatePickerDialog での日付選択や ProgressDialog での経過表示も対応している。 これを呼ぶ場合用にもショートカット用のメソッドを以下のように用意する:

/**
 * MyDialogFragment を表示する.
 *
 * @param requestCode リスクエストコード
 * @param params Bundle パラメータ
 * @param title タイトル
 * @param message メッセージ
 * @param positive positiveButton ラベル
 * @param negative negativeButton ラベル
 * @param items シングルタップ選択用の items
 * @param multiChoiceItems 複数選択用の items
 * @param multiChoiceItemValues 複数選択用の items (値)
 * @param date DatePicker 用の初期値
 * @param tag Tag
 * @param cancelable キャンセル出来るか否か
 */
fun showDialog(requestCode: Int = -1, params: Bundle = Bundle(), title: Any? = null,
        message: Any? = null, positive: Any? = null, negative: Any? = null,
        items: Any? = null, multiChoiceItems: Any? = null, multiChoiceItemValues: BooleanArray? = null,
        date: Date? = null, tag: String = "default", cancelable: Boolean = true) {
    fun getStringArray(x: Any): Array<String> = when (x) {
        is Int -> resources.getStringArray(x)
        is ArrayList<*> -> x.toArray(arrayOfNulls(0))
        is Array<*> -> x.map(Any?::toString).toTypedArray()
        else -> throw IllegalArgumentException()
    }
    if (this !is MyDialogFragment.Callback) {
        throw UnsupportedOperationException("Using MyDialogFragment must implement Callback to Fragment")
    }
    val args = Bundle()
    title?.let { args.putString("title", if (it is Int) getString(it) else it.toString()) }
    message?.let { args.putString("message", if (it is Int) getString(it) else it.toString()) }
    items?.let { args.putStringArray("items", getStringArray(it)) }
    multiChoiceItems?.let { args.putStringArray("multi_choice_items", getStringArray(it)) }
    multiChoiceItemValues?.let { args.putBooleanArray("multi_choice_item_values", it) }
    date?.let { args.putSerializable("date", it) }
    positive?.let { args.putString("positive", if (it is Int) getString(it) else it.toString()) }
    negative?.let { args.putString("negative", if (it is Int) getString(it) else it.toString()) }
    args.putBoolean("cancelable", cancelable)
    args.putBundle("params", params)

    MyDialogFragment().apply { arguments = args }.show(supportFragmentManager, tag)
}

ちょっと複雑そうに見えるのは、各種引数をリソース ID と文字列の双方で渡せるように考慮を入れているからである (その為多くの型を Any? にしている)。 このメソッドを使用して前述の MyDialogFragment を show() するには以下のように書く:

// 5 秒待ってから表示
Handler().postDelayed({ showDialog(message = "Hello World") }, 5000)

この時点では onPause() 後の pending の対応が入っていないため先ほどと同じようにスリープにしたり画面を閉じたりすると IllegalStateException がスローされる。

対処

この対処を行う上で残念なのが結局 Fragment を拡張するようなパターンが必要 (BaseFragment のようなもの) になってしまうということだ。 どうしても onResume() と密に結びついてしまうので仕方がない。 これを避けようとすると作成した Activity や Fragment の onResume() の前に毎回お決まりのコードを入れなければならないので若干辛いものがある。

abstract class MyFragment : Fragment(), MyDialogFragment.Callback {

    /** 復帰のために貯めておく PTDialogFragment の表示データ */
    private val mDialogData: MutableList<Bundle> = mutableListOf()

    /** 復帰のために貯めておく dismissDialog のデータ */
    private val mDismissingTags: MutableList<String> = mutableListOf()

    /**
     * ProgressDialog を dismiss する.
     *
     * @param tag Tag
     */
    fun dismissDialog(tag: String = "progress") {
        if (isResumed) {
            fragmentManager.findFragmentByTag(tag)?.let {
                if (it is AppCompatDialogFragment) {
                    it.dismiss()
                } else {
                    throw UnsupportedOperationException()  // DialogFragment ではない
                }
            }
        } else {
            mDismissingTags += tag  // resume でない場合は pending する
        }
    }

    /**
     * ProgressDialog を表示する.
     *
     * @param message メッセージ
     * @param tag Tag
     * @param cancelable キャンセルできるか否か
     */
    fun showProgressDialog(message: Any? = null, tag: String = "progress", cancelable: Boolean = true) {
        val args = Bundle()
        args.putBoolean("is_progress", true)
        message?.let { args.putString("message", if (it is Int) getString(it) else it.toString()) }
        args.putBoolean("cancelable", cancelable)

        // resume でない場合は pending する
        if (isResumed) {
            with(MyDialogFragment()) {
                arguments = args
                setTargetFragment(this@MyFragment, 0)
                show(this@MyFragment.fragmentManager, tag)
            }
        } else {
            // 復帰させるために requestCode, tag を格納する
            args.putString("tag", tag)
            mDialogData += args
        }
    }

    /**
     * PTDialogFragment を表示する.
     *
     * @param requestCode リスクエストコード
     * @param params Bundle パラメータ
     * @param title タイトル
     * @param message メッセージ
     * @param positive positiveButton ラベル
     * @param negative negativeButton ラベル
     * @param items シングルタップ選択用の items
     * @param multiChoiceItems 複数選択用の items
     * @param multiChoiceItemValues 複数選択用の items (値)
     * @param date DatePicker 用の初期値
     * @param tag Tag
     * @param cancelable キャンセル出来るか否か
     */
    fun showDialog(requestCode: Int = -1, params: Bundle = Bundle(), title: Any? = null,
            message: Any? = null, positive: Any? = null, negative: Any? = null,
            items: Any? = null, multiChoiceItems: Any? = null, multiChoiceItemValues: BooleanArray? = null,
            date: Date? = null, tag: String = "default", cancelable: Boolean = true) {
        fun getStringArray(x: Any): Array<String> = when (x) {
            is Int -> context.resources.getStringArray(x)
            is ArrayList<*> -> x.toArray(arrayOfNulls(0))
            is Array<*> -> x.map(Any?::toString).toTypedArray()
            else -> throw IllegalArgumentException()
        }
        if (this !is MyDialogFragment.Callback) {
            throw UnsupportedOperationException("Using PTDialogFragment must implement Callback to Fragment")
        }

        // 既に detach されている場合は何もしない
        if (isDetached) return

        val args = Bundle()
        title?.let { args.putString("title", if (it is Int) getString(it) else it.toString()) }
        message?.let { args.putString("message", if (it is Int) getString(it) else it.toString()) }
        items?.let { args.putStringArray("items", getStringArray(it)) }
        multiChoiceItems?.let { args.putStringArray("multi_choice_items", getStringArray(it)) }
        multiChoiceItemValues?.let { args.putBooleanArray("multi_choice_item_values", it) }
        date?.let { args.putSerializable("date", it) }
        positive?.let { args.putString("positive", if (it is Int) getString(it) else it.toString()) }
        negative?.let { args.putString("negative", if (it is Int) getString(it) else it.toString()) }
        args.putBoolean("cancelable", cancelable)
        args.putBundle("params", params)

        // resume でない場合は pending する
        if (isResumed) {
            with(MyDialogFragment()) {
                arguments = args
                setTargetFragment(this@MyFragment, requestCode)
                show(this@MyFragment.fragmentManager, tag)
            }
        } else {
            // 復帰させるために requestCode, tag を格納する
            args.putInt("request_code", requestCode)
            args.putString("tag", tag)
            mDialogData += args
        }
    }

    override fun onResume() {
        super.onResume()
        mDialogData.forEach {
            with(MyDialogFragment()) {
                arguments = it
                setTargetFragment(this@MyFragment, it.getInt("request_code"))
                show(this@MyFragment.fragmentManager, it.getString("tag"))
            }
        }
        mDialogData.clear()
        mDismissingTags.forEach { dismissDialog(it) }
        mDismissingTags.clear()
    }
}

こんな MyFragment を用意する。 そうして以下のように MyFragment を継承した Fragment 上で MyDialogFragment を表示してみる:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    savedInstanceState ?: supportFragmentManager.beginTransaction()
            .add(android.R.id.content, AFragment())
            .commit()
}

class AFragment : MyFragment() {
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        
        // 5 秒後表示. 今度はスリープしてもクラッシュせずに復帰時にちゃんと Dialog が表示される!!
        Handler().postDelayed({ showDialog(message = "Hello World") }, 5000)
    }
}

今度はクラッシュしないし、画面復帰時に正しく表示しようとしていた Dialog が表示される。 肝は Dialog を表示しようとする時に isResumed でなかったら tag で表示しようとしている Dialog の情報を保存し onResume() 後に改めて表示するというところだ。 これを共通ダイアログに対して行っておくとこのようなボイラープレートコードをあちこちに仕込まなくて済むようになるので有用である。

CakePHP 3 ではアソシエーションを設定し関連データを取得することが出来る。 CakePHP 2 以前もあった仕組みであるが hasMany 若しくは belongsToMany で関連付けたデータを contain にして where で絞り込もうとすると失敗する:

$this->find()
    ->contain(['Comments'])  // Post has many comments とする
    ->where(['Comments.type' => 1]);  // 指定できそうに見えるが失敗!!

この PostbelongsTo の場合は内部的に INNER JOIN 若しくは OUTER JOIN された SQL が発行されるので大丈夫なのだが hasMany 若しくは belongsToMany複数の SQL に分割して発行されるので、最初の SQL に Comments が存在せず、条件を指定してもそれが最初の SQL に対し指定されてしまい「存在しないカラムに対し条件を指定している」ことになり失敗してしまう:

# 内部的にこのように分けて発行されるので最初の SQL には Comments は存在しない!!
SELECT * FROM articles;
SELECT * FROM comments WHERE article_id IN (1, 2, 3, 4, 5);

これを避けるために contain に対し以下のようにクロージャを渡すことにより 2 つ目の SQL に対し条件を渡すことが出来る:

$query = $articles->find()->contain([
    'Comments' => function ($q) {
       return $q->where(['Comments.type' => 1]);
    }
]);

つまり contain している Entity のアソシエーションがどの関係であるかを思い浮かべて適切なコードを書かなければならない。 この手の O/R マッパーを使用する際はしばしば内部で吐かれる SQL を dump しながら挙動を確認しなければならないのが辛い所ではある。

最近会社での Android アプリの実装にも Kotlin を使いだしてというもの、記述力がとても高いので気にいってはいる。 特にエクステンションを用いた既存クラスの拡張はとても柔軟性が高く Java だと全てユーティリティメソッドに切っていたものがほとんどエクステンションになり明らかに記述量が減った。 後はトップレベルに関数を定義でき、どこからでもその参照をインポートすれば利用できるのも便利だ。 私はよく使う Bundle を簡単に作成できるような関数をトップレベルに定義して使用している。

ただ、正直もう少し何とかなったのではないか、若しくは Java よりも記述量が増えてしまって微妙に思えるところもあるのも事実だった。 以下 Kotlin を書く方ならご存知の事だと思うが列挙する。

Java のクラスインスタンスを取得する記法

Java のクラスインスタンス、いわゆる Class<T> を取得する記法は Java だと以下のような感じだ:

Hoge.class  // static に取得する場合
hoge.getClass()  // オブジェクトから取得する場合

一方 Kotlin は以下だ:

Hoge::class.java
hoge::class.java

正直見にくい。 そして Android アプリ開発では結構出てくる。

Intent(context, HogeActivity::class.java)

みたいな感じである。 ただ、これを長いから短く書けるようなショートカットを用意すればいいではないか、と思ってやってみたができない。 Kotlin のジェネリクスの型情報も Java と同じくコンパイル後に削除され普通の Object としてしか認識できないので T を受け取って Class<T> を吐くようなメソッドは定義できない (多分)。

配列初期化の記法

Java だと以下のような感じで、変数初期化の際に代入する場合に限り配列のリテラル表記が使用できる:

final String[] arrays = {"hoge", "fuga"};

だが Kotlin には配列のリテラル表記はなく { ... } の役割は代わりにラムダ式 (関数オブジェクト) で使用するようになっている。 Kotlin では arrayOf()intArrayOf() などといった関数を使用する:

val arrays = arrayOf("hoge", "fuga")
val intArrays = intArrayOf(1, 2, 3)

この intArrayOf()booleanArrayOf() などといった感じに Java のプリミティブ型に対応する配列を生成するようのメソッドが別々に用意されているあたりに苦しさを感じる。 とはいえ Java との相互運用性を考えると int[]Integer[] は区別できなければならず、内部的にこうなってしまうのは仕方がない部分かもしれない。

日本固有の悩みだろうか

Android で Google Map を配置したアプリを作成する場合は Google Maps Android API を使用することになると思うが、現状 Google Map 内に表示される文字サイズを変更するような API は用意されていないし Android 端末の設定の文字サイズを変更してもアプリ内の文字サイズは変わる (但しアプリ側が行儀よく sp 単位でフォントサイズ指定されている場合) のだが、Google Map 内の文字サイズは残念ながら連動しない。

老眼、つまり高齢化は先進国における世界的な流れだと思うのだが、何故 Google Maps Android API にそのような考慮がなされていないのか。 恐らく欧米はラテン文字を使用しているためフォントサイズが比較的小さくても視認性が高くそういった問題は余り出てこないのではないだろうか。 その点漢字は不利である。

高齢者向けのアプリを開発する場合どうしても「文字サイズを大きくする」というところを考慮しなければならない。 API が用意されていないので、逆に地図自体を大きくするのはどうかと考えた。 少しボヤケた感じにはなるが、意外と悪くないアプローチなのでここに書き残しておく。

まず地図自体を大きくする

SupportMapFragment のインスタンスを取得しそれに紐づく View に対し setScaleXsetScaleY を呼べば簡単に変更できる。 例えば 2 倍にする場合は以下でよい:

mMapFragment.view?.scaleX = 2.0f
mMapFragment.view?.scaleY = 2.0f

だがこれを愚直に行うと以下の問題が発生する:

  • Google ロゴや現在位置ボタン、ズームイン・アウトボタンが画面の領域外に見切れてしまう
  • ボタン類や配置したオブジェクト (ピン、吹き出しなど) も 2 倍になってしまう

ロゴやボタンが見切れないようにする

幸い GoogleMap#setPadding というメソッドで Google Map に対しパディングを設定できる。 パディングは端末の縦横ピクセルを取得し適切な値を設定しなければならない。 Android アプリ開発ではよくあるパターンではあるが View のサイズ取得はビューツリー構築後でないと計算されておらず 0 になってしまうところに注意する。 このように ViewTreeObserver#addOnGlobalLayoutListener を使うのがイディオムだ:

mMapFragment.view?.viewTreeObserver?.addOnGlobalLayoutListener {
    view?.let { view ->
        mMapFragment.getMapAsync { map ->
            // 2 倍だけでなく 1.5 倍などもサポートする場合もう少し工夫する必要あり
            val (width, height) = view.width to view.height
            val paddingLeft = (width / 4).toInt()
            val paddingTop = (height / 4).toInt()
            map.setPadding(paddingLeft, paddingTop, paddingLeft, paddingTop)
        }
    }
}

2 倍になってしまうボタンを使わずに自作する

2 倍になってしまう Google ロゴは仕方ないとして、ボタン類が 2 倍になっているのはどうにも不格好だ。 まずデフォルトのボタン類を以下で非表示にする:

// map は GoogleMap インスタンス (getMapAsync() での取得後)
map.uiSettings.isZoomControlsEnabled = false
map.uiSettings.isMyLocationButtonEnabled = false

続いて自作のアイコンを配置するわけだが、アイコン画像は Google Material Icons に自由に使えるアイコンが配布されているので、これを引っ張ってくれば良い。 そして GoogleMap#getMapAsync()GoogleMap インスタンスを取得できたタイミングで以下のような感じでイベントをセットすればよい:

myLocation.setOnClickListener {
    // TODO 本当は myLocation は deprecated なので FusedLocationApi を使用したものに置き換えるのがよい
    val latLng = LatLng(map.myLocation.latitude, map.myLocation.longitude)
    map.animateCamera(CameraUpdateFactory.newLatLng(latLng))
}
zoomIn.setOnClickListener { map.animateCamera(CameraUpdateFactory.zoomIn()) }
zoomOut.setOnClickListener { map.animateCamera(CameraUpdateFactory.zoomOut()) }

Kotlin スタートブックを引っ張り出してきて Kotlin のお勉強をしていた。 とはいえ Kotlin に関しては自分が過去に一人読書会して疑問点をまとめていたので、それを読み返すだけでも大分思い出せた。 やはり重要な部分は Blog にまとめておくと後で辿るのが容易なので便利だ。

非同期での DialogFragmentshow()/dismiss() が鬼門 (IllegalArgumentException がスローされる) なので、その辺りの「キレイな対策」を考えたい。 onSavedInstanceState() 後だったら DialogFragment を処理するタイミングを onResume() 後にずらす、といった感じの実装が Kotlin のトレイト……ではなくインターフェースでキレイに実装できるかといったところだ。

Java だとどうしても BaseFragment などの基底クラスを作成するという原始的な方法しか取れず、好ましくない。 Java 8 から実装を持てるインターフェースがあるが今のところ Android の Java 実装では使えないのも痛い……。

2 ヶ月半ほど使用

HUAWEI のフラッグシップモデル、5.9 インチ Android スマートフォンの Mate 9 を去年購入した。 最初品薄でなかなか手に入らなかったのだが、運良く年が変わる前に手に入れることができ、今まで 2 ヶ月半ほど使用してきた。 感想をここにちょっとしたレビューとして書き残しておく。

ちなみに私は Android アプリケーションプログラマではあるが、特にガジェットマニアというわけではなく、スマートフォンはこれ 1 台しか持っていないしタブレットも今は所有していない。 なのでコアなレビューは出来ない。 そういったものをお望みの方はネット上で多くの方がレビューしているのでそちらを参照頂きたい。

比較対象は LG の Nexus 5X である。

画面はデカイがベゼルが狭いので使い勝手が良い

5.9 インチの画面サイズというともうファブレットのサイズである為普段大きすぎるのではないか、と思われるかもしれないがベゼル (枠) が狭いので意外と大きさは感じない。 私はズボンの前ポケットに入れることが良くあるが、5.2 インチの Nexus 5X と比べてもそこまで圧迫感を感じない。 むしろ Nexus 5X でも前ポケットは厳しい時があったので、昨今の大画面のスマートフォンでは別途バッグに入れるなどして対処した方がいいかもしれない。

この画面サイズだとゲームが非常に気持ちよくプレイできるので、ゲーム重視の方はお勧めである。 私は MJ モバイル (麻雀) や将棋ウォーズをよくプレイしているが、駒や牌がとても掴みやすくなった。

電池持ちが非常に良い

私はバイクに取り付けてナビとして使用しているが、バイクから USB 給電などしていないので Nexus 5X だとどんどん電池が減ってしまいモバイルバッテリー必須の上充電速度も遅く困っていたのだが、Mate 9 にしたら電池の減るスピードが半分近くに減った。 これは自分にとって非常に重要なポイントで気に入っている。

Mate 9 のウリである「超急速充電」が出来るのは純正のアダプタだけらしいので、普通のモバイルバッテリーだと充電速度が遅いのだが全然気にならない。

スピーカーの最大音量は結構大きく出来る

バイクに乗っている時に音量を最大にしても Nexus 5X だとちょっと音が小さかったのだが、Mate 9 はもっと大きく出来るのでナビの音声が聞き取りやすい。

プリインストールされている HUAWEI のアプリがどれもシンプル

DoCoMo や au で購入したスマートフォンの場合、大抵プリインストールされているアプリがクールでない上に動作が緩慢だったりして辟易するものだが、HUAWEI がプリインストールしているアプリはどれも「そのまま使いたくなる」ものばかりで好感が持てる。 例えば「カレンダー」「ミラー」「時計」「電卓」「連絡先」といった感じなのだが、どれも日本メーカー特有の「芋臭さ」が全くない。 前はプリインストールアプリがうるさくない Android 端末を使いたければ Pixel (Nexus) を買わなければいけないのかと思っていたがそんなことはなかったわけだ。

カメラ性能は素晴らしい

私は自撮りも滅多にしないしカメラに拘る方ではないのだが、それでもこの機種のカメラは素晴らしいと分かる。 撮影した画像はこの Blog に幾つか載せてあるので参照頂きたいが、ウォーターマーク (HUAWEI Mate 9 LEICA DUAL CAMERA といった文字列) を入れることができ、これもなかなか格好いい。 勿論ウォーターマーク無しでも撮影できる。

カラーバリエーションは最近ブラックが用意された

発売当時は白と金しか無く、消去法で白を選択したのだが、本当はベゼルが黒い方が好きなので黒があればそちらが良かった。 今は販売されているようなのでちょっと悔しい。

高級感漂う

この端末は HUAWEI のフラッグシップモデルらしいが、破格の 6 万弱で手に入った。 フラッグシップモデルの名に恥じぬ造形だし、チープさは微塵も感じない。

総括

自分としては弱点らしき弱点は特に見つからない、かなりの良端末といえる。 今まで持ったスマートフォンの中でナンバーワンといえる。

実は HUAWEI の端末は初めてではなく P9lite という安い端末を使っていたことがあるのだが、それも値段に見合わず質感が良く好感が持てる端末だった。 但し動作は値段なりに緩慢な感じだったのは仕方がないところか。 Mate 9 の方は勿論サクサク動作するので心配はいらない。

普段は Vagrant 上に Composer をインストールしていた

昨今の PHP 開発では Composer は必須であるが私はいつも Vagrant 上の VM に Composer をインストールしており vagrant ssh した上で composer install / update するなどしていた。 それでも勿論良いのだが PhpStorm 上に Vagrant や Composer のメニューが有り PhpStorm から出ずに vagrant up から vagrant ssh していろいろやったり composer update したりが可能になっている。 このメニューを使用するには PhpStorm 側にローカルの Composer の実行パスを教える必要がある。 つまりローカルに Composer をインストールする必要があった。 そして Composer を使うには PHP が必要となるので PHP をインストールして Composer をインストールする手順を記す。

macOS の場合はほぼ Linux の場合と同じなのでやりやすいが Windows の場合は意外と困る。 あと PHP をインストールするのは Xampp を使うと楽なのだが余計な Apache や MariaDB はインストールしたくないので素の PHP をインストールすることとする。

PHP インストール

Windows 用の PHP バイナリをダウンロードするサイトがあるのでここからダウンロードする。 バージョンは使いたいものでいいと思うが、現時点での最新は PHP 7.1 なのでそれをダウンロード。 Non Thread Safe か Thread Safe のどちらがいいのかというところだが Apache を使う場合は Thread Safe にせよとの事らしい。 コマンドラインから使うだけの分にはどちらでも良さそうだ。

ダウンロードした zip を適当な位置に解凍する。 自分は C:\php71 にした。

その後コマンドラインから実行する為に PATH を通す。 よくある手順だがコントロールパネルの「システムの詳細設定」からの「環境変数」を押下。 自分だけに適用したい場合は「ユーザー環境変数」、ユーザ全員に適用したい場合は「システム環境変数」の PATH を選択する。 そこに先程 PHP をコピーしたディレクトリ C:\php71 を追記する。

php.ini

さて、用意した PHP にはまだ php.ini が無い。 だが元となる php.ini-development が置いてあるので、それをコピーして php.ini にリネームする。 ちなみに本番環境では php.ini-production を使うが、今回の場合は実行時にエラーメッセージが表示される development の方がいい。

そして php.ini の以下の場所のコメントアウトを外す (; を削除する):

; Windows だと ext ディレクトリに DLL が全て入っている
extension_dir = "ext"

; CakePHP 3 インストールに必要
extension=php_intl.dll
extension=php_mbstring.dll

; Composer インストールに必要
extension=php_openssl.dll

コマンドプロンプトを開き php -v などと叩き、正しく PHP バージョンが表示されることを確認する。

Composer

Composer のダウンロードページComposer-Setup.exe をダウンロードして実行する。 ここでローカル PHP のパスを聞かれるので、先程インストールしたものを教える。

コマンドプロンプトを開き composer と叩き正しく Composer のコマンドリストが表示されるのを確認する。

インストールが完了すると Composer が C:\ProgramData\ComposerSetup\bin\composer に入る (ProgramData は隠しフォルダなのでエクスプローラで普通にたどると見えないので注意)。 後は PhpStorm 側でこのパスを教えるなどすれば良い。

Vagrant

Vagrant が何かに関しては検索すれば幾らでも情報が出てくるのでここでは述べない。

まずは Vagrant をインストールする。 公式からダウンロードしてインストールすれば良い。 VirtualBox も入っていなければインストールしておく。

Ubuntu 16.04 LTS に関しては公式に box が提供されているのでそれを使用する:

vagrant init ubuntu/xenial64

これでカレントディレクトリに VagrantFile が出来るので、この中の以下の行のコメントアウトを外す:

" VM 上の 80 番ポートへのアクセスをホスト側の 8080 番ポートに変換する設定. 8080 が使用済なら適宜変更する
config.vm.network "forwarded_port", guest: 80, host: 8080

その後以下のコマンドを叩いて VM を起動する:

vagrant up --provider virtualbox

以下のコマンドで VM に SSH 接続する:

vagrant ssh

試しに Apache を導入し動作確認を行う:

sudo apt install apache2

http://localhost:8080/ にアクセスし、正しく Ubuntu の Apache テストページが表示されるのを確認する。

PHP 7.0

PHP 7.0 の環境を構築する。 Ubuntu 16.04 における PHP 7.0 関連の項目を検索するには以下のコマンドを叩く:

apt search php7.0

まとめて必要そうなのを入れてしまう:

sudo apt install libapache2-mod-php7.0 php7.0 php7.0-cli php7.0-intl php7.0-json php7.0-mbstring php7.0-sqlite3
/* sudo apt install php7.0-mysql */
/* sudo apt install php7.0-pgsql */

デフォルトの DocumentRoot が /var/www/html なのでそこに試しに PHP ファイルを置いてみる:

cd /var/www/html
sudo mv index.html index.html.old
sudo vi index.php

index.php の内容は以下とする:

<?php
phpinfo();

http://localhost:8080/ にアクセスし、正しく phpinfo が表示されるのを確認する。

DocumentRoot を /vagrant/xxx にする

デフォルトの DocumentRoot のままだとホスト側に /vagrant がマウントされる仕組みを活かすことが出来ないので変更する。 /vagrant/xxx だが xxx の部分は各自適当なプロジェクト名とする:

 sudo vi /etc/apache2/sites-available/000-default.conf

以下 DocumentRoot を編集しアクセス許可を与える:

DocumentRoot /vagrant/xxx
<Directory /vagrant/xxx>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
</Directory>

テスト用のページを用意しておく。 先程の phpinfo 再利用でよいだろう:

mkdir /vagrant/xxx
sudo mv /var/www/html/index.php /vagrant/xxx
sudo service apache2 restart

http://localhost:8080/ にアクセスし、正しく phpinfo が表示されるのを確認した上で DOCUMENT_ROOT/vagrant/xxx になっているのを確認する。

mod_rewrite 有効化

CakePHP 3 では mod_rewrite を使用しているが Ubuntu の Apache のデフォルトでは有効になっていないので以下で有効にしておく:

sudo a2enmod rewrite
sudo service apache2 restart

CakePHP 3 インストール

先程の index.php は不要なので消しておく:

rm index.php

CakePHP 3 のインストールに関しては公式のドキュメントが素晴らしいのでこれに従っておけば問題ない。 ただ composer create-projectzipunzip が必要なようなので以下で入れておく:

sudo apt install zip unzip

あと composer create-project で出来るプロジェクトがディレクトリに含まれているので (この例だと /vagrant/xxx のこと) /vagrant 直下で composer create-project するのがいい。

尚 DB ばデフォルトで MySQL を使用するように config/app.php に書かれているので例えば SQLite を使うつもりでそのまま http://localhost:8080 にアクセスしても「MySQL のドライバーが見つからない」といったエラーになってしまう。 これに関しては config/app.php を以下のように SQLite 用に直せば良い:

'Datasources' => [
    'default' => [
        'className' => 'Cake\Database\Connection',
        'driver' => 'Cake\Database\Driver\Sqlite',  // Sqlite にする
        'persistent' => false,
        'host' => 'localhost',
        'username' => '',  // 空にする
        'password' => '',  // 空にする
        'database' => 'xxx.sqlite',  // 適当な SQLite ファイル名を書く
    ... (省略) ...
    'test' => [
        'className' => 'Cake\Database\Connection',
        'driver' => 'Cake\Database\Driver\Sqlite',  // こっちも Sqlite にしないと駄目
        'persistent' => false,
        'host' => 'localhost',
        'username' => '',  // 空にする
        'password' => '',  // 空にする
        'database' => 'test.sqlite',  // 適当な SQLite ファイル名を書く
    ... (省略) ...

これで http://localhost:8080 にアクセスしカラフルな Get the Ovens Ready ページが表示されれば開発環境構築完了である。

macOS Sierra で Vagrant をインストールするまではいいのだが、

$ vagrant box add xenial64 https://atlas.hashicorp.com/ubuntu/boxes/xenial64/versions/20160521.0.0/providers/virtualbox.box
==> box: Box file was not detected as metadata. Adding it directly...
==> box: Adding box 'xenial64' (v0) for provider: 
    box: Downloading: https://atlas.hashicorp.com/ubuntu/boxes/xenial64/versions/20160521.0.0/providers/virtualbox.box
An error occurred while downloading the remote file. The error message, if any, is reproduced below. Please fix this error and try again.

などといってダウンロードできない。 ネットワークが悪いのかと思ってブラウザでアクセスしてみるも正しく box ファイルがダウンロードできた。

teratail で似たような話があり、その回答で解決できた。 どうも Vagrant に同梱されている cURL がうまく動かない模様なので、

sudo rm /opt/vagrant/embedded/bin/curl

で無事 vagrant box add できた。

今更なのだが Nexus 5X のレビューを書いてみる。 私はこの前身である Nexus 5 を使用していたこともあるのだが Nexus 5 は当時としてはコンパクトな 5 インチスマートフォンであり Nexus ブランドならではのプリインアプリ一切無しの上に挙動も早く重宝していたので 5X も購入したのだが、正直期待外れだった部分もあった。

最近使っていて一番イライラするのは MJ モバイルやちょっとしたゲームなどの挙動が明らかにもたつくことだ。 Nexus 9 に比べて処理がワンテンポ遅い。 普通画面が大きくて古い Nexus 9 の方が負けるものだと思うのだが……。

いい部分も書くと、この端末では背面に指紋認証センサーが付いており、スリープ状態でここに人差し指を当てることで即座にロック解除できるのだが便利。 これは今後日本で販売されるであろう Pixel (XL) にも踏襲されている。

電池持ちは普通で良くも悪くもない印象。 ただ急速充電できるのは便利。

最近は価格も下がってきているので手軽にプリインアプリ無しの SIM フリースマホを持ちたい方はいいかもしれない。 ただ今だとコスパで選ぶなら Huawei P9lite がいい気がする。

見やすさは yyyy 年 M 月 d 日形式が最高だが

今までコジオニルク (この Blog) では日付を示す形式として 2016 年 10 月 3 日10 月 3 日 などと書いていた。 日本人なら誰でも分かるし、何より読みやすい。 しかし最近 Nexus 5X で自分のページを見ていて気づいたことがあった。 日付の部分が妙に改行されてタイトル一覧が見辛い。

画面サイズが小さいスマホも視野に入れると

スマホと PC の両方でこのページを見ている皆さんならお気づきと思うが、コジオニルクは画面の広さ (というより横方向の有効ピクセル数) に従い自動的にレイアウトを最適化して表示するようになっている。 例えばギャラリーだと横方向のピクセル数によって画像を横方向に幾つ並べるかを変更するなどしている。 こういった画面サイズに応じて最適化する技術を Web の世界ではレスポンシブ Web デザインなどと呼んだりする。 Bootstrap ライブラリに標準でレスポンシブ対応する為の機能が付いているのでそれを活用している。

なので当初はこれで良いと思っており、自分自身はスマホで見ることが殆ど無かったので気付かなかった。 「2016 年 10 月 3 日」だとやたらと長いし好きな位置での改行を許してしまう。 とすると「2016/10/3」や「2016-10-3」が思いつくがどちらが良いのだろうか。

2016-10-03 は改行を許してしまう

日付表記に関して Wikipedia には以下のように書かれていた:

日付で、算用数字で表した月日または年月日を分ける。ただし、順序は国によって異なる。
2004/1/31 (1/31) - 日本、中国、韓国など
2004-01-31 (-01-31) - ISO 8601(参考)

スラッシュ区切りは広く知られている方法でありハイフン区切りは ISO 8601 で定義されている。 つまりどちらでもその時に合ったものを使えばいいように見える。

両方タイトル一覧の表示で試してみた。 2016/10/3 は改行されず、2016-10-03 はハイフンの後に改行されてしまった。 私はこの場合改行して欲しく無かったので 2016/10/3 の方を採用した。

但し Wikipedia に

算用数字で表した月日または年月日を分ける

と記載されているので年月のみ分ける場合はどうなのだろうと疑問を持った。 2016/3 などという記法だが公式にあるのかが分からない。

そこで年月日は 2016 年 3 月などと書いて月日または年月日は 3/15, 2016/3/15 などと書くように統一してみた。 日記のタイトルで「10 月 3 日」などと書いていたのが長くてタイトルが 2 行になっていたものが 1 行になるようになり多少見やすくなった。

今の時代 PC で閲覧する事のみを想定できない

最近の若者はスマホのみを使って PC を全く触らないので逆に私達の世代より PC の操作が覚束なかったりするらしい。 現にコジオニルクもスマホから参照されることが多いので、少しずつスマホに寄せていくように微調整していきたいところだ。

ウィジェット部分は旧アプリのものをほぼそのまま移植したコジごみカレンダーだったが、何故かウィジェットの配置するセル数が想定しているサイズより大きくなってしまうという問題が起きていた。 コジごみカレンダーは「セル縦 1 × 横 2」サイズのものを想定していたが、何故か Nexus 5X で配備しようとすると「2 × 3」になってしまう。

私は業務の Android 開発ではウィジェットを使用するという事がほぼ皆無だったので昨今のウィジェット開発事情に関して全く無知であった。 これに関しては株式会社ノーティス様の記事が非常に参考になった。

targetSdkVersion = 14 (Android 4.0) 以上の場合ウィジェットの 1 セルあたりの計算式が従来と異なるというのが原因で、旧コジごみカレンダーは Android 2.2 向けということで targetSdkVersion = 8 であったが今回の新アプリに関しては Android Nougat (Android N) に設定してあった。 まぁ今時 targetSdkVersion に過去のものを指定するというのは良くないのでウィジェットの計算式を新方式にして解決した。

株式会社ノーティス様の記事にも記載があるが、新旧仕様での n セルの dp 量は以下で計算出来る:

(targetSdkVersion 14 未満) 74 n - 2
(targetSdkVersion 14 以上)
70
n - 30

例えば 1 × 2 で配置しようとすると旧仕様だと 72 × 146 (dp) だが新仕様だと 40 × 110 (dp) となる。全然違う。

かなり手間をかけた全面刷新

コジごみカレンダー自体は 5 年近く前に初版をリリースした歴史ある Android アプリなのだが、アプリの構造が Android 2.2 時代のもののままなので更新するにも何かと制約がありモチベーションが保てなくなっていた。 なので現代の技術で全面リプレースするという構想は実は 2, 3 年前から持っていたのだが、既にかなり複雑なアプリに仕上がってしまっていたので移植しようとすると全ての機能を網羅しなければならず、なかなか最後まで続かない結果になっていた。 作っては中断、作ってはまた中断を繰り返しており、開発は頓挫状態となっていた。

Kotlin に感謝

では何故今回モチベーションが保てたのかというと間違いなく Kotlin のお陰だ。 既存の冗長な Java コードを Kotlin で書き換えるのが非常に気持ちよく、地味な移植作業を続けるのがそれ程苦ではなくなっていた。

今回 In-app Billing の既存のコード以外全て Kotlin で書き直している。 業務で Kotlin を使うのは流石に憚られたが、自分のアプリなら自由に使えるのでなかなか楽しい。

ただ 「Android 開発経験がそれほどない技術者は素直に Java で書いたほうがいい」と思った。 所々 Kotlin の文法ミスなのか Android SDK の API の使用方法が間違っているのか判断がつかない場面があったので、恐らく経験が浅いと解決できないのではないかと思われたからだ。

これが特に良かったという点

実際にコジごみカレンダーのコードを一部引用し述べる。

Fragment で Activity のインスタンスを使用する場合コンストラクタ等で行おうとするとまだ Activity に attach() されてないので getActivity()null になる。 これを避ける為には Fragment#onAttach() で Activity のインスタンスを参照するか Fragment#onActivityCreated()getActivity() するのが普通なのだが、そうすると以下のように退屈な代入を書かなければならない:

private ContentResolver mResolver;  // ここで代入できないから final にできない!!
...
@Override
public void onActivityCreated(final Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    mResolver = getActivity().getContentResolver();
}

Kotlin なら by lazy { ... } を使えば実際に使用する箇所でコードブロックが走る (遅延実行) ので大丈夫だ:

// mResolver を onActivityCreated() 以降でしか参照しないならこれで問題ない
private val mResolver: ContentResolver by lazy { activity.contentResolver }

多数の分岐を持った変数代入を Java でやると以下だ:

// 読みやすいけど退屈
final int backgroundColor;
if (year == currentYear && month == currentMonth && date == currentDate) {
    backgroundColor = getResources().getColor(R.color.calendar_today);
} else if (targetMonth != month) {
    backgroundColor = getResources().getColor(R.color.super_light_gray);
} else if (j == 0 || dayType.isHoliday(cal) || dayType.isExtra(cal)) {
    backgroundColor = getResources().getColor(R.color.calendar_sunday);
} else if (j == 6) {
    backgroundColor = getResources().getColor(R.color.calendar_saturday);
} else {
    backgroundColor = Color.WHITE;
}
columnLayout.setBackgroundColor(backgroundColor);

Kotlin だと when 文があるのでかなり簡潔になる:

columnLayout.setBackgroundColor(when {
    year == currentYear && month == currentMonth && date == currentDate -> resources.getColor(R.color.calendar_today)
    targetMonth != month -> resources.getColor(R.color.calendar_not_target_month)
    j == 0 || Utils.isHoliday(cal) || Utils.isExtra(cal) -> resources.getColor(R.color.calendar_sunday)
    j == 6 -> resources.getColor(R.color.calendar_saturday)
    else -> Color.WHITE
})

後は拡張メソッド (エクステンション) やトップレベル関数、インライン関数やラムダ式なども勿論便利だった。 もうプライベートで行うコーディングは Java で行う気がしない。

ちょっと前までは AppCompatActivity を使用した場合に PreferenceFragment が Support Library の Fragment を継承していないのでそのまま使えず歯がゆい思いをしていたが Android Support Library の Fragment を継承した PreferenceFragmentCompat が登場しその問題が解決された。 しかし古いアプリを移植しようとして RingtonePreference (通知音選択) がそのまま使えない事に気付いた。 SwitchPreferece には SwitchPreferenceCompat が用意されているのに RingtonePreference にはそういったものがない。

仕方が無いので普通の Preference で自分で実装することにした。

呼び出し側

暗黙的インテントを投げることで同機能を実装できる:

    mNotificationSound.setOnPreferenceClickListener {
        val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER)
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION)
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true)
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI)

        val existingValue = pref().getString("setting.notificationSound", null)

        // Uri として null を指定すると通知音「なし」扱いになることに注意
        val uri: Uri? = when {
            existingValue == null -> Settings.System.DEFAULT_NOTIFICATION_URI
            existingValue.length > 0 -> Uri.parse(existingValue)
            else -> null
        }
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, uri)
        startActivityForResult(intent, 0)
        true
    }

「Preference の設定がまだされていない場合」「通知音がなし (サイレント) の場合」「通知音が設定されている場合」の 3 通りあることに注意。 その場合分けは上のコードの when 文に示されている。

onActivityResult() 側

暗黙的インテントの戻り値を受け取る為に onActivityResult() も実装する:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode === 0 && data != null) {
        val ringtone = data.getParcelableExtra<Parcelable?>(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
        pref().edit().putString("setting.notificationSound", ringtone?.let { it.toString() } ?: "").apply()
    } else {
        super.onActivityResult(requestCode, resultCode, data)
    }
}

こちらは通知音がなしの場合は null で渡ってくるので Preference に空文字で設定しておく。 空文字で無くても null でさえなければ何でも構わない。 null だと未設定との区別がつかなくなる。

ちなみに上記 val ringtone には content://media/internal/audio/media/22 といった感じの CONTENT_URI 表現が入ってくる。 実際に Notification を鳴らす際にこのまま使うことができる。

ConoHa でようやく Ubuntu 16.04 LTS のイメージが用意されたので早速我がコジオニルクも CentOS 7.0 から移行してみた。 仕事で CentOS を恒常的に使用しているというのなら別だが、そうでなければ Ubuntu の方が何かとパッケージも新しいし使い勝手が良い。

ただ Ubuntu 公式から ISO をダウンロードして VM にインストールした場合と初期状態が少し異なる:

  • root ユーザしかいない
  • ロケールが英語

一般ユーザ作成

ConoHa の Web 上のコンソールからログインする。 root ユーザでパスワードは登録時のものを入力する。

ログインできたら、まず一般ユーザを追加する。 hoge ユーザを登録するとすると以下となる:

adduser hoge
gpasswd -a hoge sudo  # sudo 権限追加

SSH 設定

ConoHa の Ubuntu イメージは openssh-server は最初から入っているのでその設定をする。

vi /etc/ssh/sshd_config

Port 99999  # 使われていないポートなら何でも良いがデフォルトの 22 だと攻撃を受けるので変更しておく
PermitRootLogin no  # root で SSH ログインできるようになっていると非常に危険なので
PasswordAuthentication yes  # パスワード認証を使わない場合は OFF
GSSAPIAuthentication no  # GSSAPI を使うなら yes だが使わないなら no で (SSH 接続が遅くなる場合がある)
AllowUsers hoge  # SSH 接続を許可するユーザを制限する事でセキュリティを確保

service ssh restart

ConoHa コンソールを閉じ Putty などの SSH クライアントや各種コンソールから SSH を試みて正しく接続できることを確認する。

ホスト名

何故か ConoHa でインストールした直後は /etc/hosts/etc/hostname の値が異なっている為 sudo で何かする度に unresolved host などと怒られる。 例えばホスト名を conoha とする場合は以下となる:

sudo vi /etc/hosts

127.0.1.1 conoha

sudo vi /etc/hostname

conoha

その後再起動 sudo reboot し正しくホスト名が適用されている事を確認する。

日本語化

英語のままでも別に問題は無いが日本人として日本語の方が分かりやすいのは確かである。 Server World 様の記事 をそのまま実施すれば良い。

第 10 章 インタフェース

インターフェース (ミックスイン) の説明は大体想像通りなのだが、クラスデリゲーションの仕組みだけなるほどと思った:

class GreeterWithRecording(private val greeter: Greeter) : Greeter by greeter {
    private val _targets: MutableSet<String> = mutableSetOf()
    val targets: Set<String>
        get() = _targets
    override fun sayHello(target: String) {
        _targets += target
        greeter.sayHello(target)
    }
}

上記のような例が紹介されていた。by greeter の部分がオーバライドしていないメンバは greeter に依存するという意味らしい。うまい説明ができないので詳しくは本誌参照のこと。

第 11 章 ジェネリクス

最初の理解としては大体 Java と同様なのだが、以下の記述は書きにくいのではないかと思った:

class Baz<T> where T : Hoge, T : Fuga  // T は Hoge と Fuga インターフェースを実装している

直感的には class Baz<T : Hoge, Fuga> なのだが。

Java も Kotlin もジェネリクスの実装は不変だが Kotlin のジェネリクスは共変や反変にすることもできる。 例えば A<T> のところを A<out T> (共変) や A<in T> (反変) などと記述する。

例えば Array<String>Array<CharSequence> ではない (StringCharSequence のサブタイプ) が Array<out CharSequence> だと Array<String> が含まれるし Array<in String> だと Array<CharSequence> も含まれるということ。

最後に具象型というトピックがあった。Kotlin のジェネリクスは Java と同様に実行時に型情報が消去される (Java では常に型パラメータは Object となる) が、そうではなく型情報を保持した状態で期待する動作を実現する方法があるらしい。

inline fun <reified T> Any.instanceOf(): Boolean = this is T

このような関数が紹介されており reified というキーワードが付いている。 普通の Java だと T は常に Object となるので is キーワードによる型チェックは失敗しそうに見えるが、これなら成功するらしい。

第 12 章 Null 安全

ここに書いてある内容は Swift などと同様なので新たに知ったような事は少ない。 普通こういった機構はオプショナルというが Kotlin では Nullable というようだ。

第 13 章 その他の話題

モダンな言語には大抵付いている演算子オーバーロードの話題から始まっていた。 Kotlin では operator fun ... で書くと演算子オーバーロードとなる。 あまり乱用すると訳が分からなくなるので、明らかに分かりやすくなるケースのみに使用していくのが良いかと思う。

その次は等価性の話題となっていた。 Kotlin では Java のように参照の一致を検査したい場合 === 演算子を使用する。 Kotlin では == 演算子は equals() と同等の意味となっている。 Java だと == が参照の一致の検査になっているせいで文字列の一致の比較など "str".equals(b) などといちいち書かなければならないので不便だった。

次が中置呼び出しに関する話題だった。 infix キーワードで定義するもので、例えば MyInt(1) plus MyInt(2) などという中置記法が可能となる。 ビルトインのものだと 1..10 step 3step が中置呼び出しとなっている。

オプショナル型の値が入っていたら何か処理をする

Kotlin には let という構文がある。Swift だと if-let 文が相当するし Java だと Optional#ifPresent() だ。 あるオプショナル型の変数に値が入っていたら何かする、といった具合に使用する:

val a: String? = null
a?.let { print(it) }  // let ブロック内で it で optional が外された形で使用可能
a?.let { print(it) } ?: print("nothing")  // elvis 演算子と組み合わせる事で null の場合の処理も記述可能

この a?.let が曲者な気がする。

typo するとバグの元になりそう

上記の例で以下のように間違えると意図した結果にならない:

val a: String? = null
a.let { print(it) }  // ? が無いので a が null でも print されてしまう!!
a?.let { print(it) }  // ? があるので正しく print されない

これでうっかりハマって暫く気づかなかった……。 ちなみに let のブロック内で it のメソッド呼び出しをしようとするとオプショナル型がアンラップされていないのに気づいてコンパイルエラーになる:

val a: String? = null
a.let { it.toInt() }  // オプショナル型 it への安全でないメソッド呼び出しでコンパイルエラー
a?.let { it.toInt() }  // これは OK (it.toInt() は実行されない)

Swift や Java ではこういうことはない

Swift の if-let ではこういうことはないし Java だとそもそも Optional 型でないと ifPresent メソッドをコールできないのでこういうことはない。

カラクリ

ソースを見ると実は Kotlin の let言語構造的なものではなく拡張メソッド (エクステンション) を使用して以下のように定義されている:

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)  // 渡された関数オブジェクトを実行するだけ!!

この定義を見ると何てことはなく、ただ単に引数に渡された関数オブジェクトを実行するだけの拡張メソッドである。 つまり String? 型として let を呼べるし、呼ぶと上記 block の型は (String?) -> R となるだけのことだった。

let は実は以下の省略形と書けば理解が進む気がする:

val a: String? = null
a?.let { print(it) } // せいかい. 実は (String) -> Unit
a?.let { b: String -> print(b) }  // 冗長な書き方. 上と同じ
a.let { print(it) }  // 間違い. 実は (String?) -> Unit
a.let { b: String? -> print(b) }  // 冗長な書き方. 上と同じ

Kotlin スタートブック 一人読書会: 第 II 部 Kotlin 文法詳解 前回に引き続き Kotlin スタートブックの一人読書会を継続する。 今回は第 II 部である。第 II 部が長いので 2 回に分ける。

第 5 章 関数

可変長引数は Java だと 型... (例えば String... args) だが Kotlin だと vararg を頭につける:

fun sum(vararg ints: Int): Int

可変長引数に配列を渡す場合は先頭に * をつける。Python みたいだ:

sum(*intArrayOf(1, 2, 3))

vararg 指定された型 Int は、特化された配列クラス (IntArray) とみなされるので、IntArray のファクトリメソッド intArrayOf を使用しています。

とある。なるほど、やはり arrayOf で作成されるのは本当に Java の不自由な配列というわけだ。 確かに以下のコードを実行してみるとコンパイルエラーとなった:

sum(*arrayOf(1, 2, 3))  // Type inference failed. Expected type mismatch: inferred type is Array<Int> but IntArray was expected

結局 Java の不自由な配列 (Java のプリミティブ型とオブジェクト型でクラスが異なる) が裏に見えてしまうのはちょっとイケてない気がしてしまうが、致し方なかったのだろう。

その後再帰呼び出しの話が続くが、いきなり関数型プログラミングの話になり難しくなった。 Scala などでもそうだが巨大な再帰呼び出しでスタックオーバーフローにならないように tailrec キーワードを付けると TCO (末尾呼び出し最適化) が行われるようになる:

tailrec fun sum(numbers: List<Long>, accumulator: Long = 0): Long =
    if (numbers.isEmpty()) accumulator else sum(numbers.drop(1), accumulator + numbers.first())
    

また Kotlin でもローカル関数 (関数の中の関数) を定義できるとある。 個人的にこれも嬉しい仕様で、いわゆるヘルパー関数の影響の及ぶ範囲がわかりやすくなるのでコードが追いやすくなる。 Java 8 でも一応できる (Lambda 式をローカル変数に代入する) が書きやすいとは言い難い。

第 6 章 第一級オブジェクトとしての関数

初めに関数オブジェクトについての説明が入っていた:

Kotlin では関数も、数値や文字列などの他の値のように扱うことができます。...(中略)...このような性質を持つオブジェクトのことを、「第一級オブジェクト」(first-class object) と呼びます。

構成の関係上まだラムダ式が登場していなかったので簡単な具体例が出せなかったのだろうが、要するに以下のようなものだろう:

val plus = { x: Int, y: Int -> x + y }  // 足し算をする関数オブジェクト
print(plus(1, 2))  // 3

尚、冊子の方には関数オブジェクトという言葉は Kotlin 公式ドキュメントには登場しないので便宜上の用語として捉えて欲しい、と注釈してあった。

ただ Kotlin で関数 (関数オブジェクトではない) が定義されている場合に関数オブジェクトを取得する方法もあり、それは前置ダブルコロンで表現されるとのこと:

fun square(i: Int): Int = i * i
fun main(args: Array<String>) {
    println(::square)  // 前置 :: で関数オブジェクト取得
}

Java みたいな記法だ。 Java と同じくメソッド参照と言えば良いと思うのだが、冊子の中にはそういった記載は無い。 まぁこの場合だとメソッド参照でなく関数参照という事になるが。

その後いよいよラムダ式が登場した。

さらに、ラムダ式の引数が 1 つのときに限り、暗黙の変数 it を使用することができます。

val square2: (Int) -> Int = { it * it }

Swift だと引数が 2 個以上でも暗黙の変数 $0, $1, ... が使用できるので便利なのだが Kotlin では引数が 1 個の時しか使用できない。分かりやすさを重視したということだろうか。

その後インライン関数の説明が入っている:

インライン関数は、引数の関数オブジェクトが、コンパイル時にインライン展開される関数のことです。通常の関数に、アノテーション inline を付加するだけで、インライン関数になります。

inline はアノテーションだったのか。キーワードではなくて。

あくまで引数の関数オブジェクトがインライン展開されるようなので、引数が関数オブジェクトになっているような高階関数に使用するとパフォーマンスが向上するとのこと。以下の様な例があった:

inline fun log(debug: Boolean = true, message: () -> String) {
    // この中のコードが message() の箇所も含めて展開される
    if (debug) {
        println(message())
    }
}

コラムには同様に noinlinecrossinline もあると紹介されていた。

また、ラムダ式とは異なるものとして無名関数が紹介されていた:

val square2: (Int) -> Int = fun(i: Int): Int {
    return i * i
}

こんな書き方もできるのは初めて知ったが、ラムダ式を使わずこちらを使う意味が正直よく分からなかった。

第 7 章 オブジェクトからクラスへ

Java やその他の言語でインタフェースやクラスに馴染みのある読者は、この章を読み飛ばしてもいいでしょう。

と書いてあるようにほぼ Java と同じ概念の説明なので特筆すべきことは殆ど無い。

1 つだけ、Kotlin では Java でいうところの無名クラスの即時インスタンス化のような構文はオブジェクト式を用いて以下のように書く:

// 即時インスタンス生成
val bucket = object {
    ...
}

// Bucket インターフェース実装
val bucket = object : Bucket {
    ...
}

Android でも 2 つ以上メソッドを持つリスナの実装を行う際にこの記法が出てくる。

第 8 章 クラスとそのメンバ

プロパティの説明でバッキングフィールド (backing field) という聞き慣れない単語が出てきた。 Kotlin のクラスのプロパティ (フィールド) は自動的にバッキングフィールドが生成され、実際にはこのバッキングフィールドに値が格納されるとのこと。 バッキングフィールドを持たない例として以下の様な nameLength の例が紹介されていた:

class Person {
    var name: String = ""
        set(value) {
            println("${value}がセットされました")
            field = value
        }
    var age: Int = 0
    val nameLength: Int
        get(): Int = this.name.length
}

プロパティには上記の例のようなゲッターやセッターを定義できる。 セッター内で使用できる暗黙の変数 field に格納するとそのプロパティ値を格納したことになる。 直接 name = value などとするとセッターが無限に呼びだされ続けスタックオーバーフローになるとのこと。

プロパティは必ず初期化する必要があるが、それだと DI やユニットテストの時に不便なので lateinit キーワードを使うと初期化を遅らせることができる、という説明があった。

Kotlin におけるセカンダリコンストラクタ (2 つ以上コンストラクタを持つクラス) の定義はクラス内に constructor キーワードで定義するとのこと:

class Rational(val numerator: Int, val denominator: Int) {
    constructor(numerator: Int) :  this(numerator, 1)
}
class Rational(val numerator: Int, val denominator: Int = 1)  // この例の場合こちらの方がシンプル

また、既存のクラスを継承せずにメソッドを生やすことができる拡張関数 (エクステンション) に関して説明されていた。 Swift にもあるがとても便利だ。

第 9 章 継承と抽象クラス

Kotlin は、デフォルトではクラスを継承することができません。クラスを継承可能にするには、修飾子 open をクラスに対して付ける必要があります。

継承可能であることを明示するために open を付ける。 これは良い設計だと思う。 基本的に継承は影響が大きく、乱用するとコードがスパゲッティになりやすい。

また、オーバライド可能なメンバにも open を付けてオーバライド可能であることを明示する必要がある。

Java でいうところの全ての継承元のオブジェクトである Object にあたるのは Any である。 toString()equals()hashCode() がそれぞれ open で定義されている。

Kotlin の可視性修飾子には Java でいうところのパッケージプライベート (デフォルトのアクセス制限) とかいう訳のわからないものは勿論無く publicinternalprotectedprivate となっている。 internal は同一モジュール内に限りアクセス可能であることを示す修飾子である。 何も付けない場合はデフォルトで public となる。

待望の Kotlin 入門書

昨日待望の Kotlin に関する入門書が出版された。 都内のそれなりに大きい本屋に行ってみたが置いていなかったが、私はネットで予約しておいたので当日に入手することができた。 今回はこれの一人読書会を行う。

本書は Java エンジニア、特に Android アプリの開発経験者を対象としています。とはいえ、何かしらプログラミング経験をお持ちの方でしたら、無理なく、興味深く読み進めることができるでしょう。なぜなら……(以下略)

と本の帯にも書かれている。私はそこそこの Android アプリ開発の経験者であり Android 外でも業務系の Java を書いた人間なので対象読者にピッタリはまっている。 Kotlin に関しては Try Kotlin の Kotlin Koans (問題を解いていくと Kotlin の文法が覚えられる例題集) の基本的な部分を解いた程度の知識となっている。なので、以下の読書会の感想もあまりに基本的な部分は省くものとする。Kotlin の基礎文法に関しては Qiita などに幾らでも載っているのでそちらを参照。

第 1 章: ようこそ!Kotlin の世界へ

Kotlin が JetBrains 社によって開発されたことや Scala や Groovy などと同じく JVM 言語であること、Java の設計が最早古いものであり Kotlin がその欠点を克服していること、Kotlin の言語的な特徴、国内の幾つかの Android アプリも既に Kotlin で書かれリリースされていることが書かれている。まぁこの章は読み物として読んでおくだけでいいだろう。

第 2 章: Kotlin を始める

いきなり Hello World のコード例が出てくるが、「クラス」「シグネチャ」「変数」「仮引数」などの用語が説明無しで出てくるし、書き方が Java 経験前提のものになっているので、既に Java 未経験者は厳しい内容になっている気がする。まぁそうでもないとどうしても文章が冗長になってしまって読みにくいのでこれで良いのだが。

Try Kotlin の説明が入る。この Try Kotlin がサンドボックスとして良く出来ていて、先程も述べたように Kotlin の文法を覚える例題集 Kotlin Koans が秀逸だし、Kotlin をその場で書いて実行できる上 Command + Space (Ctrl + Space) でコード補完まで働く。わざわざ IDE をダウンロードする必要がない。

あと CUI コンパイラ (Java でいうところの javac) と REPL (対話的にプログラムを実行できるツール) と IntelliJ IDEA の説明もされているが、実際に Android 開発で使用する際は Android Studio 上で完結するので特に必要はない。また、プログラムを書いて実行するのも上記の Try Kotlin の方が手軽。

第 3 章: Kotlin ガイドツアー

Scala スケーラブルプログラミング (Scala コップ本) からヒントを得て有理数クラスを実装することにより Kotlin のチュートリアルを行うという事らしい:

class Rational (val numerator: Int, val denominator: Int)

コンストラクタありのクラス定義。Java と比べるととてもシンプル。ただこれだけだと toString した結果が hashCode を出力するような不格好なものとなるので以下の様な toString 実装例が書かれていた:

override fun toString(): String = "${numerator}/${denominator}"

override がアノテーションでなくキーワードだし、文字列の変数展開が ${...} で出来るのがやはり良い。 これや関数の定義が Scala や Swift と若干異なるのでこれらの言語を同時に書いていると混乱する。

また、以下のようにイニシャライザを書くことができるらしい:

init {
    require(denominator != 0, {"denominator must not be null"})
}

コンストラクタ内で変数代入以外の処理やチェックを行いたい場合に使えそうだ。 require という構文は assert 的なものらしい。 要するに条件を満たさない場合に IllegalArgumentException がスローされるとのこと。

Kotlin の演算子オーバーロードの書き方は operator fun ... である。 この本には例として operator fun plus(that: Rational): Rational の実装例が記載されていた。

また、Rational(1, 2) + 1 は可能だが 1 + Rational(1, 2) を可能にするために拡張関数が紹介されている。 Scala や Swift にもこういったものがあるが Java だと static なユーティリティメソッドを用意するといった不格好な方法しかないので個人的にこれは嬉しい。

例えば以下のように拡張関数を書くことで追加できるとあった:

operator fun Int.plus(r: Rational): Rational = r + this

第 4 章: 基本的な文法

この章は前の章よりも難易度が下がり Kotlin の基礎の基礎に関する説明が続く。 Kotlin を試し書きしたような人は流し読みでいい気がする。

私が初めて Kotlin を書いて正直戸惑ったのが配列のリテラル表記がないことだ。 例えば多くの言語では [1, 2, 3] などとして配列のインスタンスを書けるが Kotlin は arrayOf(1, 2, 3) などといった関数を使う。 List の場合は listOf(1, 2, 3) だ。 コンストラクタ表記 (例えば ArrayList(1, 2, 3)) ではないのは Java と同じく実装クラスでなくインターフェースを使うという意味だろうが、リテラルで書けないのはイマイチ格好良くない気がする……。 PHP 5.3 以前の array(1, 2, 3) を思い出してしまう (特に古い PHP は何でも連想配列なので度々この array() が出現し読みにくい)。 まぁ勝手に解釈すると arrayOf といった関数にしておくと初期化する要素の内容によって生成する実装クラスを変更できるので有利だ。

コーディング規約

Swiftコーディング規約@Wantedly が参考になったのでこれをそのまま使用している。 ちょっと驚いたのが定数が Upper Snake Case でなく接頭辞 k を付けた camelCase であることだ。 例えば let DEBUG_KEY = "hoge" でなく let kDebugKey = "hoge" となるということ。

先頭 k が最初奇妙に思えたが、しばらく Swift に触っていたら公式のライブラリなどもこの規則に従っているようなので正しく思えてきた。 よく考えたら Android も public でないフィールドには接頭辞 m を付けるという奇妙なルールを使用していたので、それと同じに思える。

JavaDoc 的なもの

Swift には JavaDoc 的なものが無いのかと思っていたのでコメントを適当に書いていたのだが、どうもあるようなのでそれに従うことにした。 Swift 2 のドキュメントコメントが参考になった。

三本スラッシュ /// で始めるとドキュメントコメントになるというのが結構便利に思った。 あとは markdown で書けばそのままドキュメントになるところとか、実にモダンだ。

CHAPTER 07 演算子

Swift に用意されている演算子とその優先順序、そして最近の言語にはよくある演算子を自分で定義できるという話。 実装方法はどうせまたその時に調べると思うので要点だけ書く。

  • 二項演算子は infix operator で定義する
  • 前置演算子は prefix operator で定義する
  • 後置演算子は postfix operator で定義する
  • それぞれ演算子の優先度を precedence で定義する
  • それぞれ演算子の結合規則を associativity で定義する

パターンマッチ演算子

Swift にはパターンマッチ演算子 ~= が用意されておりこれをオーバーライドすることで switch 文のパターンマッチの挙動を上書くことができる。 しかしこれはグローバルに定義されてしまうので対象を特定の列挙型に限定するなどの工夫が必要となる。

短絡評価をする演算子の定義

例えば三項演算子 a ? b : c みたいなものを自分で定義したい時に普通に書くと a を満たさなくても c が評価されてしまうのがまずい場合がある。 処理に時間が掛かる場合やログ出力を行う場合などだ。 こういう時は @autoclosure を使う:

// cond を満たさない時のみ処理を行う
func skip(cond: Bool, @autoclosure _ arg: () -> Int) {
    if !cond {
        print("value=\(arg())")
    }
}

CHAPTER 08 クラスと継承

クラスの概要

Java のクラスの知識がそのまま使えそうな雰囲気。構造体と違って mutating 修飾子は必要ない。

動的結合とキャスト

インスタンスの型が対象のものかをチェックする演算子は is でキャスト演算子は as, as!, as? である。 これは Kotlin と同様。

クラスメソッドとクラスプロパティ

なんか Python みたいだがタイプメソッドとタイププロパティ (static で記述するもの) の他にクラスメソッドとクラスプロパティ (class で記述するもの) がある。 class の方はサブクラスでオーバーライドできるが static のほうはできない。

指定イニシャライザと簡易イニシャライザ

Swift ではイニシャライザ (コンストラクタ) は init で定義するが init の中から他の init を呼ぶようなのを簡易イニシャライザ (他のイニシャライザの助けを借りるイニシャライザ) と良い convenience init で定義する。

必須イニシャライザ

required init で記述すると必須イニシャライザとなり継承先のクラスでも同一のイニシャライザを必ず実装しなければならなくなる。

遅延格納型プロパティ

Kotlin と一緒で lazy で指定すると遅延格納型となる。

CHAPTER 09 メモリ管理

この辺りは難しい話なので、読み物として読んでおくだけでいいのではと思う。

弱い参照

Java でいうところの WeakReference (弱参照) を実現するのに weak キーワードを使用する。

CHAPTER 10 プロトコル

要するに Java のインターフェース。

オプション項目のあるプロトコルを宣言する

必ずしも実装する必要のないメソッドやプロパティを宣言することができる。optional を先頭につければよい。

ネスト型とプロトコル

Java でいうところのジェネリクス (型パラメータ) は typealias キーワードで定義する。

CHAPTER 11 拡張

Kotlin と同様にすでにあるクラスに対し機能を追加することができる。 これをエクステンションと呼ぶ。

CHAPTER 04 オプショナル

今時の言語にはすべて搭載されている、ヌルポ対策機能。

オプショナル型と nil

Swift では null でなく nil で Objective-C には nilNull も両方あるらしい。 null 許容型 IntInt? とかく。これは Kotlin 等と同様。

オプショナル型の値を開示する

Int? 型を Int 型に変換するのに ! を使用する。確実に nil が入っていないと予想できるときに使用する:

let year: Int? = Int("2020")
var remain: Int = year! - 2016  // ! が無いとコンパイルエラー
year! -= 2016  // このように代入も可能

if-let 文

オプショナル型の値が nil ではなかった場合に処理をする、といった構文が if-let 文となる:

let year: Int? = Int("2020")
if let y = year {
    print("あと \(y - 2014) 年")  // y: Int として使える
} else {
    print("エラー")
}

// 複数のオプショナルがともに nil でない場合は以下のように書く
if let sapporo = Int("1972"), nagano = Int("1998") {
    print("\(nagano - sapporo) years.")
}

// where 句で更に条件を絞ることができる
if let sapporo = Int("1972"), nagano = Int("1998") where nagano > 1990 {
    print("\(nagano - sapporo) years.")
}

guard 文

if-let の逆、即ち条件を満たさない時に else 句の処理を行うことができる:

let stock = ["01", "2", "4", "05", "8", "q", "X"]
for str in stock {
    guard let v = Int(str) else {
        print(str + "??")
        break
    }
    print(v, terminator:" ")
}
// 1 2 4 5 8 q??

nil 合体演算子

オプショナル型に対し ?? で値を取り出すことで nil の場合にデフォルト値を使用するといったことができる:

opv ?? S  // opv が nil なら S が使われる. opv != nil ? opv! : S と同様

有値オプショナル型

プログラムの前後関係から確実に nil でない値が入っていると予想できる場合は Int? でなく Int! とすると使用する際に ! の開示が不要となる:

let year: Int! = Int("2020")
print("あと \(year - 2016) 年")  // ! が不要

失敗のあるイニシャライザの定義

init ではなく init? を使用する。例えば init への引数が防いで有効な構造体を生成できない時に nil を返すようにするなど。

CHAPTER 05 基本的なデータ型

部分配列

配列のスライスはどうやるのかと思ったら範囲指定がそのまま使用できるようだ:

let stock = ["01", "2", "4", "05", "8", "q", "X"]
print(stock[1...4])  // ["2", "4", "05", "8"]

但し上記でスライスした配列は Array<String> ではなく ArraySlice<String> になり、添字が元のままの 1, 2, 3, 4 になる。 これを普通の 0 から始まる配列として使用したい場合は Array<String> のイニシャライザで wrap する:

let subarray = [String](stock[1...4])
print(subarray[0])  // "2"

可変長引数

可変長引数指定は Java と一緒で Int... のようにする。 但し Java と異なり引数の途中でも可変長引数にできる。

辞書の型宣言と初期値

辞書の型は [String: Int] のように記述する。

var d: [String: Int] = [:]  // 空の辞書代入

辞書へのアクセス

「辞書のキーが存在したらなにか処理をする」のようなものは if-let 文が使える:

var d = ["Swift": 2014, "Objective-C": 1983]
if let v = d["Swift"] { print(v) }  // 2014
if let v = d["Ruby"] { print(v) }  // 何も出力されない

集合の型宣言と初期値

配列リテラルが使用できるが型を明示的に指定しないといけない:

var s: Set<String> = ["フランダース", "フランドル", "フランシスカ"]

CHAPTER 06 パターン

キーワード付きのタプル

タプルにキーワードが付けられるというのは知らなかった:

let photo = (file: "tiger.jpg", width: 640, height: 800)
print(photo.0)  // tiger.jpg
print(photo.file)  // tiger.jpg

タプルを switch 文で使う

結構柔軟なことができるようだ:

switch day {
case (1, 1...5):  // 範囲指定ができる
    print("正月休み")
case (4, 29), (5, 2...6):  // 更に条件を並べることができる
    print("連休")
case (8, let d) where d > 10:  // タプルの一部を使用することができるし where で条件を絞れる
    print("8/\(d)は夏休みです")
default:
    break
}

オプショナル型を switch 文で使う

オプショナル型を含むタプルに対し nil でないことを条件とするには ? を付けるようだ:

case let (name, age?) where age >= 18:  // 開示した数値を代入
case let (name, (15...18)?):  // 開示して区間指定

シンプルな列挙型

Java と似ているが case 接頭辞を付けるようだ:

enum Direction {
    case Up, Down, Right, Left
}

値型の列挙型

列挙型に実体型 (raw value) を割り当てることができるらしい:

enum Direction: Int {
    case Up = 0, Down, Right, Left  // こう書くと 0, 1, 2, 3 となる
}
print(Direction.Right.rawValue)  // 2
print(Direction(rawValue: 3)!)  // Left

共用型の列挙型

イマイチ使い所がよく分からないが、それぞれの要素の型 (タプル) が異なる列挙型も定義できる:

enum WebColor {
    case Name(String)
    case Code(String)
    case White, Black, Red
}

let background = WebColor.Name("indigo")
let tuiquoise = WebColor.Code("#04E0D0")
let textColor = WebColor.Black

if-case 文

switch 文だとパターンマッチングが使用できるが default 句を書かなければならず 1 つのパターンマッチングを行うだけだと冗長なので if-case 文という構文が用意されているようだ:

if case .card(let y, _) = t where y > 1200 { ...

for-in 文で case パターンを使う

for-in でも case が同様に使用できる。やはり switch だと冗長なので代わりに使用できるようだ。

再帰的な列挙型

……こんな構文使うのか?と思うので、メモだけ。自分自身の型を再帰的に使用する場合は case の前に indirect を指定し特別扱いする。

プログラミング言語 Swift の解説書として名著である詳解 Swift 改訂版の一人読書会を開始する。 ここには私が「なるほど」と思った Swift の特筆すべき文法を備忘録代わりに記録するものとする。

私は Android アプリの開発経験は 5 年以上だが iOS アプリの開発経験は無く Swift 経験も過去に公式ドキュメントを流し読みして Hello World した程度となっている。 なので Java などのプログラミング経験は豊富だが Swift 経験は無いという方がこの文章に一番マッチする対象読者となる。

CHAPTER 01 Swift でプログラミング

文字列

Swift の文字列埋め込みの記法は \(式) のようにする:

let n = 8
let str = "\(n) の 2 乗は \(n * n) です。"  // "8 の 2 乗は 64 です。"

print 関数

print 関数は Python のようにセパレータを指定することができる:

print(6, 2014, "林檎", separator:"; ")  // "6; 2014; 林檎"

Swift 1.2 までは println があったが Swift 2.0 からは print がデフォルトで改行するようになった。 もし改行したくない場合はキーワード terminator に空文字列を渡す:

print("ほげ", terminator:"")

配列

若干記法が独特だ:

// Int 型の配列は [Int] とかく
let a: [Int] = [2, 5, 8, 11, 7]

// 空の配列生成 (イニシャライザ呼び出し)
let s = [String]()
let s: [String] = []

Java と違って配列も値型のデータらしい。なので普通に他の変数に代入するとディープコピーされるようだ。

型に別名を付ける

ちょっとイマイチ使いどころが分からないが、型に別名を付けることができる:

typealias SInteger = Int32

repeat-while 文

Java や C の do-while にあたるものは repeat-while となっている。Swift だと do が他の文法で使用されている為だろう。

for-in 文

for-in (foreach) が使えるのは大体想像がつくが where でループ対象を絞り込むことができる:

for i in 1..<64 where i % 3 != 0 && i % 8 != 0 {
    print(i, terminator:" ")
}
print("")
// 1 2 4 5 7 10 11 13 14 17 19 20 22 23 25 26 28 29 31 34 35 37 38 41 43 44 46 47 49 50 52 53 55 58 59 61 62

switch 文

switchbreak で抜ける必要が無くなった。 break 必須というのはバグの元なので当然だろう。 もし Java や C でいうところのあえて break を書かずに続けて実行させるという場合は fallthrough キーワードを使用する。

do 文

変数のスコープを絞るのに Java や C だと {} で囲えばできるが Swift はできない。代わりに do 文を使う:

if a >= 0 {
    b += a
    do {
        // t はこのブロック内でのみ有効
        let t = a
        a = c
        c = t
    }
}

CHAPTER 02 関数

外部引数名

Swift では関数の第 1 引数のみキーワードを省略できるが第 2 引数以降はキーワードが必須という文法になっており、これは Objective-C を踏襲したものになっている:

func add(x: Int, y: Int) -> Int {
    return x + y
}
add(1, y:2)  // 本当は add(1, 2) と書きたいが許されない

第 1 引数もキーワード必須にしたり第 2 引数以降もキーワード不要にする文法もあり以下のようにする:

// 引数に別名を付けると第 1 引数でも必須となる
func add2(a x: Int, b y: Int) -> Int {
    return x + y
}

// 引数に _ を別名として付けると第 2 引数以降でもキーワード不要
func add3(x: Int, _ y: Int) -> Int {
    return x + y
}
add2(a:1, b:2)  // 両方キーワード引数
add3(1, 2)  // 両方キーワードなし

下線の特殊な用法

基本的に _ はワイルドカードとして無視するのに使える:

_ = myGreatProcess(10, 20)  // 結果を捨てることを明示
func compare(a: Int, _ b: Int, _: Bool) -> Bool { return a > b }  // 第 3 引数は使わない
func compare(a: Int, _ b: Int, option _: Bool) -> Bool { return a > b }  // 第 3 引数は外部引数としては要るが内部で使わない

inout 引数

Swift の関数の引数は値渡しだが inout を付けるといわゆる参照渡しになる。 ちょっと今の時点で使いどころがよくわからない。 PHP などでもよくある話だが参照渡しは分かりにくくなる元だ。

引数の値を処理中に変更できるようにする

Swift の関数の引数は定数だが var を付けると処理中に変更可能になる。 これも分かりにくくなるのであまり使わない気がする。

返り値を必ず使うための指定

@warn_unused_result という属性 (Java のアノテーションもしくは Python のデコレータみたいなもの) を使うと関数を呼び出した箇所で戻り値を使っていないと警告を表示するようになる。

CHAPTER 03 構造体

Swift は構造体があるしクラスもあるが、どう違うのかがよくわからなかったので調べてみたらこの Qiita の記事がうまくまとまっていた。 要するに構造体は値渡しになりクラスは参照渡しになるらしいので、比較的小さいデータ型を定義するのに構造体を使用するようだ。

構造体を定数に代入した場合

この辺りの話がちょっと Java の感覚と違ったので注意が必要だった。 構造体が値型なのでプロパティも let 扱いとなるようだ。 構造体で var で定義してあるプロパティであっても構造体を定数に代入すると var のプロパティが変更不可となる:

struct Date {
    var year = 2010
    var month = 7
    var day = 28
}
var d = Date()  // 2010-07-28
d.day = 29  // これは OK
var camp = Date(year:1998, month:8, day:8)  // 1998-08-08 変更可
let event = Date(year:2000, month:9, day:13)  // 2000-09-13 変更不可
event.day = 30  // これは許されない!

イニシャライザの定義

構造体のイニシャライザ (コンストラクタのようなもの) は init で書く:

struct Date {
    var year, month, day: Int
    init() {
         year = 2095
         month = 10
         day = 31
     }
 }

もちろん init を複数定義したり (コンストラクタのオーバーロード) もできる。

構造体の内容を変更するメソッド

構造体の内容を変更するメソッドを定義する場合は mutatingfunc の前に付ける。 そもそも内容を変更するのは推奨されないのであまり使うことはないだろう。

計算型プロパティ

計算型プロパティ (変更可能なプロパティ) にはゲッタとセッタを定義することができるが、以下の様な記法となる:

struct Ounce {
    var mL: Double = 0.0
    static let ounceUS = 29.5735
    init(ounce: Double) {
        self.ounce = ounce
    }
    var ounce: Double {
        get {
            // get 節のみの場合 get と {} を省略可能
            return mL / Ounce.ounceUS
        }
        set {
            // 仮引数を省略した場合 newValue という名前の引数として使用できる
            mL = newValue * Ounce.ounceUS
        }
    }
}
  • セッタが不要の場合 get{} が省略可能
  • セッタの仮引数は省略可能
  • セッタの仮引数を省略した場合 newValue という名前で引数を使用できる

計算型プロパティに対する特殊な設定

get でプロパティを変更するような場合は mutating を付加し set でプロパティを変更しない場合は nonmutating を付ける。 あまり使わない気がする。

プロパティオブザーバ

格納型プロパティの値が更新されるときにメソッドを起動するのをプロパティオブザーバと呼ぶ。 willSetzdidSet (要するに pre と post) で定義できる。 ログ出力とかで使えるかもしれない。

ちなみに willSet では newValue で参照でき didSet では oldValue で参照できる。

添字付け

要するに構造体を使って配列のような仕組みを自前で定義できるというもの。 subscript を使う。

struct A {
    let items = ["ほげ", "ふが", "はげ"]
    subscript(i: Int) -> String {
        return items[i]
    }
}
print(A()[1])  // ふが

Qiita で Bundle を少しだけ簡単に書くという記事を書いた。今回はそれの Kotlin バージョンとなる。

ソースコード

/**
 * Bundle オブジェクトを作成する utility method.
 *
 * @param args key と value の pair
 * @return Bundle
 */
fun bundle(vararg args: Pair<String, Any?>): Bundle = Bundle().put(*args)

/**
 * すでにある Bundle オブジェクトに key と value を複数追加する utility method.
 *
 * @receiver Bundle
 * @param args key と value の pair
 * @return Bundle
 */
fun Bundle.put(vararg args: Pair<String, Any?>): Bundle = args.fold(this, { bundle, (key, value) ->
    when (value) {
        null -> bundle.putString(key, null)
        is Boolean -> bundle.putBoolean(key, value)
        is BooleanArray -> bundle.putBooleanArray(key, value)
        is Bundle -> bundle.putBundle(key, value)
        is Byte -> bundle.putByte(key, value)
        is ByteArray -> bundle.putByteArray(key, value)
        is String -> bundle.putString(key, value)
        is Char -> bundle.putChar(key, value)
        is CharArray -> bundle.putCharArray(key, value)
        is CharSequence -> bundle.putCharSequence(key, value)
        is Double -> bundle.putDouble(key, value)
        is DoubleArray -> bundle.putDoubleArray(key, value)
        is Float -> bundle.putFloat(key, value)
        is FloatArray -> bundle.putFloatArray(key, value)
        is Short -> bundle.putShort(key, value)
        is ShortArray -> bundle.putShortArray(key, value)
        is Int -> bundle.putInt(key, value)
        is IntArray -> bundle.putIntArray(key, value)
        is Long -> bundle.putLong(key, value)
        is LongArray -> bundle.putLongArray(key, value)
        is Parcelable -> bundle.putParcelable(key, value)
        is Array<*> -> {
            if (value.size > 0 && value.all { it is String }) {
                bundle.putStringArray(key, value.map { it as String }.toTypedArray())
            } else if (value.size > 0 && value.all {it is CharSequence}) {
                bundle.putCharSequenceArray(key, value.map { it as CharSequence }.toTypedArray())
            } else if (value.size > 0 && value.all {it is Parcelable }) {
                bundle.putParcelableArray(key, value.map { it as Parcelable }.toTypedArray())
            } else {
                throw IllegalArgumentException("$key の配列における型パラメータが Bundle で扱えない.")
            }
        }
        is ArrayList<*> -> {
            if (value.size > 0 && value.all { it is String }) {
                bundle.putStringArrayList(key, ArrayList(value.map { it as String }))
            } else if (value.size > 0 && value.all {it is CharSequence}) {
                bundle.putCharSequenceArrayList(key, ArrayList(value.map { it as CharSequence }))
            } else if (value.size > 0 && value.all {it is Int}) {
                bundle.putIntegerArrayList(key, ArrayList(value.map { it as Int }))
            } else if (value.size > 0 && value.all {it is Parcelable }) {
                bundle.putParcelableArrayList(key, ArrayList(value.map { it as Parcelable }))
            } else {
                throw IllegalArgumentException("$key の ArrayList における型パラメータが Bundle で扱えない.")
            }
        }
        else -> throw IllegalArgumentException(key + "に対応する値が解釈できない.")
    }
    bundle
})

使用例

以下の様な感じで使える:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // Bundle オブジェクト生成 {hoge: "fuga", hage: [1, 2, 3]}
    val b = bundle("hoge" to "fuga", "hage" to arrayOf(1, 2, 3))

    // Bundle にあたかも put メソッドがあるかのように {hehe: "ouie"} 追加
    b.put("hehe" to "ouie")
    println(b)  // Bundle[{hage=[Ljava.lang.Integer;@a0250eb, hehe=fufu, hoge=fuga, hoho=ouie}]
}

拡張関数

Kotlin だと何が有利かというところで 1 つめ。 拡張関数という仕組みがあり、既存クラスにあたかもそういうメソッドを持っているかのように関数を追加できる。 上記例の fun Bundle.put(vararg args: Pair<String, Any?>): Bundle がそれである。 この仕組のお陰で別途ユーティリティクラスを作成してそれを呼ぶようなことをしなくても直接オブジェクトから呼べて自然な実装となる。

第一級関数

Java だと必ずクラスを作ってその中にメソッドを書かなければならないが Kotlin は一番外側に関数を記述可能。 上記例の fun bundle(vararg args: Pair<String, Any?>): Bundle がそれである。 しかもこの外側の関数をそのまま import できる (Java だとちょっと違うが static インポートに当たるか) のでソースコードがスッキリする。

to は custom operator

"hoge" to "fuga"to って何だろう。言語仕様だろうか。などと思ったかもしれないが、これは custom operator でビルトインのライブラリに以下のように定義されている:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

この infix というのを付けると自由な演算子が追加できるらしい。 この例だと任意のクラス A, B に対し to 演算子を適用すると Pair<A, B> インスタンスを生成して返す、というところか。

以前 Kotlin にはタプルがあったらしいが、これがあるので廃止されたらしい。

Hello World が短い

関係ないが Kotlin の Hello World が短い。以下で良い:

fun main(args: Array<String>) = println("Hello Kotlin!")

Java は以下:

public class Java {
    public static void main(String...args) {
        System.out.println("Hello World");
    }
}

項目 22: 辞書やタプルで記録管理するよりもヘルパークラスを使う

長いけど要するに mutable な辞書や要素を位置で指定するタプルを複雑な情報保持に使用するのは止めてクラスを使おうという事だろう。 でも namedtuple は知らなかった。クラスを作るほどでもないけど辞書や tuple だと心もとない場合に使えそうだ。

項目 23: 単純なインタフェースにはクラスの代わりに関数を使う

他の言語だと、フックが抽象クラスで定義されます。

Java の事を言っているのだろう。まぁ Python を始めとした昨今の LL 言語は大抵関数をファーストクラスオブジェクトとして関数の引数に直接渡すことができるが、

状態を保守するために関数が必要な場合、状態を持つクロージャを定義する代わりに、__call__ メソッドを提供するクラスを定義することを考える。

なるほど。クロージャを使っていたかもしれない。

項目 24: @classmethod ポリモルフィズムを使ってオブジェクトをジェネリックに構築する

内容が難しくて若干辛みが……。ともかく、Python はクラスに対して __init__ メソッドという 1 つのコンストラクタしかサポートしていないので、代わりのコンストラクタを定義するために @classmethod を使うこと。

項目 25: 親クラスを super を使って初期化する

親クラスのコンストラクタを単純に (親クラス名).__init__() で呼び出すと、特にダイヤモンド継承時に親のコンストラクタが不当に 2 回呼びだされてしまい意図しない動作となることが書かれている。こういう場合は組み込み関数 super() を使うとダイヤモンド継承の頂点の __init__ は 1 回しか呼び出されない。

項目 26: 多重継承は mix-in ユーティリティだけに使う

まぁ多重継承は宜しくないのであまり使わないが mix-in 的要素だったらアリということだろう。

項目 27: プライベート属性よりはパブリック属性が好ましい

Python のプライベート変数 (頭に __ をつける) は厳密には特殊な構文で普通にアクセスできてしまう。

なぜ、プライベート属性の構文は、厳密な可視性を強制しないのでしょうか。最も単純な回答は、よく引用される Python のモットー「みんないい大人なんだから。」です。

Python はそういうところがある言語なのは認識している。定数が無くて UPPER_SNAKE_CASE で書いた変数を定数をみなすところとか。

プライベート属性は、コントロール外のサブクラスによる名前衝突を避けるためだけに使用する。

それが Python 流ということか。プロテクテッドの方がまだマシというのが驚いた。

項目 28: カスタムコンテナ型は collections.abc を継承する

abc は Abstract Base Class (抽象基底クラス) だ。Python でもこれを使えば抽象クラスが使えるということ。

項目 14: None を返すよりは例外を選ぶ

このあたりは Java と同じだ。null を返すより例外を選ぶ。

項目 15: クロージャが変数スコープとどう関わるかを知っておく

外側の関数の変数が内側の関数のスコープ内で参照できるのは知っていたが、外側の関数の変数への代入の挙動は知らなかった。 コードを見てみるのが早い:

def sort_priority(values: list, group: set):
    def helper(x: int):
        return 0 if x in group else 1, x  # group が helper 関数内で参照できる
    values.sort(key=helper)

numbers = [8, 3, 1, 2, 5, 4, 7, 6]
sort_priority(numbers, {2, 3, 5, 7})
print(numbers)  # [2, 3, 5, 7, 1, 4, 6, 8]

が、以下は正しく動かない:

def sort_priority2(values: list, group: set):
    found = False
    def helper(x: int):
        if x in group:
            found = True  # 外側の関数の変数に代入しているように見えるが, 実際は新たなローカル変数の定義, 代入
            return 0, x
        return 1, x
    values.sort(key=helper)
    return found

numbers = [8, 3, 1, 2, 5, 4, 7, 6]
found = sort_priority2(numbers, {2, 3, 5, 7})
print(found)  # False (想定どおりではない)
print(numbers)  # [2, 3, 5, 7, 1, 4, 6, 8] (これは合っている)

こういう場合 Python 3 だと nonlocal を使うらしい。上記コードの def helper(x: int): の下に nonlocal found とすれば動く。 ただ何か global 変数定義と似た危うさを感じるのでなるべく使わないほうがいいように見える。

ちなみに PyCharm だと nonlocal を使う前のコードで外側のローカル変数名と同じだと警告を出してくれる。

項目 16: リストを返さずにジェネレータを返すことを考える

その通りとしか言いようがないが、内包表記からジェネレータに変えるのは []() に替えるだけなので楽だが、 普通の関数の場合はなかなか思考が働かなかったりはする。

項目 17: 引数に対してイテレータを使うときには確実さを尊ぶ

いきなり内容が難しくなった気がする。イテレータは結果を一度しか生成しないので以下の様なことが起きる:

def f():
    for x in range(10):
        yield x

r = f()  # イテレータを変数に代入
print(list(r))  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(list(r))  # [] になる. 既にイテレートは終了してしまっているがエラーは発生しない

print(list(f())  # 常に新たなイテレータを使うようにすれば OK
print(list(f())  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

クラスで __iter()__ を定義したものを使った場合は挙動が違うのか。知らなかった:

class A(object):
    def __iter__(self):
        for x in range(10):
            yield x

r = A()  # インスタンスを変数に代入
print(list(r))  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(list(r))  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

内部的には list() が新たなイテレータオブジェクトを作成するために A.__iter__() を呼び出すのだということが書いてある。 ただ、これだと複数回イテレートしてしまうことになるので、そうしたくない場合は結果を変数に代入して再利用するのがよい。

項目 18: 可変長位置引数を使って、見た目をすっきりさせる

これは Java にもある機能ではある。特筆すべきことはない:

def f(x, *args):
    return x + ': ' + ', '.join(args)

print(f('hoge', 'fuga', 'hage'))  # hoge: fuga, hage

項目 19: キーワード引数にオプションの振る舞いを与える

Python のキーワード引数は非常に有用だと思う。これのお陰で可読性が高まる:

def location(name, lat=35, lng=139, bearing=0):
    return '{}: ({}, {}) / {}'.format(name, lat, lng, bearing)

print(location(name='家', lat=35.1829, lng=139.8237))  # 引数の意味が明確なので読みやすい
print(location('家', 35.1829, 139.8237))  # これでも呼べるがどれが何の引数なのかがパット見わからない

特に関数定義が離れていた場合引数の位置と意味を調べるのが非常に面倒なので、キーワード引数にすることにより意味が明確になり読みやすい。

項目 20: 動的なデフォルト引数を指定するときには None とドキュメンテーション文字列を使う

関数の引数のデフォルト値が mutable なインスタンスだった場合に値が関数の初回呼び出し時の 1 回しか評価されないので 2 回目以降の呼び出しで前回の値が残っており奇妙な動作になってしまう、というのは Python では有名な話だと思う:

def log(message, when=datetime.now()):
    print('{}: {}'.format(when, message))

log('hoge')  # 2016-02-06 09:04:39.305342: hoge
log('fuga')  # 2016-02-06 09:04:39.305342: fuga

現在時刻をロギングするはずが、常に最初の log() 呼び出しの日時が出力されてしまう。 これを避けるためにデフォルト引数に None を使用しドキュメンテーション文字列に文書化せよとある:

def log(message, when=None):
    """タイムスタンプを使用したログメッセージを出力する.

    :param message: 出力するメッセージ
    :param when: メッセージ出力時の datetime. デフォルトは現在時刻
    """

    when = datetime.now() if when is None else when
    print('{}: {}'.format(when, message))

log('hoge')  # 2016-02-06 09:09:36.333739: hoge
log('fuga')  # 2016-02-06 09:09:36.333805: fuga

項目 21: キーワード専用引数で明確さを高める

Python 3 にキーワード引数を要求する構文があるとは知らなかった。普通の引数とキーワード引数の間に *, を挟むというもの。これは使いたい:

def log(message, *, when=None):
    when = datetime.now() if when is None else when
    print('{}: {}'.format(when, message))

log('hoge')
log('fuga', datetime.now())  # キーワード引数を使用していないので TypeError
log('fuga', when=datetime.now())  # OK

Python 2 にはこの構文が無いので **kwargs を使用せよとの事。

Effective Python の発売日だった

自宅に Effective Python がやってきた。233 ページしか無いのに 3,200 円もするという技術本。 まぁ技術本なんてこのような値段設定は珍しくないが、やっぱり高い。

オライリーの書籍は O'Reilly Ebook で電子書籍としても購入することができるが、こちらは PDF フォーマットな上に若干出版にタイムラグがある。いつもだったら Ebook を待つのだがちょっと今回はすぐ読んでみたいのとたまには物理的な紙の媒体で読んでみたいというので前もって楽天で予約しておいたというわけだ。

折角高いお金を出して買ったので、ちょっと一人読書会でもやってみよう、というお話。 Blog や Qiita などで不特定多数に向けて何かを書くことを心掛けると、いい加減なことを書けないのでちゃんと理解しようという縛りを自分に設けることができる。

Effective Python の目次は O'Reilly の書籍紹介ページにある。 各項目に対して自分なりの考察若しくは感想文を書こうというわけだ。というわけで、以下 1 章 Python 流思考 (Pythonic Thinking) に関する一人読書会を実行する。

項目 1: 使っている Python のバージョンを知っておく

Python には 2.x 系と 3.x 系があり双方には互換性が無い。処理系にも CPython, Jython, IronPython, PyPy などあるよ、という話。 OS X や CentOS 等に最初からインストールされている Python は未だ 2.x 系である。Ubuntu は 16.04 LTS から Python 3.5 がデフォルトになるようだ。

これから新規 Python プロジェクトを立ち上げる場合は特に理由がない場合は 3.x 系を使用すること。 とはいえ、仕事としてやっているとどうしても 2.x の方を相手せざるを得ないのも事実。

項目 2: PEP 8 スタイルガイドに従う

Python プログラマの従うべき最も有名なコーディング規約として PEP 8 がある。 PyCharm などの IDE であれば最初から PEP 8 のチェックが入るし Vim や Emacs のようなエディタでもチェックする方法がある。

書籍には PEP 8 の中でも特筆すべき項目について列挙されている。筆者が気になったものを以下に引用する。

各行は、長さが 79 文字かそれ以下とする。

PEP 8 のこれはかなり有名なのだが、何故 79 文字以下なのだろうか。80 文字では駄目だったのだろうか。

またコードが短めの Python ならまだこれも守れないこともないが Java で 80 文字制限などしたら悲惨なことになるし、 今のディスプレイは高精細なので割と横に多く表示できるので 120 文字くらいでもいい気はしないでもないが、まぁこういう規約なので守っておく。

長さを使って空値かどうかをチェックしない。空値が暗黙に False と評価されることを使う。

暗黙型変換を使うのは危ないのでは?長さを使ったほうが安全では?と思ってしまった。 特に PHP では if ($hoge) と書くと if ($hoge == true) の意味 (緩やかな比較) となり PHP では割と変な値まで true, false になってしまうというのがあるので strlen($hoge) と書くのは結構よくやるので Python もアリではないかと思っていたが、暗黙型変換を用いた方が構文がシンプルになって良いということだろうか。

# 冗長な書き方
if len(somelist) == 0:
    ...

# 好ましい書き方
if not some list:

項目 3: bytes, str, unicode の違いを知っておく

Python 2 では文字列は str で Unicode 文字列を扱うのに unicode を使わなければならなかった。 英語圏の人は全く困らない仕様だが、我々の使用しているような非 ASCII 文字を使用している言語の場合 u'日本語' などと頭に u を付けて Unicode 文字列であることを明示しなければならなかった。

しかし Python 3 ではこれば str に一本化され単純に '日本語' と表現できるようになった。つまり Python 2 の unicode が Python 3 の str になったという話。

一時期 Unicode と UTF-8 がごっちゃになっていた時があったのだが UTF-8 はあくまで Unicode の効率的なエンコード方式であり別物である。 Unicode についてがすごく分かりやすかった。

Unicode 文字をバイナリ (生の 8 ビット値) で表すには多くの手法があります。一番多いのは UTF-8 符号化です。 重要なのは Python 3 の str インスタンスと Python 2 の unicode インスタンスがバイナリ符号化を伴っていないことです。 Unicode 文字をバイナリデータに変換するには、メソッド encode を使わなければなりません。

つまりファイルから読み込んだ場合などで bytes 型になっている時はエンコードされている状態 (多くは UTF-8) なのでそれを decode しなければならない。

項目 4: 複雑な式の代わりにヘルパー関数を書く

複雑な式を 1 行に詰め込むなとか部分的に共通化できるならヘルパー関数を書けという話。 Python は気軽に関数内関数が書けるので、このあたりは積極的に使っていきたいところ。

項目 5: シーケンスをどのようにスライスするか知っておく

Python は文字列もシーケンス型なので配列のようなスライスが簡単に使えるのが便利で美しい。

リストの先頭からスライスするときには、添字のゼロは省いて、見た目をスッキリさせましょう。

assert a[:5] == a[0:5]

0 ... つけてしまっていたかもしれない。

末尾までスライスするときには、末尾の添字は冗長なので省きましょう。

assert a[5:] == a[5:len(a)]

これはちゃんとできていた。Java 等の substring が第二引数を付けないと末尾までスライスするという意味なので類推しやすかったように思う。

一箇所、パット見よくわからなかった箇所が以下:

添字 start も end もないスライスに代入を行うと、(新しいリストが作成されるのではなくて) リストの内容全体が右辺のリストが参照している要素に置き換わります。

a = []
b = a
print('Before', a)  # []
a[:] = [101, 102, 103]
assert a is b  # True
print('After ', a)  # [101, 102, 103]

なるほど単純に a = [101, 102, 103] とやってしまうと a is not b になってしまう。リストの参照を変えずにリストの内容全体を書き換えたい時に使うわけだ。

項目 6: 1 つのスライスでは start, end, stride を使わない

start, end, stride とは somelist[start:end:stride] みたいなものの事で stride でリストの取得間隔を指定できるが、 これが読みにくいのでなるべく避けましょうという事だった。 それ以前にあまり使うことが無いわけだが……。

項目 7: map や filter の代わりにリスト内包表記を使う

これはもうその通りとしか言いようが無い。リスト内包表記は便利すぎる。他の言語にも欲しいくらいだ。

項目 8: リスト内包表記には 3 つ以上の式を避ける

リスト内包表記は for 文をネストできるが、当然だがやり過ぎると読みにくいので普通に for 文を使うほうが良い。

項目 9: 大きな内包表記にはジェネレータ式を考える

これもその通りとしか言いようがない。Python はリスト内包表記を少し書き換えるだけでジェネレータ式になるので便利だ。

項目 10: range よりは enumerate にする

恥ずかしながら私も range() で書きがちだったのでこれは肝に命じることにする。

for i in range(len(flavor_list)):
    flavor = flavor_list[i]
    ...

よりも

for i, flavor in enumerate(flavor_list):
    ...

の方がずっと簡潔だという話。PHP の foreach, Java の拡張 for 文にあたるものは Python では enumerate として用意されていると覚える。

項目 11: イテレータを並列に処理するには zip を使う

これも Python の便利なところで、他の言語でも zip() が欲しいと思い自分で実装してしまったケースもあるくらいだ。

項目 12: for と while ループの後の else ブロックは使うのを避ける

恥ずかしながら私はこの else が通る場合の条件を理解していなかった:

for i in range(3):
    ...
else:
    ...  # for 分が break されなかった場合に呼ばれる (!!)

Python での else, except, finally のすべての用法から、初めてのプログラマは for/else の else 部分は「ループが完了しなかったらこれをしなさい」という意味だと思い込むものです。

for 文が実行されなかった時 (対象のリストが 0 件だった時) だと思っていた。全然違った。 というわけで、確かに混乱の元なので使わないほうがいいだろう。わかりやすさを好む Python でこんな分かりにくいパーツがあるのが驚いた。

項目 13: try/except/else/finally の各ブロックを活用する

こちらでは前章と違い else も活用せよ と書いてある。しかし、こちらも誤解を呼ぶ (正直パット見分からない) から使わないほうが良いのではないかと思うが……。

写真を効率的にバックアップするのはどうすればよいのか

私は写真を撮るのがそんなに頻繁ではないのだが、写真を撮った後 PC に送ったりする時にどうするのがベストなのかいつも悩んでいた。 この話をすると何故か多くの人が SD カード を使用する事を選択しているように思う。 SD カードで移す話を聞くと、昔 CF (コンパクトフラッシュ) カードに記録するタイプのデジカメを使用していて、それのデータを PC に移す時の事を思い出す。 物理デバイスを差し替えるというのは 1 回 1 回は大した手間ではないのだが、毎回それを行うのはやはり手間に感じるものだ。

iPhone は Lightning ケーブルを Mac に繋いで iTunes で自動なのは知っているが、以下 Android での話。

世の中で Android が使われるようになって Android 端末を microUSB ケーブルで PC と繋いでファイルをやり取りするというのは定着したと思う。 OS X だと Android File Transfer だろう。 Android の良い所は Linux ディレクトリ構造がちゃんと表示されるのでパーミッションが許される場所ならば置きたいところに好きなファイルを置けることだ。 これで写真や音楽やら簡単にやり取りすることができる。が、やはりベストではない。

ケーブルを繋ぐのも面倒なので、いつもカメラで撮影後 Google ドライブにアップロードするという方法を取っていた。 しかし、手動でアップロードするのも面倒だ。カメラアプリで写真を撮った瞬間に勝手にクラウドストレージにバックアップして欲しい。 Dropbox だと昔からこのサービス (写真の自動アップロード) があって凄く便利だった。が、筆者は Dropbox を使っておらず Google ドライブを愛用している。

Google フォトのバックアップ

いつだったか、とはいえ最近だと思うが Android の Google フォトアプリが更新されバックアップの機能が追加された。 Android で Google フォトアプリを開き「設定」->「バックアップと同期」を押下。 するとバックアップの設定が出てくるのでこれを ON にする。 そうすると Android のカメラアプリで撮影すると自分の Google ドライブの「Google フォト」フォルダに自動的にバックアップしてくれるようになる。

一つ、「アップロードサイズ」の設定があり、「高画質」の方だと写真が無制限でバックアップできるというのがある。 これは写真を撮りまくる人には非常に便利なのではないか。 もう一つの設定「元のサイズ (一切圧縮されない)」だともともとの Google ドライブの容量 (2016/01 現在無料枠で 15G) が適用される。 私の場合は写真をあまり撮らないのでこれで十分かもしれない。

しかも Android だけではなく iOS 版もあるし、なんと Windows 版と OS X 版もある。 PC 側で収集した画像も自動で Google ドライブの同一フォルダにバックアップしてくれるわけだ。

Stream API を割と使っている

今参画しているプロジェクトでは Java8 を使用しているのもあって遠慮無く Lambda Expression や Stream API を使用している。 特に Stream API は以前だと簡単な絞り込みをしたい場合でも for ループを回して面倒くさい書き方をしなければいけなかったのがシンプルに書けるようになっていて素晴らしいと思う。思うがやはり Scala なんかと比べると冗長に感じて仕方がないところはある……。

特に以下の collect(Collectors.toList()) は長い。もうすこし何とかならなかったのだろうかと感じる:

public static void main(String[] args) {
    // 途中で List<E> にするのに boxing する必要があるのも地味に辛い
    final List<Integer> list = IntStream.range(1, 11).map(x -> x * x).boxed().collect(Collectors.toList());
}

Scala だと以下で凄くシンプルで羨ましい:

val seq = 1.to(10).map(x => x * x).toSeq

まぁ無い物ねだりをしても仕方がない。Java で飯を食っている以上 Java の枠組みの中でなるべく綺麗なコードを書くように気をつけるしかない。

他の言語でよくやるメソッド内関数が簡単に書けるようになったけどやっぱり辛いという話

例えば PHP や Python などだと lambda 構文もあるし関数内で関数を定義する事も簡単なのでよく使う:

# f(x) 内でしか使用しないが何度も出てくるような関数を f(x) 内で g(y) として定義する事で影響範囲がわかりやすい
def f(x):
    def g(y):
        return y ** 2
    return g(g(x) + 1)

これが Java でやろうとすると Java8 以前だと無名クラスを使用しなければならなくてこれがもうとんでもなく面倒くさかった、というかやろうとも思わなかった:

public static void main(String[] args) {
    final Func<Integer, Integer> f = new Func<Integer, Integer>() {
        @Override
        public Integer apply(Integer x) {
            return x * x;
        }
    };
    System.out.println(f.apply(f.apply(1) + 1));
}

public static interface Func<X, R> {
    R apply(X x);
}

Java8 からは Lambda Expression を用いて以下で書ける:

public static void main(String[] args) {
    final Func<Integer, Integer> f = x -> x * x;
    System.out.println(f.apply(f.apply(1) + 1));
}

public static interface Func<X, R> {
    R apply(X x);
}

更にこのような 1 つの引数を受け取り 1 つの結果を返すような Interface は java.util.function パッケージに用意されているのでわざわざ自分で Interface を定義しなくてもよい:

public static void main(String[] args) {
    final IntUnaryOperator f = x -> x * x;
    System.out.println(f.applyAsInt(f.applyAsInt(1) + 1));
}

OK. これはシンプル。素晴らしい。……とは言い難い。IntUnaryOperator とはなにか?

FunctionalInterface 群の型が覚えられない...

Scala だと全部型推論できるので左辺は val と書けば良い。つまり右辺がどうなっていようが関係ない:

val f = (_: Int) * (_: Int)
val g = (x: Int) => x * x
val h = () => print("Hello")
val i = (x: String) => print(s"Hello $x")

一方、これを Java8 で書こうとすると以下になる:

public static void main(String[] args) {
    final IntBinaryOperator f = (x, y) -> x * y;
    final IntUnaryOperator g = x -> x * x;
    final Runnable h = () -> System.out.println("Hello");
    final Consumer<String> i = x -> System.out.println(String.format("Hello %s", x));
    i.accept("Baka");
}

この BinaryOperator だとか Consumer だとかを覚えないと書けない。 とりあえずひしだまさんの Functional Interface Memo を見て確認すると、以下の 5 つに大別されるのがわかる:

クラス名 概要
Supplier 引数はなく 1 つの結果を返す。供給する (supply) と覚える。
Consumer 1 つの引数を受け取り何も返さない。消費する (consume) と覚える。2 つ受け取って何も返さない BiConsumer というのもある。
Predicate 1 つの引数を受け取りその判定結果 (boolean) を返す。2 つの引数を受け取り判定する BiPredicate というのもある。Stream API の filter() に渡す際によく使う。
Operator 引数の型と同じ型の戻り値を返す。引数の個数により UnaryOperator, BinaryOperator と用意されている。
Function 引数の型と別の型の戻り値を返す。引数の個数により Function, BiFunction と用意されている。

番外で引数も戻り値もない、ただ単に決まった処理をしたいといったものは Runnable を使う。まぁこれはいいだろう。

上記 5 つの大別の中で更に Java には int, long, double, boolean 等のプリミティブ型があるので、その組み合わせぶん全て用意されている。 そこが便利ではあるが分かりにくくしている。例えば DoubleToLongFunction とか IntBinaryOperator とか、そういうものだ……。 しかし、定義しようとしている関数のシグネチャを考えて、上の表に当てはめてみれば多少は考えやすくなるのではないだろうか。つまり、

  1. 引数はあるか? -> 無いなら Supplier
  2. 戻り値はあるか? -> 無いなら Consumer
  3. 戻り値は boolean か (何らかの判定をしたいのか) ? -> そうなら Predicate
  4. 引数と戻り値の型が一緒か? -> そうなら Operator, そうでないなら Function

このチャートで適切なものが選択できるはずだ。

昨日今日と妻が子供たちとママ友の所にお泊まりに行っているので、若干風邪気味なのも相まって自宅に引き篭もってプログラミングを行っていた。 いずれ必要になるので Blog にコメント機能を付加していた。

コメント機能を追加するにはスパム対策が必須

自前の Blog にコメント機能を追加する場合に考慮しなければならない最重要の問題はスパム対策だ。 何も対策をしない場合、手塩にかけて作った Blog が奇妙な URL やメールアドレス、英語などに汚染されたコメントで荒らされてしまう。

日本人のみ投稿できればいいのであれば、投稿する際に日本人しか回答不能な質問を付加して答えさせるのが有効だ。 例えば「十三たす四=」などといった感じだ。この場合 17 が飛んでくればコメントの投稿を許すこととなる。 まぁこれでもいいのだが、折角 Google が提供している画像認証用のライブラリを見つけたのでそれを使ってみた。 reCAPTCHA という。

reCAPTCHA で認証後に Ajax で送る場合

この記事の右下に「コメント投稿」というボタンがあると思うのでそれを押してみてほしい (ただボタンを押しただけではコメントは投稿されないので安心して欲しい)。 そうするとダイアログが開くが、画面左下に「私はロボットではありません」などというチェックボックスが表示される。 これをチェックすると時にはそのままで「人間であることの証明」が終わり、時には追加の証明手段として画像認証が表示される。 どういうロジックになっているのか分からないがすごい。

これの実装の仕方はこの解説サイトがものすごく分かり易かったので興味があればそちらを参照して頂きたい。 あと reCAPTCHA 公式を見るのもいい。

要するに JavaScript のライブラリを読み込んで指定された HTML タグを書くとそこに認証用のビューが表示される。 そこで認証を行うとフォームに g-recaptcha-response という ID, クラスの HTML 要素に認証済を示すトークンが埋め込まれるので、それをそのまま POST するなり Ajax で送るなりすれば良い。そんなに難しくはなかった。つまり jQuery だと以下で認証済トークンが取れる:

var gRecaptchaResponse = $('#g-recaptcha-response').val();

注意点として、この認証済トークンを POST (Ajax) した先でこのトークンが有効なものかの検査 (https://www.google.com/recaptcha/api/siteverify?secret={Secret key}&response={認証コード} に対し GET リクエストを送り戻りの JSON を見る) を行うが、 この g-recaptcha-response は使い捨てなので 1 回チェックを行ってしまうと次からは不正を示す {success: false} が返却されてしまう。 なのでこの検査はバリデーションチェック等がすべて終わりこれさえ通れば正常終了という箇所で行うのが良い。

CsrfViewMiddleware

Django 1.9 の場合最初に manage.py startproject した際に既に settings.py に以下のように CSRF 対策用のミドルウェアが組み込まれた状態となる:

MIDDLEWARE_CLASSES = [
    ....
    'django.middleware.csrf.CsrfViewMiddleware',
    ....
]

これを使う場合は以下のように form タグの中に CSRF 対策用トークンを埋め込む記述を行う:

<form action="." method="post">{% csrf_token %}

正しい CSRF トークンを付与していなかった場合 403 Forbidden が返却される。

jQuery で CSRF トークンを付与したい

最近よくあるのが POST する際に Ajax 通信にして JSON で結果を受け取りエラーだったらそのまま画面にエラーを表示し成功だったら画面遷移を行うといったものだ。 これを Django で使用しようとすると上記 CSRF トークンが邪魔になる。

不要な場合は CSRF ミドルウェアを除去する

他のライブラリを使用したトークンチェック等行っている場合は組み込みの CSRF チェックは必要ないので settings.py の 'django.middleware.csrf.CsrfViewMiddleware', の記述を削除する。

XMLHTTPRequest のヘッダに CSRF トークンを付与する

幸いこのあたりは Django 公式ドキュメントに書いてある。

// using jQuery
function getCookie(name) {
    var cookieValue = null;
    if (document.cookie && document.cookie != '') {
        var cookies = document.cookie.split(';');
        for (var i = 0; i < cookies.length; i++) {
            var cookie = jQuery.trim(cookies[i]);
            // Does this cookie string begin with the name we want?
            if (cookie.substring(0, name.length + 1) == (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}
var csrftoken = getCookie('csrftoken');

のように CSRF トークンが取得できるので以下のように Ajax 時に付与する:

function csrfSafeMethod(method) {
    // these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
    beforeSend: function(xhr, settings) {
        if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
            xhr.setRequestHeader("X-CSRFToken", csrftoken);
        }
    }
});