3.1K Views
February 28, 25
スライド概要
Scala/Clojureでの開発経験を経てKotlinに入門して感じた印象を簡単にまとめました。
「楽しく楽にcoolにsmartに」を理想とするprogrammer/philosopher/liberalist/realist。 好きな言語はClojure, Haskell, Elixir, English, français, русский。 読書、プログラミング、語学、法学、数学が大好き! イルカと海も大好き🐬
From Scala/Clojure to Kotlin #serverside_kotlin_meetup 1
🐬カマイルカ lagénorhynque 株式会社スマートラウンドのシニアエンジニア 主要技術スタック: Kotlin + Ktor Server-Side Kotlin Meetupの運営企業 関数型言語/関数型プログラミングが好き 仕事や趣味でClojure, Scala, Haskellなどに長く 触れてきた(JVM言語の割合高め) 現職で初めて仕事でKotlinを扱うようになってそろ そろ1年 2
[PR] カンファレンス「関数型まつり2025」6月開催 関連のプロポーザルも大歓迎📨 すでに複数あります😎 Kotlin 3
言語Scala, Clojure, Kotlin JVM 4
Scala OOP, FP static 2004 paradigm typing first appeared designer Martin Odersky 's note OOPL in FPL's skin 🐬 Clojure FP dynamic 2007 Kotlin OOP static 2011 Rich Hickey modern functional Lisp JetBrains better Java influenced by Scala 5
に およそ)似ているところ Scala ( 6
クラス・メソッド/関数・変数の定義 /* Scala */ scala> case class Person( | val name: String, | val birthDate: LocalDate, | ): | def age(now: LocalDate): Int = ChronoUnit.YEARS .between(birthDate, now).toInt // defined case class Person scala> val = Person("lagénorhynque", LocalDate.of(1990, 1, 18)) val : Person = Person(lagénorhynque,1990-01-18) scala> .name val res0: String = lagénorhynque scala> .age(LocalDate.now) val res1: Int = 35 🐬 🐬 🐬 🐬 7
/* Kotlin */
>>> data class Person(
...
val name: String,
...
val birthDate: LocalDate,
... ) {
...
fun age(now: LocalDate): Int = ChronoUnit.YEARS
.between(birthDate, now).toInt()
... }
>>> val ` ` = Person("lagénorhynque", LocalDate.of(1990, 1,
18))
>>> ` `.name
res4: kotlin.String = lagénorhynque
>>> ` `.age(LocalDate.now())
res5: kotlin.Int = 35
🐬
🐬
🐬
基本的な構文は酷似している
Scala 3ではオフサイドルールも導入された
Scalaでは0引数メソッド呼び出しで括弧が省略可能
8
関数リテラル(ラムダ式)と高階関数
/* Scala */
scala> (1 to 10). // REPL
|
filter(_ % 2 != 0).
|
map(x => x * x).
|
foldLeft(0)((acc, x) => acc + x)
val res0: Int = 165
でのメソッドチェーンのため . が末尾にある
/* Kotlin */
>>> (1..10).
...
filter { it % 2 != 0 }.
...
map { x -> x * x }.
...
fold(0) { acc, x -> acc + x }
res0: kotlin.Int = 165
丸括弧や矢印、destructuring (分配束縛)の差異に
よく戸惑う
9
メソッドの関数としての利用
/* Scala */
scala> def factorial(n: Long): Long = (1L to n).product
def factorial(n: Long): Long
scala> (0L to 9L).map(factorial)
val res0: IndexedSeq[Long] = Vector(1, 1, 2, 6, 24, 120, 720,
5040, 40320, 362880)
/* Kotlin */
>>> fun factorial(n: Long): Long = (1L..n).fold(1L) { acc, x
-> acc * x}
>>> (0L..9L).map(::factorial)
res1: kotlin.collections.List<kotlin.Long> = [1, 1, 2, 6, 24,
120, 720, 5040, 40320, 362880]
ではメソッド名そのままで関数オブジェクト
として参照できる(ref. eta-expansion)
Scala
10
文指向ではなく式指向
/* Scala */ // import scala.math.sqrt
scala> def isPrime(n: Int): Boolean =
|
if (n < 2) false else !(2 to sqrt(n).toInt)
.exists(n % _ == 0) // if
def isPrime(n: Int): Boolean
は式
/* Kotlin */ // import kotlin.math.sqrt
>>> fun isPrime(n: Int): Boolean =
...
if (n < 2) false else !(2..sqrt(n.toDouble())
.toInt()).any { n % it == 0 } // if
は式
コードブロックで明示的な return を要求される
ことによく戸惑う
Scalaでは常に最後の式が戻り値になり、
return はめったに使わない
11
Option type vs nullable type
/* Scala */ // import.scala.math.pow
scala> (Some(2): Option[Int]).map(pow(_, 10))
val res0: Option[Double] = Some(1024.0)
scala> (None: Option[Int]).map(pow(_, 10))
val res1: Option[Double] = None
/* Kotlin */ // import kotlin.math.pow
>>> (2 as Int?)?.let { it.toDouble().pow(10.0) }
res1: kotlin.Double = 1024.0
>>> (null as Int?)?.let { it.toDouble().pow(10.0) }
res2: kotlin.Double = null
の
型は Some(x), None の値をとり、
コレクションなどと同じように扱う
参照型で null が代入できてしまうという点で
null-safeではない(が、習慣として常に避ける)
Scala Option
12
が恋しくなるところ Scala 13
式 内包表記) for (a.k.a. for 型の場合 scala> for // Range (Seq) | x <- 1 to 3 | y <- 4 to 5 | yield x * y // => 1*4, 1*5, 2*4, 2*5, 3*4, 3*5 val res0: IndexedSeq[Int] = Vector(4, 5, 8, 10, 12, 15) scala> for // Option (Kotlin nullable type ) | x <- Some(2) | y <- Some(3) | yield x * y // => Some(2*3) val res1: Option[Int] = Some(6) scala> for | x <- Some(2) | y <- None: Option[Int] | yield x * y // => None val res2: Option[Int] = None 型 では 全体として で表すもの の場合 に モナド(的な構造)を簡潔に扱うためのシンタックス シュガーとして非常に便利 Haskellのdo記法に相当するもの 14
パターンマッチング(主にmatch式) 書籍 のサンプルコードより引用 scala> enum Tree[+A]: // FP in Scala | case Leaf(value: A) | case Branch(left: Tree[A], right: Tree[A]) | | def depth: Int = this match | case Leaf(_) => 0 | case Branch(l, r) => 1 + (l.depth.max(r.depth)) // defined class Tree scala> import Tree._ scala> Branch(Leaf("a"), Branch(Branch(Leaf("b"), Leaf("c")) , Leaf("d"))).depth val res0: Int = 3 構造に基づいて場合分けしながら分解できる 代数的データ型を扱う上で常にセットでほしい存在 コンパイラによる網羅性チェックも行われる 15
が恋しくなるところ Clojure 16
エディタと統合されたREPL の生産性の源泉 特に局所評価 できると非常に捗る Lisper/Clojurian (eval-last-sexp) 17
マクロ
Lisp
標準ライブラリの
user> (defmacro unless [test & body] ;
when-not
`(when (not ~test)
~@body))
#'user/unless
user> (unless (= 1 2) (println "Falsy!"))
Falsy!
nil
user> (unless (= 1 1) (println "Falsy!"))
nil
user> (macroexpand-1 '(unless (= 1 2) (println "Falsy!")))
(clojure.core/when (clojure.core/not (= 1 2)) (println
"Falsy!"))
シンタックスレベルでよくあるパターンを抽象化し
たい場合が稀にある
とはいえ(Lisperの良識として)マクロ定義は抑制
的であるべき
18
まとめ 言語の中で相対化することで理解が深まる での開発体験のさらなる改善にも期待🐤 JVM Kotlin 19
Further Reading Scala 公式サイト: https://www.scala-lang.org/ For Comprehensions | Tour of Scala | Scala Documentation cf. do Notation Equivalents in JVM languages: Scala, Kotlin, Clojure | Pattern Matching | Tour of Scala | Scala Documentation ドクセル 20
Clojure 公式サイト: https://clojure.org/ Clojure - Programming at the REPL: Introduction cf. Clojure RDD TDD で と のハイブリッドな開発ス タイルを実践しよう Clojure - Macros ref. defmacro - clojure.core | ClojureDocs Community-Powered Clojure Documentation and Examples 21