Skip to content

Instantly share code, notes, and snippets.

@lagenorhynque
Last active February 28, 2025 07:04
Show Gist options
  • Save lagenorhynque/51739763bdd9b70703b6c4436710a578 to your computer and use it in GitHub Desktop.
Save lagenorhynque/51739763bdd9b70703b6c4436710a578 to your computer and use it in GitHub Desktop.
From Scala/Clojure to Kotlin

From Scala/Clojure

to Kotlin

#serverside_kotlin_meetup


x icon


[PR] カンファレンス「関数型まつり2025」6月開催

関数型まつり2025


JVM言語Scala, Clojure, 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 */
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では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 はめったに使わない

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
  • ScalaのOption型は Some(x), None の値をとり、コレクションなどと同じように扱う
    • 参照型で null が代入できてしまうという点でnull-safeではない(が、習慣として常に避ける)

Scalaが恋しくなるところ


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
  • モナド(的な構造)を簡潔に扱うためのシンタックスシュガーとして非常に便利

パターンマッチング(主に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
  • 構造に基づいて場合分けしながら分解できる
  • 代数的データ型を扱う上で常にセットでほしい存在
    • コンパイラによる網羅性チェックも行われる

Clojureが恋しくなるところ


エディタと統合されたREPL

REPL integrated in the editor

  • Lisper/Clojurianの生産性の源泉
  • 特に局所評価(eval-last-sexp)できると非常に捗る

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の良識として)マクロ定義は抑制的であるべき

まとめ

  • JVM言語の中で相対化することで理解が深まる

  • Kotlinでの開発体験のさらなる改善にも期待🐤


Further Reading

Scala


Clojure

#!/usr/bin/env bash
# npm install -g reveal-md
reveal-md from-scala-clojure-to-kotlin.md --theme night --highlight-theme monokai-sublime -w $@
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment