Kotlin:SAM変換

投稿日:  更新日:

Androidのアプリを作成しているとSAM変換が頻繁に登場します。

プログラムが簡素に記述できることから、プログラマーに好評なようです。

Kotlinが関数を第一級オブジェクトとして扱える恩恵です。

しかし、私は経験のないSAM変換に出合うと、いつも内容が把握できなくて戸惑います。

なので、苦手です。あまり好きではありません。

苦手を克服するために、もっとSAM変換の理解を深めてみることにしました。

SAM変換についてまとめます。

スポンサーリンク

SAM変換とは

PlayerクラスにplayCD( )関数を委譲して、CDPlayerオブジェクトを作るプログラムをKotlinで記述すると、次のようになります。

CDPlayerクラスを作るプログラムのプロローグ
総称して「プレーヤー(Player)」と呼ばれる物の中には、アプリケーションならばAudioプレーヤー、Videoプレーヤー、機器ならばCDプレーヤー、DVDプレーヤー、などが考えられます。

これらの共通点は、再生(Play)・一時停止(Pause)・停止(Stop)が出来ることです。ただし、Playを行うための具体的な処理はプレーヤー毎に異なります。PauseとStopも同様です。

CDプレーヤーをプログラムで実現する方法を考えてみると…

play()・pause()・stop()関数(またはメソッド)を持つPlayerクラスを作成して、CDプレーヤ用に具体的な処理を実装したplayCD()・pauseCD()・stopCD()を委譲してあげれば、CDプレーヤーが作れそうです。

class Player {
    var playCtrl: ((String)->Unit)? = null
    ...
    fun play(title: String) {
        playCtrl?.invoke(title)	             // 委譲された関数の実行
    }
    ...
    fun setPlayCtrl(ctrl: (String)->Unit) {  // 関数オブジェクトを引数で受ける
        playCtrl = ctrl
    }
    ...
}
        fun playCD(title: String) {
            println("CDで「${title}」を再生する!")
        }
        ...
        val _CDPlayer = Player().apply {
            setPlayCtrl(::playCD)	                // 関数オブジェクトを引数で渡す
            ...
        }
        _CDPlayer.play("SampleMusic")
        ...

関数オブジェクトを渡し、関数オブジェクトを引数に持つ高階関数で受ける形になります(※1)

関数オブジェクトは関数の記述そのものを格納しています。「関数がどのクラスに属しているか」といった所在の情報は失われています。

JVM(Java Virtual Machine)はクラスに属さないメソッド(関数)を処理できません。所在の情報を持たない関数オブジェクトのままでは、委譲ができません。

従って、関数オブジェクト内の関数は、内部的に「SAM型のインターフェースを実装したクラスに属するメソッド(関数)」へ変換されます。ここで定義されたクラスが関数オブジェクトを表しています。
※SAM(Single Absolute Method):メソッド(関数)が一つしかない抽象クラス

この変換をSAM変換といいます。

また、「SAM型のインターフェース」のことを関数型インターフェースと言います。

SAM変換とは

同様に、関数オブジェクトを引数に持つ高階関数も、関数型インターフェースを受けるメソッドに変換されます。

(※1)で示したように、ソースコード上は渡し側も受け側も、引数が同じ関数オブジェクトになっています。なので、プログラム文法上の違和感がなく、内部で行われているSAM変換が確認できないケースです。

スポンサーリンク

多用されるSAM変換

「SAM変換とは」で示したCDPlayerオブジェクトを作るプログラムを、PlayerクラスがJavaで記述された場合で考えてみると、次のようになります。

class Player {
    private PlayCtrl mPlayCtrl = null;
    ...
    public void play(String title) {
        if(mPlayCtrl != null) mPlayCtrl.play(title); // 委譲された関数の実行
    }
    ...
    public void setPlayCtrl(PlayCtrl ctrl) { // 関数型インターフェースを引数で受ける
        mPlayCtrl = ctrl;
    }
    interface PlayCtrl {
        void play(String title);
    }
    ...
}
        fun playCD(title: String) {
            println("CDで「${title}」を再生する!")
        }
        ...
        val _CDPlayer = Player().apply {
            setPlayCtrl(::playCD)	// 関数オブジェクトを引数で渡す
			...
        }
        _CDPlayer.play("SampleMusic")
        ...

関数オブジェクトを渡し、関数型インターフェースを引数に持つメソッドで受ける形になります(※2)

多用されるSAM変換

(※2)で示したように、ソースコード上は渡し側と受け側の引数が異なっています。なので、プログラム文法上の違和感がありますが、内部で行われるSAM変換の効果で受け渡しができてしまいます。SAM変換が確認できるケースです。

