- 株式会社スマートラウンドのシニアエンジニア
- 主要技術スタック: Kotlin + Ktor
- Server-Side Kotlin Meetupの運営企業
- 関数型言語/関数型プログラミングが好き
- 現職で初めて仕事でKotlinを扱うようになってそろそろ1年
[PR] カンファレンス「関数型まつり2025」6月開催
- Kotlin関連のプロポーザルも大歓迎📨
- すでに複数あります😎
Scala | Clojure | Kotlin | |
---|---|---|---|
paradigm | OOP, FP | FP | OOP |
typing | static | dynamic | static |
first appeared | 2004 | 2007 | 2011 |
designer | Martin Odersky | Rich Hickey | JetBrains |
🐬's note | OOPL in FPL's skin | modern functional Lisp | better Java influenced by Scala |
/* 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
/* 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引数メソッド呼び出しで括弧が省略可能
/* 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 (分配束縛)の差異によく戸惑う
/* 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]
- Scalaではメソッド名そのままで関数オブジェクトとして参照できる(ref. eta-expansion)
/* 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
はめったに使わない
- Scalaでは常に最後の式が戻り値になり、
/* 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
- ScalaのOption型は
Some(x)
,None
の値をとり、コレクションなどと同じように扱う- 参照型で
null
が代入できてしまうという点でnull-safeではない(が、習慣として常に避ける)
- 参照型で
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記法に相当するもの
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
- 構造に基づいて場合分けしながら分解できる
- 代数的データ型を扱う上で常にセットでほしい存在
- コンパイラによる網羅性チェックも行われる
- Lisper/Clojurianの生産性の源泉
- 特に局所評価(eval-last-sexp)できると非常に捗る
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の良識として)マクロ定義は抑制的であるべき
-
JVM言語の中で相対化することで理解が深まる
-
Kotlinでの開発体験のさらなる改善にも期待🐤
-
公式サイト: https://www.scala-lang.org/