Help us understand the problem. What is going on with this article?

【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さんです。

AAkira
Android Engineer, Kotlin
http://aakira.app
cyberagent
サイバーエージェントは「21世紀を代表する会社を創る」をビジョンに掲げ、インターネットテレビ局「AbemaTV」の運営や国内トップシェアを誇るインターネット広告事業を展開しています。インターネット産業の変化に合わせ新規事業を生み出しながら事業拡大を続けています。
http://www.cyberagent.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした