転職します
本日(2012年5月31日)をもって、現在お世話になっている会社を退社し、明日から別の会社に入社します。
他所様に伝えるべき何かを持ち合わせてはいないのですが、私に職を紹介してくれたN氏、現職の方々、次職の方々への私信という事で慣れない筆を執りました。
現職について
参入障壁が高く競合他社が少ない、安定した収入を確保できる職場を去るにあたり、様々な葛藤がありましたが、解くべき問題の難易度が低いという理由だけで転職を決めました。ありがちな理由で簡単に詳細を予測できる内容ではありますが、少しだけ補足します。
勤続7年(グループ会社通算で10年)*1の中で、サービスを安定稼働させるために様々な試行錯誤を重ねてきました。あまり詳しい話は守秘義務があるためできませんが、俗にいう上流から下流工程まで様々な事に手を入れてきました。しかし、人間には趣味嗜好があり、私という人間がプログラミングに喜びを感じる性格である都合上、ついプログラミングに比重を置いてしまう傾向があります。過去、本ブログに書き綴った内容も8割以上がサービスの安定稼働という一念あって書き綴ったものですが、ほぼプログラミングに特化した内容となっています。そうやってプログラミングに偏重していくと「現状」のサービスの安定稼働という目標を達成するための十分な技術を獲得しているにも関わらず、プログラミング技術への枯渇感が満たされないので、更にプログラミング技術を追い求め、いつしか「難しい」問題は、社内外の政治的な問題だけになっていました。現行のサービスの限界を越える案件が発生した際、現行のサービスの同等品を Erlang を用いて一人/二ヶ月でフルスクラッチで構築する段階になると、かなり傲慢な考え方ですが、現職に留まる限り、プログラミングで解決するべき難しい問題には出会えない…という考えに確信を持つに至りました。勿論、自ら問題を創出するという方法もあるとは思いますが、その場合、地位*2か企業内起業のどちらかが必要となるので、難しい問題を抱えている職場を探した方が早いだろうと判断しました。
勿論、世の中、きれいごとばかりではありませんし、在職中に不愉快な思いをした事もあります。しかし、今回の転職に前述以外の他意はありません。それが引き金だったのではないかと心配されている方々もいらっしゃるでしょうが、その程度の事で職を放棄したりしません。良い上司・同僚・部下に恵まれ、素晴らしい環境の中で仕事に集中できた事に関しては疑う余地がありません。もし関係があるとしたら、政治的な理由でプログラミングの時間を削られてしまったという箇所のみです。とは言え、辞めるのは確かであり、社内外問わず関係者の方々に多大なるご迷惑を掛けました事を、ここにお詫び申し上げます。誠に申し訳ございませんでした。
また、多々ご迷惑を掛けたにも関わらず、現職の社長が次職の社長に対して、私の事をよろしく頼むという一報を入れてくれたり*3、10年間、本当の意味で私と苦楽を共に*4してきた取締役が、私を修行に出すだけというスタンスで送り出してくれた事により、私の中の罪悪感を打ち消してくれたりと…、転職が決まった後も様々な形でご支援頂き、現職の方々に対しては、本当に感謝の念に堪えません。今まで、本当にありがとうございました。
次職について
N氏の Erlang 雇用を創出するという陰謀に加担する予定です。希望としては、(人的にもサーバ的にも)少ないリソースで大量トラフィックを捌くサービス基盤やログ解析基盤を構築する事に注力したいと考えていますが、現行サービスとの折り合いを見つつ臨機応変に対応したいと考えています。とは言え、研究投資とは名ばかりのお荷物確定の働きっぷりでは信用を得られないので、いきなり (」・ω・)」Erlang!(/・ω・)/Scala!*5 と騒ぐつもりは無く、現職同様に少しずつ侵略していければ良いと考えています。そんな呑気な事では給料を払う意味が無い!と次職の上司にお叱りを受けそうで怖いのですが…、そこは、お手柔らかにお願い申し上げます。
さて、明日の初出社に備えて寝ます。おやすみなさい。
Scala で Android アプリ開発(NDK 編)
下準備
始めに /path/to/hello-world/project/build.scala を次のように修正する。
// ..snip.. object AndroidBuild extends Build { lazy val main = Project ( "Hello World", file("."), settings = General.fullAndroidSettings ++ AndroidNdk.settings // これを追加 ) // ..snip.. }
次に /path/to/hello-world/src/main/jni/ を作成する。
$ make -p /path/to/hello-world/src/main/jni/
ラッパークラスを作る
ものは試しにファイルの情報を取得するラッパ /path/to/hello-world/src/main/scala/FileStat.scala を作成する。
package com.github.cooldaemon.HelloWorld import _root_.java.io.File import _root_.java.util.{Map => JMap} import _root_.scala.collection.JavaConversions._ object extendFileStat { implicit def fileToFileStat(file: File): FileStat = new FileStat(file) } class FileStat(file: File) { System.loadLibrary("file_stat") @native private[this] def getStat(name: String): JMap[String, Long] def stat: Map[String, Long] = mapAsScalaMap(getStat(file.toString)).toMap }
C 関連のファイルを用意する
.class ファイルを作るため一度コンパイルし、その後、javah でラッパクラスから C ヘッダファイルを作成する。
$ cd /path/to/hello-world $ sbt > compile > exit $ javah -o ./src/main/jni/file_stat.h -classpath ./target/scala-2.9.1/classes com.github.cooldaemon.HelloWorld.FileStat
sbt 経由で javah を実行する方法は試していない。
Makefile として /path/to/hello-world/src/main/jni/Android.mk を作成する。
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := file_stat LOCAL_SRC_FILES := file_stat.c #LOCAL_C_INCLUDES += $(SBT_MANAGED_JNI_INCLUDE) include $(BUILD_SHARED_LIBRARY)
最後にファイル本体 /path/to/hello-world/src/main/jni/file_stat.c を作成する。
#include <sys/stat.h> #include "file_stat.h" jobject NewLong(JNIEnv* env, jlong value) { jclass longClass = (*env)->FindClass(env, "java/lang/Long"); jmethodID init = (*env)->GetMethodID(env, longClass, "<init>", "(J)V"); jobject longObj = (*env)->NewObject(env, longClass, init, value); (*env)->DeleteLocalRef(env, longClass); return longObj; } JNIEXPORT jobject JNICALL Java_com_github_cooldaemon_HelloWorld_FileStat_getStat (JNIEnv *env, jobject obj, jstring name) { jclass mapClass = (*env)->FindClass(env, "java/util/HashMap"); jmethodID init = (*env)->GetMethodID(env, mapClass, "<init>", "(I)V"); jobject mapObj = (*env)->NewObject(env, mapClass, init, 1); jmethodID put = (*env)->GetMethodID(env, mapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); jboolean iscopy; const char *mfile = (*env)->GetStringUTFChars(env, name, &iscopy); struct stat finfo; lstat(mfile, &finfo); (*env)->CallObjectMethod(env, mapObj, put, (*env)->NewStringUTF(env, "dev"), NewLong(env, finfo.st_dev) ); (*env)->CallObjectMethod(env, mapObj, put, (*env)->NewStringUTF(env, "ino"), NewLong(env, finfo.st_ino) ); (*env)->CallObjectMethod(env, mapObj, put, (*env)->NewStringUTF(env, "mode"), NewLong(env, finfo.st_mode) ); (*env)->CallObjectMethod(env, mapObj, put, (*env)->NewStringUTF(env, "nlink"), NewLong(env, finfo.st_nlink) ); (*env)->CallObjectMethod(env, mapObj, put, (*env)->NewStringUTF(env, "uid"), NewLong(env, finfo.st_uid) ); (*env)->CallObjectMethod(env, mapObj, put, (*env)->NewStringUTF(env, "gid"), NewLong(env, finfo.st_gid) ); (*env)->CallObjectMethod(env, mapObj, put, (*env)->NewStringUTF(env, "rdev"), NewLong(env, finfo.st_rdev) ); (*env)->CallObjectMethod(env, mapObj, put, (*env)->NewStringUTF(env, "size"), NewLong(env, finfo.st_size) );
コンパイルしてみる。
$ sbt > android:ndk-build
使用する
val f = new File(context.getFilesDir, "foo") import extendFileStat._ f.stat foreach {case (k, v) => Log.d(k + ":" + v.toString)}
Scala で Android アプリ開発(AlertDialog 編)
毎回、AlertDialog を作るのは面倒なので、次のようなオブジェクトを作っておく。
package com.github.cooldaemon.HelloWorld import _root_.android.content.Context import _root_.android.app.{Dialog, AlertDialog => AAlertDialog} import _root_.android.content.DialogInterface object AlertDialog { def create(message: String)(f: () => Unit)(implicit c: Context): Dialog = { (new AAlertDialog.Builder(c)) .setMessage(message) .setCancelable(true) .setPositiveButton("はい", new DialogInterface.OnClickListener() { override def onClick(dialog: DialogInterface, id: Int) { f.apply() dialog.dismiss() } }) .setNegativeButton("いいえ", new DialogInterface.OnClickListener() { override def onClick(dialog: DialogInterface, id: Int) { dialog.cancel() } }) .create() } }
次のように onCreateDialog 内で使用する。
class FooActivity extends TypedActivity { // ..snip.. object DialogID extends Enumeration { val WIFI,BLUETOOTH = Value } override protected def onCreateDialog(id: Int, args: Bundle): Dialog = id match { case id if id == DialogID.WIFI.id => AlertDialog.create("Wi-Fi設定を行う") { () => startActivity(new Intent(android.provider.Settings.ACTION_WIFI_SETTINGS)) } case id if id == DialogID.BLUETOOTH.id => AlertDialog.create("Bluetooth設定を行う") { () => startActivity(new Intent(android.provider.Settings.ACTION_BLUETOOTH_SETTINGS)) } } // ..snip.. }
Scala で Android アプリ開発(AsyncTask 編)
ScalaでAndroidアプリ作成時、AsyncTaskの可変長引数メソッドが使えないことへの対策とサンプル ― Gist を丸パクリ。
Scala は、可変長引数を持つメソッドを上書きできないので、可変長引数を上書き済みの /path/to/hello-world/src/main/java/com/github/cooldaemon/HelloWorld/AsyncTask.java を作成する。
package com.github.cooldaemon.HelloWorld; public abstract class AsyncTask<Params, Progress, Result> extends android.os.AsyncTask<Params, Progress, Result> { @Override protected Result doInBackground(Params... params) { return doInBackground(params.length > 0 ? params[0] : null); } abstract protected Result doInBackground(Params param); @Override protected void onProgressUpdate(Progress... values) { onProgressUpdate(values.length > 0 ? values[0] : null); } protected void onProgressUpdate(Progress value) {} @SuppressWarnings({"unchecked"}) protected final void publishProgress(Progress value) { super.publishProgress(value); } }
用意した AsyncTask を継承して、別スレッドで処理したいタスクを記述する。
//可変長引数を使えない分は、case class で対応する case class FooParam(foo: String, bar: String) // AsyncTask から Activity を操作する(doInBackground は除く)ために Activity のインスタンスを渡しておく class FooTask(val activity: FooActivity) extends AsyncTask[FooParam, Int, Either[Throwable, Unit]] { override protected def onPreExecute() { // Activity に変化を加える。例えばダイアログの表示など } // doInBackground だけ別スレッドで動作する // doInBackground 内で publishProgress を使うと UI のスレッドに Int の値を送れる // 送った Int の値は onProgressUpdate で受け取れる override protected def doInBackground(param: FooParam): Either[Throwable, Unit] = { for { _ <- ham(param).right chick <- egg(param).right _ <- spam(param, chick).right } yield () } override protected def onProgressUpdate(progress: Int) { // プログレスの更新 } // AsyncTask の cancel メソッドを使用されると onCancelled が呼ばれる override protected def onCancelled() { // doInBackground の停止を促す。例えば doInBackground で参照しているフラグを onCancelled で更新するなど } override protected def onPostExecute(result: Either[HttpClientErrorResult, Unit]) { // Activity に変化を加える。例えばダイアログを消すなど } }
このコードは、画面の回転に対応していない。
画面が回転すると Activity のインスタンスは置き換えられるが AsyncTask は残り続けるので AsyncTask が保持する Activity のインスタンスを置き換える必要がある。
また、画面の回転と ProgressDialog を組み合わせる場合、画面回転後に ProgressDialog を再表示する必要があり、onCreateDialog に頼る事になる。
ProgressDialog を再表示する場合、以前の ProgressDialog 進行状況を引き継いだり、キャンセル処理を正常に動作させる必要があるので、なるべく画面が回転しないように固定しておいた方が良い。
もし、どうしても AsyncTask + ProgressDialog + 画面回転に対応したいのであれば、Activity から操作できる AsyncTask と ProgressDialog を管理する object を用意する。
この辺りは、ダウンローダ編で詳しく解説する。
Scala で Android アプリ開発(Log 編)
ログを出力する際、ログ出力位置の情報も一緒に出力する。
package com.github.cooldaemon.HelloWorld import _root_.android.util.{Log => ALog} object Log { val TAG = "HelloWorld" def e(m: String) = printlog(ALog.e, m) def w(m: String) = printlog(ALog.w, m) def i(m: String) = printlog(ALog.i, m) def d(m: String) = printlog(ALog.d, m) def v(m: String) = printlog(ALog.v, m) private def printlog(f: (String, String) => Int, m: String): Int = { val st = ((new Throwable()).getStackTrace).apply(2) f(TAG, "%s.%s(%s:%s): %s".format( st.getClassName, st.getMethodName, st.getFileName, st.getLineNumber, m )) } }
製品出荷時には、ログを出力しないよう注意。
Scala で Android アプリ開発(ActionBarSherlock 編)
ActionBar は使いたいけれど、ターゲットの API Level が低い場合に重宝する ActionBarSherlock を Scala から使う。
sbt プロジェクト設定
まずは依存するライブラリを指定する。準備編を参考に作成したディレクトリ /path/to/hello-world の直下に main.sbt を作成する。
import sbt._ import Keys._ import AndroidKeys._ libraryDependencies ++= Seq( "com.actionbarsherlock" % "library" % "4.0.0-SNAPSHOT" artifacts(Artifact("library", "apklib", "apklib")), "android" % "compatibility-v4" % "r3-SNAPSHOT" )
次に依存するライブラリの位置を指定する。/path/to/hello-world/project/build.scala を次のように修正する。
import sbt._ import Keys._ import AndroidKeys._ object General { val settings = Defaults.defaultSettings ++ Seq ( name := "Hello World", version := "0.1", versionCode := 0, scalaVersion := "2.9.1", platformName in Android := "android-15", // android-10 から android-15 に変更 resolvers += "ActionBarSherlock snapshots" at "http://r.jakewharton.com/maven/snapshot/" // 新規追加 ) // 以下、変更が無いので省略
最後に依存ライブラリを取得する。
$ cd /path/to/hello-world $ sbt > update // 省略 [info] Resolving com.actionbarsherlock#library;4.0.0-SNAPSHOT ... [info] Resolving android#compatibility-v4;r3-SNAPSHOT ... // 省略 [info] Done updating. [success] Total time: 4 s, completed 20XX/XX/XX XX:XX:XX > exit
API Level の修正
AndroidManifest.xml 内の
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.drops_market.Downloader"> <!-- 変更が無いので省略 --> <!-- android:minSdkVersion="10" から変更 --> <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="15"/> </manifest>
Activity から使用する
package com.github.cooldaemon.HelloWorld import _root_.android.app.Activity import _root_.android.os.Bundle import _root_.android.content.Context // 新規に追加 import _root_.com.actionbarsherlock.app.SherlockActivity import _root_.com.actionbarsherlock.view.{Menu, SubMenu, MenuItem} // Activity を SherlockActivity に変更 class MainActivity extends SherlockActivity with TypedActivity { implicit lazy val c: Context = this override def onCreate(bundle: Bundle) { setTheme(R.style.Theme_Sherlock) // テーマを設定 super.onCreate(bundle) setContentView(R.layout.main) findView(TR.textview).setText("hello, world!") } object MenuID extends Enumeration { val FOO,BAR,BAZ = Value } // 試しにメニューを追加してみる override def onCreateOptionsMenu(menu: Menu): Boolean = { menu .add(0, MenuID.FOO.id, Menu.NONE, "Foo") .setIcon(android.R.drawable.ic_menu_view) .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) val subMenu = menu.addSubMenu(0, MenuID.BAR.id, Menu.NONE, "Bar") subMenu.getItem .setIcon(android.R.drawable.ic_menu_more) .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) subMenu .add(0, MenuID.BAZ.id, Menu.NONE, "Baz") .setIcon(android.R.drawable.ic_menu_add) .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM) true } override def onOptionsItemSelected(item: MenuItem): Boolean = { item.getItemId match { case id if id == MenuID.FOO.id => AtomicToast.show("Selected Foo") case id if id == MenuID.BAR.id => AtomicToast.show("Selected Bar") case id if id == MenuID.BAZ.id => AtomicToast.show("Selected Baz") case _ => } true } }
Toast 編で作成した AtomicToast を利用している。
テーマとして何が使えるか?SHOW_AS_ACTION_IF_ROOM って何よ?という疑問をお持ちの方は Google 先生にお尋ねください。
/path/to/hello-world/main.sbt に次のような設定していると…
javacOptions ++= Seq("-Xlint:unchecked") scalacOptions ++= Seq("-verbose", "-unchecked", "-deprecation")
ものすごい数の ActionBarSherlock 関連の警告が出力されるが、ご愛嬌という事で。
Scala で Android アプリ開発(Toast 編)
Toast 表示中に Toast を表示すると問題があるらしい*1ので、次のようなラッパーオブジェクトを用意する。
package は、準備編で用意したもの。
package com.github.cooldaemon.HelloWorld import _root_.android.content.Context import _root_.android.widget.Toast object AtomicToast { private[this] var toast: Toast = null def show(message: String)(implicit c: Context) = synchronized { if (toast != null) toast.cancel() toast = Toast.makeText(c, message, Toast.LENGTH_LONG) toast.show() } }
これを Activity から使うには、次のようにする。
package com.github.cooldaemon.HelloWorld // 省略 class MainActivity extends Activity with TypedActivity { implicit lazy val c: Context = this // 省略 // 何らかのメソッドの中で… AtomicToast.show("Selected Foo") // 省略 }
implicit で Context を渡すのが嫌な方は適宜修正してご利用ください。
*1:先に本記事のような対策をしてしまったので、困った経験がない