Androidアプリのプログラムは(※2)のケースが多用されます。次のようなインターフェースが使われる場面です。

  OnClickListener#onClick(v: View)
  OnLongClickListener#onLongClick(v: View)
  Runnable#run( )
  …

Java APIやAndroid APIがJavaで記述されており、それらライブラリをKotlinから呼出すためです。

スポンサーリンク

SAM変換の例

Kotlinは「プログラムが簡素に書ける」という特徴を持っています。不要な部分を省略できるからです。

特にSAM変換が行われる部分はラムダ式を利用します。ラムダ式は省略の方法が色々あります。知っていないとプログラムを読む時に苦労します。

以下にSAM変換の例を省略の方法と共に示します。

OnClickListener#onClick(v: View)

        // ★関数オブジェクトの参照値を受け渡す
		//   省略できる箇所なし
        fun onClickB(v: View) {
            println("${v.id}ボタンが押されました!")
        }
        btn.setOnClickListener(::onClickB)
		
		// ★無名関数を受け渡す
		//   省略できる箇所なし
        btn.setOnClickListener(fun(v: View){ println("${v.id}ボタンが押されました!") })

        // ★ラムダ式(無名関数)を受け渡す
        //   原形
        btn.setOnClickListener({ v: View -> println("${v.id}ボタンが押されました!") })
        //   引数リストの最後がラムダ式ならカッコの外に出せる
        btn.setOnClickListener(){ v: View -> println("${v.id}ボタンが押されました!") }
        //   引数のカッコが空なので削除可能
        btn.setOnClickListener{ v: View -> println("${v.id}ボタンが押されました!") }
        //   ラムダ式の引数の型が明確な場合(ここではView)は削除可能
        btn.setOnClickListener{ v-> println("${v.id}ボタンが押されました!") }
        //   ラムダ式の引数が一つの場合は削除可能、引数はitで参照
        btn.setOnClickListener{ println("${it.id}ボタンが押されました!") }

OnLongClickListener#onLongClick(v: View)

        // ★関数オブジェクトの参照値を受け渡す
		//   省略できる箇所なし
        fun onLongClickB(v: View): Boolean {
            println("${v.id}ボタンが押されました!")
            return true
        }
        btn.setOnLongClickListener(::onLongClickB)
		
		// ★無名関数を受け渡す
		//   省略できる箇所なし
        btn.setOnLongClickListener(
            fun(v: View): Boolean{
                println("${v.id}ボタンが押されました!")
				return true
            }
        )

        // ★ラムダ式(無名関数)を受け渡す
        //   原形
        btn.setOnLongClickListener({ v: View -> println("${v.id}ボタンが押されました!");true })
        //   引数リストの最後がラムダ式ならカッコの外に出せる
        btn.setOnLongClickListener() { v: View -> println("${v.id}ボタンが押されました!");true }
        //   引数のカッコが空なので削除可能
        btn.setOnLongClickListener{ v: View -> println("${v.id}ボタンが押されました!");true }
        //   ラムダ式の引数の型が明確な場合(ここではView)は削除可能
        btn.setOnLongClickListener{ v -> println("${v.id}ボタンが押されました!");true }
        //   ラムダ式の引数が一つの場合は削除可能、引数はitで参照
        btn.setOnLongClickListener{ println("${it.id}ボタンが押されました!");true }
スポンサーリンク

関連記事:

Kotlinは関数を変数に代入したり、引数で受け渡したりできます。 関数を第一級オブジェクトで扱えるためです。 これにより、関数を使った処理の委譲が容易になりました。ArrayOf#forEach( )がその典型的な例です。 以上のような操作で必要となる「関数オブジェクト」について、まとめます。 ...
続きを読む
クロージャ(Closure)は新しい概念ではなく、関数型プログラミングに古くから存在していました。 手続き型やオブジェクト指向プログラミングにも、だいぶ前から採用が進んでいます。 Kotlinもクロージャが使える言語の1つです。 ...
続きを読む
近頃の携帯端末はクワッドコア(プロセッサが4つ)やオクタコア(プロセッサが8つ)が当たり前になりました。 サクサク動作するアプリを作るために、この恩恵を使わなければ損です。 となると、必然的に非同期処理(マルチスレッド)を使うことになります。 JavaのThreadクラス、Android APIのAsyncTaskクラスが代表的な手法です。 Kotlinは上記に加えて「コルーチン(Coroutine)」が使えるようになっています。 今回は、このコルーチンについて、まとめます。 ...
続きを読む
スポンサーリンク