私たちのチームは、記事の著者と同様に、Scalaから主要な言語としてKotlinにほぼ1年が経過しました。 私の意見は主に著者と一致するので、彼の興味深い記事の翻訳を提供します。
ブログを更新しなかったので、それはまともな時間でした。 今から一年、私はメインの言語であるScalaからKotlinに切り替えました。 この言語は、Scalaにある多くの落とし穴とあいまいさを回避できる一方で、Scalaで気に入っている多くの良いものを借りてきました。
以下に、ScalaとKotlinで気に入っている例と、両言語での実装方法の比較を示します。
宣言と型推論
両方の言語で特に気に入っているのは、どちらも静的に型推論で型付けされていることです。 これにより、コード内で面倒な宣言をすることなく、静的型付けの力を最大限に活用できます( 元:宣言型ボイラープレート )。 ほとんどの場合、これは両方の言語で機能します。 両方の言語で、名前の後に変数の型のオプションの宣言とともに、不変の型の設定も追跡されます。
サンプルコードは両方の言語で同じです。
ageおよびtype Intという名前の不変変数の宣言:
val age = 1
String型の可変変数の宣言:
var greeting = "Hello"
どちらの言語も、ラムダ関数を最初のクラスのオブジェクトとしてサポートしています。これは、変数に割り当てるか、関数にパラメーターとして渡すことができます。
スカラ
val double = (i: Int) => { i * 2 }
コトリン
val double = {i: Int -> i * 2 }
データ / ケースクラス
ScalaとKotlinには、 データモデルオブジェクトの表現であるデータクラスに対して同様の概念があります 。
Scalaアプローチ
Scalaでは、これらは次のようなケースクラスです。
case class Person(name: String, age: Int)
- 適用方法があります (インスタンスの作成時に新しいキーワードを使用する必要はありません)
- アクセスメソッドは各プロパティに対して宣言されます ( プロパティが varとして宣言されている場合、 setterメソッドも存在します)
- toString 、 equalおよびhashCodeが合理的に宣言されています
- コピー機能があります
- 適用解除メソッドがあります( パターンマッチングでこれらのクラスを使用できます)。
コトリンアプローチ
Kotlinはこれらのクラスをデータクラスと呼びます
data class Person (val name: String, val age: Int)
主な機能:
- アクセスメソッドは各プロパティに対して宣言されます ( プロパティが varとして宣言されている場合、 setterメソッドも存在します)。 これはデータクラスの例外的な機能ではなく、このステートメントはKotlinのすべてのクラスに当てはまります。
- 合理的に宣言されたtoString 、 equal 、およびhashCode
- コピー機能
- component1..componentN関数。 類推により、 unapplyとして使用されます 。
- Hibernate、JacksonなどのJavaフレームワークに必要なJavaBean getterおよびsetterを変更なしで実装します。
Kotlinでは、クラスを初期化するために新しいキーワードが必要ないのと同じように、特別な適用メソッドは必要ありません。 したがって、これは他のクラスと同様に標準のコンストラクタ宣言です。
比較
基本的に、 ケースクラスとデータクラスは似ています。
以下の例は、両方の言語で同じように見えます。
val jack = Person("jack", 1) val olderJack = jack.copy(age = 2)
一般に、 データとケースクラスは日常使用で交換可能であることがわかりました。 Kotlinにはデータクラスの継承にいくつかの制限がありますが、落とし穴を避けるためのequalsおよびcomponentN関数の実装を考えれば、これは意図的に行われました。
Scalaの場合、 Kotlinが 'when'ブロックのデータクラスを操作する方法と比較して、クラスはパターンマットにおいてより強力です。
Kotlinのアプローチは、既存のJavaフレームワークに対して次のように機能します。 通常のJava Beanのように見えます。
どちらの言語でも、名前でパラメーターを渡し、それらのデフォルト値を指定できます。
安全にヌル/オプション
Scalaアプローチ
Scalaでは、 nullはモナドオプションを安全に使用することです。 簡単に言えば、 オプションは次の2つの特定の状態のいずれかになります 。Some (x)またはNone
val anOptionInt: Option[Int] = Some(1)
または
val anOptionInt: Option[Int] = None
isDefinedおよびgetOrElse関数(デフォルト値を示す)を使用してオプションで操作することは可能ですが、より頻繁に使用される状況は、モナドがmap 、 foreach、またはfold演算子で使用される場合です。
たとえば、次のように2つのオプション変数の合計を計算できます。
val n1Option: Option[Int] = Some(1) val n2Option: Option[Int] = Some(2) val sum = for (n1 <- n1Option; n2 <- n2Option) yield {n1 + n2 }
変数sumの値はSome(3)になります。 forキーワードは、 yieldキーワードの使用に応じてforeachまたはflatMapとして使用する方法の良い例です。
別の例:
case class Person(name: String, age: Option[Int]) val person: Option[Person] = Some(Person("Jack", Some(1))) for (p <- person; age <- p.age) { println(s"The person is age $age") }
「The person is age 1」という行が印刷されます。
コトリンアプローチ
Kotlinは、日常使用で非常に実用的なgroovy構文を借用しています。 Kotlinでは、すべての型はnull不可であり、「?」で明示的にnull可能と宣言する必要があります nullを含めることができる場合
同じ例を次のように書き換えることができます。
val n1: Int? = 1 val n2: Int? = 2 val sum = if (n1 != null && n2 != null) n1 + n2 else null
これは、Kotlinがコンパイル時チェックを強制してnullをチェックせずにnull許容変数が使用されないようにすることを除き、Java構文にはるかに近いため、NullPointerExceptionを恐れる必要はありません。 また、 nullを non-nullableとして宣言された変数に割り当てることはできません。 さらに、コンパイラーは、 nullの変数を再チェックする必要性を排除するのに十分スマートであり、Javaのような変数の複数のチェックを回避します。
2番目の例と同等のKotlinコードは次のようになります。
data class Person(val name: String, val age: Int?) val person: Person? = Person("Jack", 1) if (person?.age != null) { printn("The person is age ${person?.age}") }
または、「let」を使用して、「if」ブロックを次のように置き換えます。
person?.age?.let { person("The person is age $it") }
比較
Kotlinでのアプローチが好きです。 読みやすく理解しやすく、複数のネストされたレベルで何が起こるかを簡単に把握できます。 Scalaのアプローチはモナドの振る舞いに基づいており、一部の人々はもちろんそれを好んでいますが、私自身の経験から言えば、コードは少額の投資では過負荷になりつつあると言えます。 mapまたはflatMapを使用する際のこの複雑さには非常に多くの落とし穴があり、モナドマッシュで何か間違ったことをしている場合や、代替を探しずにパターンマッチを使用している場合、コンパイル時に警告も表示されません。明らかではない例外 。
Kotlinのアプローチは、Javaコードからの型がデフォルトでnull可能という事実により、Javaコードとの統合のギャップも減らします( ここで著者は完全に正しいわけではありません。Javaの型はnullableとnull Scalaは、 nullを安全に保護しない概念としてnullをサポートする必要があります 。
機能コレクション
もちろん、Scalaは機能的なアプローチをサポートしています。 Kotlinは少し少ないですが、主要なアイデアはサポートされています。
以下の例では、 foldおよびmap関数の操作に特別な違いはありません。
スカラ
val numbers = 1 to 10 val doubles = numbers.map { _ * 2 } val sumOfSquares = doubles.fold(0) { _ + _ }
コトリン
val numbers = 1..10 val doubles = numbers.map { it * 2 } val sumOfSquares = doubles.fold(0) {x,y -> x+y }
どちらの言語も、レイジーコンピューティングのチェーンという概念をサポートしています。 たとえば、10個の偶数の出力は次のようになります。
スカラ
val numbers = Stream.from(1) val squares = numbers.map { x => x * x } val evenSquares = squares.filter { _%2 == 0 } println(evenSquares.take(10).toList)
コトリン
val numbers = sequence(1) { it + 1 } val squares = numbers.map { it * it } val evenSquares = squares.filter { it%2 == 0 } println(evenSquares.take(10).toList())
暗黙的な変換と拡張メソッド
これは、ScalaとKotlinがわずかに分岐する領域です。
Scalaアプローチ
Scalaには暗黙的な変換の概念があり、必要に応じて別のクラスに自動的に変換することにより、クラスに高度な機能を追加できます。 広告の例:
object Helpers { implicit class IntWithTimes(x: Int) { def times[A](f: => A): Unit = { for(i <- 1 to x) { f } } } }
次に、コードで次のように使用できます。
import Helpers._ 5.times(println("Hello"))
これにより、「Hello」が5回出力されます。 これは、 "times"関数(実際にはIntには存在しない)が呼び出されると、変数がIntWithTimesオブジェクトに自動的にパックされ、関数が呼び出されるという事実により機能します。
コトリンアプローチ
Kotlinは同様の機能に拡張機能を使用します。 Kotlinでは、このような機能を実装するために、拡張が行われる型の形式のプレフィックスのみで、通常の関数を宣言する必要があります。
fun Int.times(f: ()-> Unit) { for (i in 1..this) { f() } }
5.times { println("Hello")}
比較
Kotlinのアプローチは、私がScalaでこの機能を主に使用する方法と一致しますが、わずかに単純化された理解可能なレコードという形でわずかに利点があります。
KotlinにはないScalaの機能で、見逃せないもの
私にとってKotlinの最高の機能の1つは、その機能にさえありませんが、ScalaのKotlinにはない機能にあります。
- 名前による呼び出し-これは可読性を破壊します。 関数が渡された場合、単にコードを見るだけで関数ポインタが渡されたことがわかりやすくなります。 ラムダの明示的な転送と比較して、これがもたらす利点は見当たりません。
- 暗黙の変換-これは私が本当に嫌いなものです。 これにより、インポートされたものに応じてコードの動作が大幅に変化する状況が発生します。 その結果、IDEが適切にサポートされていないと、どの変数が関数に渡されるかを言うのは非常に困難です。
- オーバーロード-上記の複数のモナドの問題。
- 中置文と後置文のオプションの構文の混乱-Kotlinはもう少し形式的です。 その結果、その中のコードは曖昧さが少なくなり、読みやすくなり、単純なタイプミスが明らかなエラーになることはそれほど容易ではありません。
- 演算子を最大に再定義することにより、Kotlinは主要な演算子(+、-など)のみを上書きできます。 Scalaでは、任意の文字シーケンスを使用できます。 「〜%#>」と「〜+#>」の違いを本当に知る必要がありますか?
- コンパイル時間が遅い。
ご清聴ありがとうございました。
オリジナルScala対Kotlin
PS翻訳のいくつかの場所では、特に翻訳なしの単語を残しました(null、null safe、infix、postfixなど)。