「Scala言語らしさ」を理解しよう! オブジェクト指向と関数型プログラミングの融合とは?

プログラミング言語Scalaの設計思想にあるという、オブジェクト指向と関数型プログラミングの融合(fusion)という理想と、それを掲げつつも現実主義的な点について、水島宏太(kmizu)さんが解説します。

「Scala言語らしさ」を理解しよう! オブジェクト指向と関数型プログラミングの融合とは?

kmizuと申します。株式会社ドワンゴでエンジニアを務めています。

最近では、毎年の新卒エンジニア向けScala研修の講師や、N予備校 プログラミングコースの一部教材のレビューといった教育、および研究等の面でも活動しています。

ドワンゴでは、私が入社した時点でScalaがかなり採用されており、社内にScalaをより深く広めることも職務の一環でした。私は2007年くらいの、Scalaがまだほとんど注目されていなかった頃からScalaを触り始めており、その縁で新卒エンジニア向けのScala研修資料作成にもメイン執筆者として携わることになりました。また、それ以外でも、その経歴を買われてさまざまな場所でScalaに関する発表を行っています。

今回の記事執筆依頼が来たときは、私よりもScalaエンジニアとしての技量が高い他の方のほうが向いているのではと思いましたが、ことScalaの設計思想に関しては誤解されがちなので、昔からScalaを追ってきた私がScalaの入門記事を書くのもいい機会になるように思えました。

本稿では、Scalaの誕生・進化の経緯を追うことで、Scalaの設計思想を俯瞰していきます。あわせて、私がScalaを書くときに気を付けていることや、Scalaらしい書き方をお伝えしていきたいと思います。

1The Scala Programming Language2

言語設計の思想に見られるScalaらしさ

実は「Scalaらしさ」を語るのは、けっこう難しいことです。Scalaの設計者が語る設計思想は書籍やインタビュー記事などを通じて知ることができるのですが、実際の使われ方がそこからしばしば逸脱することがあるからです。

特に最近は、純粋関数型プログラミングに近いことをScalaで行おうとする人たちがいて、オブジェクト指向派の人と対立したりすることもあります。とはいえScalaらしさを語るには、言語設計者の設計思想を知るのが一番です。

Scalaの作者=Martin Odersky先生について

Scalaの設計思想を知るには、作者であるMartin Odersky(マーティン・オダースキー)先生のバックグラウンドや、Scala以前に開発した言語についても掘り下げる必要があります。

Martin Odersky先生は、スイス連邦工科大学ローザンヌ校(EPFL)の教授を務めています(「先生」という敬称をつけているのはそのためです)。彼の専門の研究分野は、型理論と関数型プログラミングです。

型理論というのは、パッと見で何をする学問なのかよく分からないかもしれません。型理論は、プログラミング言語等の静的型を対象として、その基盤と応用について研究する学問です。「Java and scala's type systems are unsound: the existential crisis of null pointers」という、JavaとScalaの型システムが実は「安全でない」ことを示した論文が2016年に発表されましたが、例えばこうした研究も型理論の範疇に入ります。

ちょっと話が逸れましたが、彼は型理論についての研究者であり、また、オブジェクト指向と関数型の融合というテーマに取り組んできたために、Scalaにもそういった理論面での蓄積やオブジェクト指向を重視した跡が見られます。

誕生前夜~PizzaとGJ、Funnel

Odersky先生は、型理論や関数型プログラミングの研究をもとに、現在のScalaにつながる重要なプログラミング言語を開発しています。PizzaFunnelです。PizzaはJavaに高階関数や代数的データ型、ジェネリクスなどを取り入れた、当時としては非常に先進的な言語でした。

Set<String> s = new TreeSet(
    fun(String x, String y) -> boolean {
        return x.compareTo(y) < 0;
    }
);
for (int i = 1; i < args.length; i++)
    s.include(args[i]);
System.out.println(s.contains(args[0]));

リスト:Pizzaのサンプルコード

