Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save lagenorhynque/6b08101b2568e4234709a4a6260f5566 to your computer and use it in GitHub Desktop.
Save lagenorhynque/6b08101b2568e4234709a4a6260f5566 to your computer and use it in GitHub Desktop.
ミュータビリティとイミュータビリティの狭間: 関数型言語使いから見たKotlinコレクション

ミュータビリティと

イミュータビリティの狭間

関数型言語使いから見たKotlinコレクション

#kotlinfest2024_after_33


twitter icon


  1. きっかけ

  2. 実験・調査

  3. まとめ


1. きっかけ


Kotlin Fest 2024のセッションを聴いていた私🐬

  • 「Kotlinにはイミュータブルとミュータブルの2種類のコレクションがあります」

  • 「Kotlinを導入することで私たちはイミュータビリティを得ました」

    • Kotlinの List のコードとともに破壊的更新操作ができない例が示される

🐬の反応

  • Kotlinの List, Set, Map などは「イミュータブル」(不変)なコレクションといえるのだろうか🤔

    • 同じくJVM言語(さらに関数型言語)のScalaやClojureとはどう違うのだろう
  • 確かKotlinには準標準ライブラリ(?)としてkotlinx.collections.immutableがあったはず🤔

    • immutable/persistent collectionsを別途用意しているということは標準のコレクションはそうではないと暗示しているような?

2. 実験・調査


Lisper/Clojurianの本能に従ってREPLで試してみる

ℹ️ 実行環境

# Kotlin
$ kotlinc -version
info: kotlinc-jvm 2.0.0 (JRE 22.0.1)
# Scala
$ scala -version
Scala code runner version 3.4.2 -- Copyright 2002-2024, LAMP/
EPFL
# Clojure
$ clj -version
Clojure CLI version 1.11.3.1463

Kotlinの List

MutableList インスタンスを List 変数に代入する:

>>> val xs: MutableList<Int> = mutableListOf(1, 2, 3)
// イミュータブル(?)とされるList型の変数に代入できてしまった
>>> val ys: List<Int> = xs
>>> ys.add(4)
error: unresolved reference: add
ys.add(4)
   ^
>>> xs.add(4)
res3: kotlin.Boolean = true
>>> xs
res4: kotlin.collections.MutableList<kotlin.Int> = [1, 2, 3,
4]
// MutableList型の変数xsからの破壊的更新がysに必然的に波及している
>>> ys
res5: kotlin.collections.List<kotlin.Int> = [1, 2, 3, 4]

>>> xs::class
res6: kotlin.reflect.KClass<out kotlin.collections.MutableLi
st<kotlin.Int>> = class java.util.ArrayList
>>> ys::class
res7: kotlin.reflect.KClass<out kotlin.collections.List<kotl
in.Int>> = class java.util.ArrayList

List インスタンスをJavaのメソッドで操作する:

>>> val zs: List<Int> = listOf(1, 3, 2)
// Javaの破壊的なメソッドがコンパイル/実行時エラーなく呼び出せてしまった
>>> java.util.Collections.sort(zs, { a, b -> b.compareTo(a) }
)
// イミュータブル(?)とされるListのインスタンスが更新できてしまった
>>> zs
res10: kotlin.collections.List<kotlin.Int> = [3, 2, 1]

>>> listOf<Int>()::class
res11: kotlin.reflect.KClass<out kotlin.collections.List<kotl
in.Int>> = class kotlin.collections.EmptyList
>>> listOf(1)::class
res12: kotlin.reflect.KClass<out kotlin.collections.List<kotl
in.Int>> = class java.util.Collections$SingletonList
>>> listOf(1, 2)::class
res13: kotlin.reflect.KClass<out kotlin.collections.List<kotl
in.Int>> = class java.util.Arrays$ArrayList
>>> listOf(1, 2, 3)::class
res14: kotlin.reflect.KClass<out kotlin.collections.List<kotl
in.Int>> = class java.util.Arrays$ArrayList
  • listOf に2要素以上を与えて得られる実体は java.util.Arrays.asList の戻り値と同じ(ミュータブルな固定長リスト)

参考: Scalaの List

ListBuffer インスタンスを List 変数に代入する:

scala> val xs: List[Int] = List(1, 2, 3)
val xs: List[Int] = List(1, 2, 3)
// Listは独自のイミュータブルコレクション
scala> xs.getClass
val res0: Class[? <: List[Int]] = class scala.collection.immu
table.$colon$colon
// ミュータブルなListBufferとイミュータブルなListにまったく互換性なし
scala> val ys: scala.collection.mutable.ListBuffer[Int] = xs
-- [E007] Type Mismatch Error: ------------------------------
// (エラー詳細は省略)

List インスタンスをJavaのメソッドで操作する:

scala> val zs: List[Int] = List(1, 3, 2)
val zs: List[Int] = List(1, 3, 2)
// ScalaのListはJavaのCollection/Listと互換性がない
scala> java.util.Collections.sort(zs, Ordering[Int].reverse)
-- [E007] Type Mismatch Error: ------------------------------
// (エラー詳細は省略)
// 内部的な実体を維持しながら(コピーせず) Javaコレクションに変換できる
scala> import scala.jdk.CollectionConverters.*
scala> val zs_ = zs.asJava
val zs_: java.util.List[Int] = [1, 3, 2]
// 破壊的更新操作はサポートしていない(実行時エラーになる)
scala> java.util.Collections.sort(zs_, Ordering[Int].reverse)
java.lang.UnsupportedOperationException

参考: Clojureのvector (IPersistentVector)

user=> (def xs [1 2 3])
#'user/xs
;; vectorは独自のイミュータブルコレクション
user=> (class xs)
clojure.lang.PersistentVector
;; IPersistentVectorインターフェースを実装している
user=> (instance? clojure.lang.IPersistentVector xs)
true
;; Java interopを円滑にするため、java.util.Listも実装している
user=> (instance? java.util.List xs)
true

vectorインスタンスをJavaのメソッドで操作する:

user=> (def zs [1 3 2])
#'user/zs
;; 破壊的更新操作はサポートしていない(実行時エラーになる)
user=> (java.util.Collections/sort zs >)
Execution error (UnsupportedOperationException) at java.util.
List/sort (List.java:514).
  • Javaのインターフェースを実装しているが、破壊的更新はできない(できたらイミュータブルコレクションと呼べない)

3. まとめ


  • イミュータブル(不変)っぽく見えてイミュータブルとは言いがたいデータ構造がある
    • Kotlinの List はその例だといえる

    • immutableではなく read-only (読み取り専用) なインターフェースと呼ぶのが正確と思われる

    • effectively immutable (実質的にイミュータブル)だと形容されることもあるが、抜け道が残れば当然ながらイミュータビリティ(不変性)は保証されないことになる


  • 🐬の感想:
    • Kotlin言語/標準ライブラリの設計判断には実用上の合理性を感じる

      • ある意味でScala/Clojure以上にJava interopを重視するからこそ必然性があるのかも(?)
    • しかし、純粋な関数とデータで副作用を最小化/局所化するプログラミングスタイル(関数型プログラミング)を好む私としては、イミュータブルコレクションがデフォルトであってほしかった

      • (Kotlinでの総合的な開発体験は好印象😊)

Further Reading

KotlinのListについて


Kotlin, Scala, Clojureのコレクションについて


関数型プログラミングについて

#!/usr/bin/env bash
# npm install -g reveal-md
reveal-md boundary-between-mutability-and-immutability.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