interprism's blog

インタープリズム株式会社の開発者ブログです。

KotlinでInline Constructorっぽいものをつくる

こんにちは、andoです。

KotlinはBetter Javaとしてとても使いやすく弊社の開発でも採用が増えています。 Javaから多くの改善が行われていますが、Javaとの相互運用のため制限となっている機能もあり、その一つに型消去があります。
型消去については詳しく書きませんが、Javaでは総称型のメソッド、コンストラクタの実行時に型パラメータの情報を取得することがでません。 Kotlinの場合、関数については具象化された型パラメータを用いることで実行時に型情報を取得できるのですが、コンストラクタについては具象化された型パラメータは使用できないため、型情報を取得できません。
今回はそのコンストラクタ(っぽい)で型情報を取得してみたというお話です。

型引数から情報を取得

関数編

O/Rマッパーにありそうなレコードを取得するselect()の定義を考えてみます。
テーブルの型や型制約については本題ではないので省略させてください。

できればこう書きたい

この記述で動作すればシンプルで良いのですが、型消去により関数の実行時に呼び出し側からTの型情報が渡されないためコンパイルエラーとなります。

fun <T> select(): Sequence<T> {
    println(T::class) // Cannot use 'T' as reified type parameter. Use a class instead.
    return TODO("not implemented")
}

fun main() {
    val xs = select<String>()
}

Javaっぽく書く

Javaでは引数にClassを渡すのが定石です。
関数の定義は冗長ですが、呼び出す側の型パラメータは型推論により省略できます。

fun <T> select(t: Class<T>): Sequence<T> {
    println(t)
    return TODO("not implemented")
}

fun main() {
    val xs = select(String::class.java)
}

具象化された型パラメータ

reifiedを使用します。reifiedを使用する場合はinlineも宣言する必要があります。
また、synthetic methodとなるためJavaから呼び出すことはできなくなります。

inline fun <reified T> select(): Sequence<T> {
    println(T::class)
    return TODO("not implemented")
}

fun main() {
    val xs = select<String>()
}

コンストラクタ編

O/Rマッパーにありそうなカラムの値を取得する委譲プロパティの定義を考えてみます。
getValueinlineにして引数と戻り値の型をreifiedとしています。

abstract class Entity

class PropertyDelegate {
    inline operator fun <reified E : Entity, reified P> getValue(receiver: E, property: KProperty<*>): P {
        println(E::class)
        println(P::class)
        return TODO("not implemented")
    }
}

class Person : Entity() {
    val id: Int by PropertyDelegate()
    val name: String by PropertyDelegate()
}

fun main() {
    val person = Person()
    val id = person.id
}

もう一つ型引数を追加すると、、、

getValueに型パラメータを追加してもレシーバと戻り値以外は型推論できずコンパイルエラーとなります。 そのため、型パラメータをコンストラクタに追加してみますが、関数の場合と同様にコンストラクタの実行時に呼び出し側からTの型情報が渡されないためコンパイルエラーとなります。

class PropertyDelegate<T> {
    inline operator fun <reified E : Entity, reified P> getValue(receiver: E, property: KProperty<*>): P {
        println(E::class)
        println(P::class)
        println(T::class) // Cannot use 'T' as reified type parameter. Use a class instead.
        return TODO("not implemented")
    }
}

class Person : Entity() {
    val id: Int by PropertyDelegate<String>()
    val name: String by PropertyDelegate<String>()
}

具象化された型パラメータ

コンストラクタは具象化された型パラメータを扱えないため、いずれもコンパイルエラーとなります。

// inlineの宣言がありません。
class PropertyDelegate<reified T> { // Only type parameters of inline functions can be reified
    ...
}
// コンストラクタにinline修飾子は適用できません。
class PropertyDelegate<reified T> inline constructor() { // Modifier 'inline' is not applicable to 'constructor'
    ...
}
// inline classは記述できますが別の機能です。
inline class PropertyDelegate<reified T> { // Only type parameters of inline functions can be reified
    ...
}

Javaっぽく書くしかない?

動きますが、、、書きたくないでござる!

class PropertyDelegate<T>(val t: Class<T>) {
    inline operator fun <reified E : Entity, reified P> getValue(receiver: E, property: KProperty<*>): P {
        println(E::class)
        println(P::class)
        println(t)
        return TODO("not implemented")
    }
}

class Person : Entity() {
    val id: Int by PropertyDelegate(String::class.java)
    val name: String by PropertyDelegate(String::class.java)
}

ファクトリ関数経由で簡潔に

Kotlinらしくなりました。

class PropertyDelegate<T>(val t: Class<T>) {
    inline operator fun <reified E : Entity, reified P> getValue(receiver: E, property: KProperty<*>): P {
        println(E::class)
        println(P::class)
        println(t)
        return TODO("not implemented")
    }
}

inline fun <reified T> delegate() = PropertyDelegate(T::class.java)

class Person : Entity() {
    val id: Int by delegate()
    val name: String by delegate()
}

ファクトリ関数の名前をクラス名と同じにする

大文字始まりの関数名はKotlinでは一般的ではないこと、また、クラス名と名前空間が異なるため、IDEリファクタリング機能でクラス名を変更しても関数名は変わらない点に注意が必要です。

class PropertyDelegate<T>(val t: Class<T>) {
    inline operator fun <reified E : Entity, reified P> getValue(receiver: E, property: KProperty<*>): P {
        println(E::class)
        println(P::class)
        println(t)
        return TODO("not implemented")
    }
}

inline fun <reified T> PropertyDelegate() = PropertyDelegate(T::class.java)

class Person : Entity() {
    val id: Int by PropertyDelegate()
    val name: String by PropertyDelegate()
}

ファクトリ関数をコンパニオンオブジェクトに定義する

ファクトリ関数をコンパニオンオブジェクトに定義し、Invoke演算子オーバーロードすることで呼び出す際の関数名を無くします。

class PropertyDelegate<T>(val t: Class<T>) {

    companion object {
        inline operator fun <reified T> invoke() = PropertyDelegate(T::class.java)
    }

    inline operator fun <reified E : Entity, reified P> getValue(receiver: E, property: KProperty<*>): P {
        println(E::class)
        println(P::class)
        println(t)
        return TODO("not implemented")
    }
}

class Person : Entity() {
    val id: Int by PropertyDelegate()
    val name: String by PropertyDelegate()
}

最後に

なんとかコンストラクタっぽくなりましたが、実体はコンパニオンオブジェクトに対するinvoke演算子オーバーロードであることに留意してください。また、ほとんどのケースはファクトリ関数で十分なはずです。トリッキーなコードは可読性、保守性が低下しますので、使用するにはそれなりの理由、根拠が必要ではないでしょうか。

また、今回は型情報を渡すのにjava.lang.Classを使用しましたが、Javaとの相互運用を考慮しないのであれば KClassがより適切ではないかと思います。

配属1ヶ月後の感想【2019年入社】

2019年度入社 S.K

案件配属後1ヶ月が経ちました。
グラフDBに関する案件に関わっています。
言語はKotlinとTypeScriptを使っています。どちらも初めての言語でしたが、研修で学んだJavaJavaScriptと関係があるので、基本文法などの習得はスムーズにできていますが、Vue.jsなどのフレームワークに苦戦しています。
実装は先輩社員に教わりながらしています。自分一人ではエラーの原因がわからなかったり、何をしていいかわからなくなることが多いですが、 1日でも早く戦力になれるように頑張りたいです。
またグラフDBについてもよくわかっていないので、勉強したいと思っています。

配属1ヶ月後の感想【2019年入社】

2019年度入社 Y.O

配属されて1ヶ月が経ち、ようやくプロジェクト全体の方針がぼんやりと理解できたような気がします。
配属された案件はサービスが3つあり、おおよそ同じ処理をするのですが、各々で別の処理やメソッドがあるため、なかなか理解することが難しく感じます。
そのため、1つのことをやった後に、次に別のサービスについて見ると異なっている部分がすぐには分からず、頭の切り替えがついていかないこともあります。
今のところ、プロジェクト全体の観点や特定のサービスの観点や、他の処理との兼ね合いから「こうあるべき」という判断はできません。
また、理解が足らない部分があるため、質問や現状報告の際にうまく表現できないことがあるのが問題です。
そこで、分からない部分があればその度に少しづつ先輩に質問して、それぞれのサービスの特徴を理解に努めたいと思います。
使用している言語やツールには少しずつ慣れてきたので、手すきの時間にそれらの勉強もしたいと考えています。
研修内容とはガラリと変わり、まだまだ戸惑う部分が多いですが、新しいことを知る絶好の機会だと捉えて、プロジェクトの理解を進めたいと思います。

GitLab 9.5.4 のバックアップとリストアの作業ログ

弊社ではソースコード管理に Gitlab CE を使っています。 今回、GitLabを別のサーバーに移し替えたのですが、GitLabのデータを一旦バックアップしてその後新サーバーでレストアするという手順を踏みました。それなりにハマりポイントがあったので作業ログとして残します。

なお、原則としてここの手順を元に作業しました。

docs.gitlab.com

続きを読む

yum で database disk image is malformed エラーが yum clean all で解決しない場合の対処

結論

時間が惜しい人はこれで。

yum clean all でだめなら yum clean dbcache

情報ソースが知りたい場合は続きをどうぞ。

続きを読む

アセンブリで簡単なプログラムを作る編

この投稿は インタープリズムはAdvent Calendarを愛しています。世界中のだれよりも。 Advent Calendar 2017の13日目 の記事です。

アセンブリで簡単なプログラムを作る

こんにちは、yumeです。最近アセンブリについて学習したので、簡単に何か、0から入力した正整数までの和を返すものを作ってみます。

はじめに

  • アセンブリとは、機械語(0と1)をもう少し人間にとって分かりやすい形にした言語です。
  • NASMというアセンブラを使っています。
  • 環境構築の部分は省略します。
  • 「詳しいことは(ひとまず)置いておいて、とにかく実装を見たい!」という人を意識して書こうと思ってます。

まずは10までの足し合わせ

section .text
global _start

_start:
mov     eax, 0      ;レジスタの初期化。eaxに0を入れる
mov     ecx, 10     ;ecxレジスタはカウンタとして使用される。今回は10までなので、ecxに10を入れる

_loop:
add     eax, ecx    ;eaxにecxを足す
loop    _loop       ;ecxがデクリメントされて_loopラベルへ

call    print       ;下で定義したprintサブルーチンを呼び出す

mov     eax, 0x0a   ;改行を出力
call    print

mov     eax, 1      ;
mov     ebx, 0      ;
int     0x80        ;システムコール。この一連で動作が終了する。慣れないうちは何がなんだか

print:
push    eax         ;eaxの中身をスタックへ逃す
mov     eax, 4      ;標準出力の準備
mov     ebx, 1      ;
mov     ecx, esp    ;出力するのはesp、つまりスタックの先頭
mov     edx, 1      ;1byte分の出力
int     0x80        ;システムコール
pop     eax         ;スタックから戻す
ret                 ;サブルーチンの呼び出し元へ

これを実行すると、

7

と出ます。「何事だ?」と驚かず、冷静にASCII文字コード表を見ると、文字'7'は10進数で55。一応、計算は出来ていそうです。

が、このままだとなんだか気持ち悪いので、10進数で返って来るようにします。

section .text
global _start

_start:
mov     eax, 0      ;レジスタの初期化。eaxに0を入れる
mov     ecx, 10     ;ecxレジスタはカウンタとして使用される。今回は10までなので、ecxに10を入れる

_loop:
add     eax, ecx    ;eaxにecxを足す
loop    _loop       ;ecxがデクリメントされて_loopラベルへ

mov     edx, 0      ;この後のdiv演算でedxには剰余が入るので、初期化
mov     ebx, 10     ;この後のdiv演算での引数になるレジスタ
div     ebx         ;これを書くだけで、eaxをebxで割り、結果はeaxに、余りはedxに格納される。面白いけど分かりにくい
add     eax, '0'    ;ASCII文字に変換するために'0'を足す
add     edx, '0'    ;

push    edx         ;一の位をスタックに避難
call    print       ;ここで出力されるのは十の位
pop     edx         ;

mov     eax, edx    ;
call    print       ;printサブルーチンを呼び出す

mov     eax, 0x0a   ;改行を出力
call    print

mov     eax, 1      ;
mov     ebx, 0      ;
int     0x80        ;システムコール。この一連で動作が終了する。慣れないうちは何がなんだか

print:
push    eax         ;eaxの中身をスタックへ逃す
mov     eax, 4      ;標準出力の準備
mov     ebx, 1      ;
mov     ecx, esp    ;出力するのはesp、つまりスタックの先頭
mov     edx, 1      ;1byte分の出力
int     0x80        ;システムコール
pop     eax         ;スタックから戻す
ret                 ;サブルーチンの呼び出し元へ

これで実行すると、

55

となります。

続きを読む

PAGE TOP