Edited at

【Kotlin】 OperatorとInfixでCustom operatorを作る

More than 3 years have passed since last update.

Kotlinアドベントカレンダー2015 23日目の記事です。

前日は@laprasDrumさんの「kapt の generateStubs と DI ツールとの関係」でした。


はじめに

CyberAgent2015年度新卒Androidエンジニア AAkira(github), @AAkira(twitter)です。

現在、Kotlinアドベントカレンダー の18日目の執筆者@magie_poohさんと25日の執筆予定者@satorufujiwaraさんと共にAmebaFRESH!という動画サービスをフルKotlin(Dagger等 一部Java)で開発中です。


目次


  1. Operator

  2. Infix

  3. 応用例


    • 2行以上のelvis演算子

    • nullableなif-else




Operator


Custom operator(Swift)

KotlinはAndroidとの相性がバッチリでAndroidで用いられている事が多く、

iOS用言語Swiftと言語仕様がとても似ているので、お互い比較される事が多いように感じます。

そんなSwiftにはcustom operatorという独自に演算子を追加してゴニョゴニョ出来る仕組みがあります。

Swiftではこのように定義することで

infix operator +- { associativity left precedence 140 }

func +- (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y - right.y)
}

+-という独自のoperatorを用いて便利な計算をすることが出来ます。

let firstVector = Vector2D(x: 1.0, y: 2.0)

let secondVector = Vector2D(x: 3.0, y: 4.0)
let plusMinusVector = firstVector +- secondVector
// plusMinusVector is a Vector2D instance with values of (4.0, -2.0)

他にもΣとか等の文字も使えるので数学関係の利用にも適してそうです。

イケてるライブラリとかでよく独自の記号が使われていてカッコいいです!


Operator overloading(Kotilin)

さて本題のKotlinのOperatorについて見ていきましょう

Kotlinでは、前述のSwiftのように好きな記号を定義することは出来ないのですが、(後述あり)

C++のようにOperatorをoverloadする事が出来ます。

例えば、普段何気なく使ってるString同士の足し算。よく使いますよね。

KotlinにはString Templatesがあるので使わなくてもいいのですが、個人的に見難いのとJavaからの流れでちょっとLog出すだけの時等は+で結合してしまいます。

これがどのように実装されているのか、kotlin.String.ktのソースを見るとplusOperatorをoverloadしているのがわかります。


String.kt

public class String : Comparable<String>, CharSequence {

...

public operator fun plus(other: Any?): String

...
}


これによって

val hoge: String = "foo" + "bar"

みたいな事が実現出来ます。

plusをoverload出来るということは引き算や足し算、Rangeや比較演算子も書けます。[Kotlin Expression一覧]

拡張関数と組み合わせるといい感じに書けます。toInt()が微妙ですが…

operator fun String.minus(other: String) = this.toInt().minus(other.toInt())

operator fun String.times(other: String) = this.toInt().times(other.toInt())

val foo = "10" - "5" // 5
val bar = "10" * "5" // 50

ちなみにKotlinの拡張関数は必ずメンバのメソッドが呼び出されるため、既に親クラスに定義されているoperatorを拡張関数で書き換えてもコンパイルエラーにはなりませんが値は変わりません。

拡張関数は何処にでも書けてしまい、優先順位が付けられないので、当然と言えば当然の挙動ですね。

operator fun Int.plus(other: Int) = this.minus(other)

val hoge = 10 + 5 // 15


利用シーン

ThreeTen使えって話ですが、Comparableを実装した日付を表すclassがあったとします。


MyDate.kt

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int) : Comparable<MyDate> {

override fun compareTo(other: MyDate) = when {
year != other.year -> year - other.year
month != other.month -> month - other.month
else -> dayOfMonth - other.dayOfMonth
}
}

さらにcontainsoperatorを持った日付の範囲を計算するDateRangeclassを作成し、拡張関数でMyDateclassにrangeTooperatorを足します。

class DateRange(val start: MyDate, val endInclusive: MyDate) {

operator fun contains(item: MyDate) = start <= item && item <= endInclusive
}

operator fun MyDate.rangeTo(other: MyDate): DateRange = DateRange(this, other)

val date = MyDate(2005, 5, 19)
val first = MyDate(1977, 5, 25)
val end = MyDate(2015, 12, 18)

val hoge = date in first..last // true

このようにOperatorを独自のclassに足すことが出来るので、

Javaでは出来なかった様々な事が行えるようになりました!

便利ですねKotlin(*´ω`*)


Infix

Kotlinには上記のOperatorと似た機能としてInfixというものがあります。

上記でのOperatorの説明ではSwiftのように好きな記号を定義することは出来ないと書いたのですが、

なんとKotlinはメソッド名で使える文字列に関してはCustom Operatorを作ることが出来ます!!


(12/21 11:30補足)


記号は定義出来ないと書いたのですが、たろうさんにアクサン グラーブを使うと書けなくは無いと教えていただきました!


infix fun Int.`**`(n: Int) = ...

5 `**` 3 //=> 125


ただ、本来はJavaコード上での識別子がKotlinの予約語とぶつかったときの対処法らしいので本来避けるべき書き方みたいです。


例えばBooleanはこの様に書いても比較することが出来ます。

val foo = true

val bar = true

// pattern 1
if(foo and bar) return

// pattern 2
val hoge = when {
foo or bar -> 100
else -> 0
}

Booleanのソースを見ると、このように定義されています。


Boolean.kt

public class Boolean private () : Comparable<Boolean> {

/**
* Returns the inverse of this boolean.
*/
public operator fun not(): Boolean

/**
* Performs a logical `and` operation between this Boolean and the [other] one.
*/
public infix fun and(other: Boolean): Boolean

/**
* Performs a logical `or` operation between this Boolean and the [other] one.
*/
public infix fun or(other: Boolean): Boolean

/**
* Performs a logical `xor` operation between this Boolean and the [other] one.
*/
public infix fun xor(other: Boolean): Boolean

...
}


先程のoperatorinfixが定義されてますね。

つまり、Kotlinでもinfixを利用すればcustom operatorが作れるということです。


応用例


2行以上のElvis演算子

先日kotlinlang-jpのslack

null checkは「if else」でするか、「Elvis演算子」でするかが話題になっていたのですが、

個人的にKotlinでは、比較にnullを使いたく無いと思っています。(代入や戻り値以外でnullを書きたくない)

そのため普段コードを書く時には

val foo: Int? = null

foo = 100

val hoge = foo ?: 0

のようにして代入を行ったり

fun hoge(value: Int?) {

valure ?: return
...
}

のようにして早期returnをおこなっているのですが、エルビス演算子は後ろに1行しかとれない問題があります。

現状チームとしてはrunを使って2行以上の処理を行っています。

fun generator() = 10 * 10

val foo: Int? = null
foo = 100

// possible
val hoge = foo ?: 0

// possible
val hoge = foo ?: generator()

// possible
val hoge = foo ?: run {
// 2行以上の処理
}

// impossible
val bar = true
val hoge = foo ?: {
// 2行以上の処理
val a = if(bar) 10 else 100
a * generator()
}

そこで、infixを用いて以下の様な関数を作成します。

inline infix fun <R> R?.elvis(f: () -> R): R = this ?: f()

これを用いると先ほど書けなかった式のコンパイルが通ります。(名前はもう少し考えたいですね…)

// possible :-D

val hoge = foo elvis {
// 2行以上の処理
val a = if(bar) 10 else 100
a * generator()
}


nullableなif-else

他に使えそうな箇所としては、

ifで比較をしたい時に中身がnullableだとKotlinのコンパイラはエラーを出します。

nullだったらfalseの方にして欲しいけど、比較前にsmart castして…と結構手間なことがあります。

そんな時に先ほど自作したelvis関数とinfixを組み合わせて このように書けます。

val hoge: Int? = null

inline infix fun <R> nullableIf(value: Boolean?, f: () -> R): R? {
value ?: return null
return if (value) f() else null
}

nullableIf(hoge == 0) {
// don't be called here
} elvis {
// be called here
}

凄いぞKotlin!!

なんだかんだKotlin歴6ヶ月になりますが、この記事を書いてる時に初めてこんな事が出来る!!って気づきました。

KotlinにはrequireNotNull()等の便利なメソッドが標準で多数用意されているので、

もしかするとこんな事をしないでもいい感じに実現出来るのかもしれないですが、

他の用途にも応用出来ると思うので面白い使い方があったら教えて下さい!

以上operatorinfixについてお話をしました。

何か間違い等ありましたら、@AAkira, Mailに連絡を頂けると有り難いです。

Slackにもいるので是非みなさんで議論しましょう!


明日はrabitarochanさんです。