Pizzaのジェネリクスは、当時Sun Microsystemsに居たJavaチームのメンバーに刺激を与え、Odersky先生やSun MicrosystemsのJavaチームの人たちが中心となって、GJというJavaにジェネリクスを追加するプロジェクトが始まりました。GJプロジェクトは1998年には成果を出しており、成果物であるGJコンパイラがSun Microsystemsに買収されるという形で、後のJavaへのジェネリクス追加につながりました。

GJが成功を収めた一方で、Odersky先生は、Java言語の後方互換性を維持しつつジェネリクスを追加するという作業に限界を感じていたようです。そのためか、GJプロジェクトの後にOdersky先生は、Funnelという別のプログラミング言語を設計することになります。

Funnelは、Functional Netsという並行計算のための理論的基盤を持ち、かつ非常にシンプルな言語仕様で多様なものごとを表現できることを目指したプログラミング言語でした。 以下のFunnelコードは、1要素だけを持つバッファオブジェクトを定義したものです。

def newBuffer = {
  def get & full x = x & empty;
  def put x & empty = () & full x;
  (get, put) & empty
}; // オブジェクトの「定義」
val (get', put') = newBuffer; //オブジェクトの「生成」

リスト:Funnelのサンプルコード

Funnelはシンプルな言語仕様でさまざまなものごとを実現できる反面、やりたいことをFunnelのコードに落とし込むのに多くの手順が必要であり、また、実用性を重視していなかったために、あまり受けは良くなかったようです。

そこで、GJの成功とFunnelの失敗を教訓にして開発された言語が、Scalaなのです。ScalaはGJ(の実用性)とFunnel(のシンプルさ)の中間を目指して設計されました。

このあたりの経緯についてはOdersky先生へのインタビュー記事「The Origins of Scala」に詳しいです。

Scalaの設計思想

Scalaの設計思想を理解する上で絶対に外せないのが、オブジェクト指向プログラミングと関数型プログラミングの融合fusionという思想です。この思想については多くの人が誤って理解していますが、オブジェクト指向プログラミング「も」できるし、関数型プログラミング「も」できるというハイブリッドhybridアプローチではありません

オブジェクト指向プログラミングと関数型プログラミングの融合という思想を正確に理解するには、オブジェクト指向プログラミングと関数型プログラミングは対立するものではなく直交する(直接関係がないため、自由に組み合わせられる)ものであることを知る必要があります。

読者の方の中には、状態を持つオブジェクト指向プログラミングと、状態を持たない関数型プログラミングは相反するものではないのかという疑問を持たれる方がいるかもしれません。しかし、よく考えてほしいのですが、オブジェクト指向プログラミングでもValue Objectパターンなど不変性を重視したパターンがいくつもありますし、Javaなどの標準ライブラリにもそのような不変オブジェクトを採用したものがあります。

本来、関数型プログラミングと対立するのは、手続き型(状態あり)とオブジェクト指向プログラミングの組み合わせであって、オブジェクト指向プログラミングが関数型プログラミングと対立しているわけではないのです。

主流のオブジェクト指向言語が概ね手続き型とオブジェクト指向プログラミングの組み合わせを手法として採用しているのでピンと来ないかもしれませんが、アカデミックな世界では、昔からオブジェクト指向プログラミング言語を関数型の理論で捉えることが行われてきましたし、一切の可変状態を持たない関数型オブジェクト指向プログラミング言語というのもあります。オブジェクト指向と関数型プログラミングの融合という思想はそのようなアカデミックな土壌の中から出てきたのでしょう。

Scalaを形作るもうひとつの要素は、実用的であることです。Scalaは、スケーラブルかつ、現実主義な言語として設計されました。

スケーラブルというのは、小さなプログラムから大きなプログラムまで同じ概念で記述できることをいい、現実主義というのはとどのつまり、原理原則から外れていても実用上それが必要ならば認めようという態度です。

そのため、Scalaには本来の設計思想に必ずしも沿わなくても、それが実用的であれば受け入れられるという懐の広さがあります。関数型プログラミングを非常に重視しているのに可変コレクションや変更可能な変数が言語仕様や標準ライブラリにあるのも、そのような現実主義のあらわれでしょう。

Scalaの歴史 - バージョン2.7から現在まで

Scalaは、オブジェクト指向プログラミングと関数型プログラミングの融合という理想を掲げつつも、現実主義な言語であることに先ほど触れました。ここからはScalaの歴史をたどりつつ、バージョンアップに伴って追加された新機能や変更点を整理してみます。

歴史を追っていく過程で、実用性のために「あまり綺麗でない」機能や、Javaとの互換性を考慮した機能を取り込んだりすることがしばしばあることが理解でき、現実に対して適度に妥協しつつ進化していく言語であることがよく分かるのではないでしょうか。

ただし、Scala 2.7(2008年2月リリース)より前は、資料が比較的少なく、言語仕様についてきちんと調査することが難しいことや、Scalaがおそらく初めて実用に使われ始めたのが2.7であることなどを考慮して、バージョン2.7以降の歴史について主な新機能や変更点を追っていくことにします。

Scala 2.7 - 本格的に注目され始めたバージョン

Scala 2.7は、海外および日本で本格的に注目され始めた時期のバージョンということで重要な意味を持っています。Scala 2.7では、主にJavaのジェネリクスとの相互運用性を改善する変更が入りました。

Scala 2.7より前のバージョンでは、Javaのジェネリックなクラスは型パラメータが一切ない状態で見えていたため、例えばjava.util.ArrayListを扱うコードは以下のようになっていました。

val alist = new java.util.ArrayList
alist.add("Hoge")
val hoge = alist.get(0).asInstanceOf[String]

せっかくJava 5以降、ジェネリクスによっていろいろな領域に型安全性がもたらされたのですが、ScalaからJavaのライブラリを利用する限り、バージョン2.7までその安全性を享受することはできませんでした。

Scala 2.7では、クラスファイル中のJavaジェネリクスに関する情報を読み取れるようになったため、それ以降は(現在でも)上記のコードを以下のように書くことができます。

val alist = new java.util.ArrayList[String]
alist.add("Hoge")
val hoge = alist.get(0)

Scalaは現在ですら、しばしばJavaのライブラリ資産に依存せざるを得ませんから、このようにJavaの資産との相互運用性を高める改善は、実用性のためには重要です。

また、Javaコードの中からScalaコードを呼び出すケースを適切に扱えるようにするために、Java側からもScalaジェネリクスの情報を読み取れるように、クラスファイルの情報の格納方法が変わりました。Scalaのジェネリックなコードをクラスファイルにコンパイルする際、Javaジェネリクスが使っている領域に、ジェネリクスに関する情報を格納するようにしたのです。

ただし、Scalaのジェネリクスの方がJavaのジェネリクスより高機能であり、Javaジェネリクスでは表現し切れない部分を含んでいるため、Scalaジェネリクスに関する情報も別途含むようになっています。

以下は、JavaからScalaのジェネリックなコレクションであるBufferを利用したコード例です{$annotation_1}

import scala.collection.mutable.ArrayBuffer;
...
ArrayBuffer<String> buffer = new ArrayBuffer<String>();
buffer.append("A");
buffer.append("B");
buffer.append("C");

Scala 2.7では、JavaとScalaが混在したプロジェクトでも適切にコンパイルが行えるように、処理系が改良されました。現在でも、アノテーションなどの一部機能はScalaでは書けず、Javaのコードを書く必要があるため、このような混在プロジェクトを扱えるように進化したのは、実用上とても重要です。

Scala 2.8 - コレクションライブラリを全面的に再設計

Scala 2.8では、新機能がいくつか追加されるとともに、コレクションライブラリの全面的な再設計が行われました。私見では、言語機能の追加よりコレクションライブラリの再設計の影響が大きかったのではないかと思います。

例えば、Scala 2.7までは、不変コレクションと可変コレクションはそれぞれ個別のクラスとして定義されていましたが、Scala 2.8ではscala.collection.immutable(不変コレクション)scala.collection.mutable(可変コレクション)という形で明確にパッケージが切り分けられ、あるコレクションが不変か可変かがすぐに分かるようになりました。

また、継承階層も整理され、不変コレクションと可変コレクションそれぞれにルートクラスが用意され、さらにコレクション全体を統合するルートクラスが用意されるようになりました。これによって、コレクションライブラリを使ったプログラミングが非常に容易になりました。

配列の扱いも若干変わっています。2.8以前は配列はあくまで普通の可変コレクションという形で取り扱われていたのですが、それではJavaとの相互運用性に問題があるということで、Scalaの配列とJavaの配列が同じ表現になりました。

val x: Array[Int] = Array(1, 2, 3) // int[] x = new int[]{1, 2, 3} とほぼ同じ

Scalaの世界で完結するならそれまでのアプローチの方が美しかったですが、Scalaが実用性を重視していることが、このような変更からも見て取れます。

これまで要望されていた、名前付き引数とデフォルト引数の機能も追加されました。

名前付き引数は最近の言語ではおなじみになりましたが、メソッドを呼び出すときに仮引数名でどの実引数を与えるかを指定することができる機能です。例えば、以下のようなコードが記述できます。

val person = Person(name = "Kota Mizushima", age = 34)

名前付き引数をうまく使えば、コードの可読性を上げることができます。

デフォルト引数も最近の言語ではおなじみですが、引数のデフォルト値をメソッドの定義時に指定しておくことで、その引数が省略されたときにデフォルト値を補ってくれる機能です。

object Strings {
  def join(values: List[String], separator: String = ","): String = values.mkString(separator)
}

上記のメソッドjoinvaluesの各要素をseparatorで挟んだ文字列を返すメソッドですが、separator: String = ","という形でseparatorのデフォルト値として","が指定されています。このようにすることで、以下のコードは

Strings.join(List("A", "B", "C"))

次のように解釈されます。

Strings.join(List("A", "B", "C"), ",")

デフォルト引数を活用することでメソッドをオーバーロードせずに、しばしば一つの定義に収めることができるようになります。また、特定の引数について、大半のユースケースでは引数の値が決まっている場合にも有効です。

Scala 2.7までは、トップレベルには次のいずれかしか書くことができませんでした。

  • パッケージ宣言
  • インポート宣言
  • クラス宣言
  • トレイト宣言
  • オブジェクト宣言

そのため、ユーティリティメソッドはオブジェクト宣言の中に定義する必要がありました。

Scala 2.8では、パッケージオブジェクトという機能を導入することで、パッケージに直接ユーティリティ関数などを定義できるようになりました。パッケージオブジェクトを使ったコードは、例えば以下のようになります。

package com.github.kmizu.myproject
package object commons {
  def foo(): String = "Foo"
  def bar(): String = "Bar"
}

このコードを利用する側は以下のようになります。

import com.github.kmizu.myproject.commons._
foo() // "Foo"
bar() // "Bar"

実態としては、パッケージに見えるオブジェクトを定義して、その下にユーティリティメソッドを定義しているのですが、パッケージ名とオブジェクト名をあわせることができるため、直接パッケージからユーティリティメソッドをインポートできるようになり、利便性が向上しました。

その他にもいくつか変更点があるのですが、それほど重要ではないので、Scala 2.8についてはここまでにしておきます。Scala 2.8は、新機能の導入によって実用性を高めるとともに、コレクションライブラリの再設計などによって標準ライブラリを綺麗にしようと試みるバージョンであったと言えます。

Scala 2.9 - やや早過ぎた並列コレクション

エンジニアHubに会員登録すると
続きをお読みいただけます(無料)。
登録のメリット
  • すべての過去記事を読める
  • 過去のウェビナー動画を
    視聴できる
  • 企業やエージェントから
    スカウトが届く