Scala しい新世界

作成日 : 2023-04-05
最終更新日 :

Scala とは

Scala とは、オブジェクト指向言語と関数型言語の両方の顔を持つコンピュータ言語である。 表題は、オルダス・ハクスリーの「すばらしい新世界」のただのダジャレである。

なお、Scala の本も参照してほしい。

Scala の情報源

トピックス

リテラル

文字列補間

文字列リテラルの前に s をつけると式を埋め込むことができる。

scala> s"="72/1.8/1.8 = ${72/1.8/1.8}"
val res0: String = 72/1.8/1.8 = 22.22222222222222

このように式を埋め込む機能を文字列補間という。同様の機能を Ruby では式展開という。

インストール

最初のインストール

WSL の Ubuntu 20.04 で動かしている。Java は open jdk 14.0.1 である。 Scala は 2.13.3 である。Scala は $ apt install scala でインストールせずに、 次でインストールした。(2020-09-23)

	$ wget https://downloads.lightbend.com/scala/2.13.3/scala-2.13.3.deb
	$ sudo apt install ./scala-2.13.3.deb

参考:https://www.ninton.co.jp/archives/4440
Java と Scala のバージョンの組み合わせによっては、上記 URL の StackOverflow で紹介されているエラーが起こった。

次のインストール

その後 Windows で Java 19.0.2 をインストールしたので、こちらを使うような Scala をインストールすることにした。 https://scala-lang.org/download を見ると cs setup が推薦されているので、こちらを使うことにした。cs-x86_64-pc-win32.exe を実行すると、 システム環境変数 JAVA_HOME を見て Java がインストールされているフォルダを探し、これを使うようにインストールする。 インストール後、scala が PowerShell から起動できることを確かめた。

PowerShell 7.3.3
PS C:> scala
Welcome to Scala 3.2.2 (19.0.2, Java Java HotSpot(TM) 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.

scala> println("Saluton, mondo!")
Saluton, mondo!

scala> :q
PS C:>

これでひとまず安心した(2023-03-30)。

平方数の和問題の解法

関数型言語のプログラミングは不慣れである。 平方数の和の問題を Scala で解くにはどうしたらいいか、 その考えをメモする。

まず、基本となる数の列をどのように作るかが問題である。C 言語では配列しかないが、 最近の高級言語では複数の選択肢がある。Scala もそうで、配列もあるが、 関数型言語の常でリストが基本となる。

最初に「リストが基本となる」といったのは少々早とちりで、まあそうには違いないのだが、 Scala の場合に要素の集合をどのように表し、どのように分類するかを見るべきだろう。 要素の集合を Scala ではコレクションという。Scala のコレクションには大きく分けて、 Seq 、Set 、Map がある(みな3文字だ)。リストは Seq のうちのさらに LinearSeq という小分類に属する。

さて、平方数の和の問題だから、まずは平方数を一覧で表示したい。 わざわざリテラルで List(0, 1, 4, 9, ..., ) などと手で打ち込むのはばからしい。 ただ、不慣れな言語だとどのようにして最初の平方数を作ればいいかわからなかったのだ。 Seq のオブジェクトを初期化するためのメソッドがそろっているが、 その中に tabulate という添字から要素を計算するメソッドがあり、任意の数以下に対応できるようにプログラムを組むことは今後の課題である。 これを使えばいいだろう。なお、当面は平方の和の上限を 50 と小さい状態で試してみる。

scala> List.tabulate(7)(i => (i+1)*(i+1))
res0: List[Int] = List(1, 4, 9, 16, 25, 36, 49)

次に、i2 + j2 のような和の形を作らないといけない。i と j が同じであれば、 上記 res0 の要素をそのまま縫合(zip)して足せばよい。

scala> res0.zip(res0).map(x => x._1 + x._2)
val res1: List[Int] = List(2, 8, 18, 32, 50, 72, 98)

うまくいっているようだ。

i と j の差が 1 であれば、一方を tail で取り出して、他方はそのままで、 あとは上記と同じように res1 の要素をそのまま綴じ合わせて足せばよい。

scala> res1.tail.zip(res1).map(x => x._1 + x._2)
val res2: List[Int] = List(10, 26, 50, 82, 122, 170)

最初はご丁寧に、res1.tail.zip(res1.init).map(x => x._1 + x._2)としていたが、 zipped map の結果は短いほうに揃えられることがわかったので、res1 の init は不要である。

なお、ここで 50 を超える数は不要なので、filter を使って 50 以下の要素のみ抽出しよう。

scala> res2.filter(x => x <=50)
val res3: List[Int] = List(10, 26, 50)

では、i と j の差が 2 以上のときの和はどう求めるか。差を k とすると drop(k) が使える。 差が 5 の場合を実験してみる。

scala> res0.drop(5).zip(res0).map(x => x._1 + x._2).filter(x => x <= 50)
val res4: List[Int] = List(37)

ここでやりたいことをまとめる。i と j の差を k とする。k = 0 のときの平方和のリスト、 k = 1 のときの平方和のリスト、…… と順次作っていって、これらのリストのリストを flatten してソーティングすれば答に近づくと思う。まず、リストのリストを手で作ってみる。 scala は認識するだろうか?

scala> List(List(2, 8, 18, 32, 50), List(5, 13, 25, 41), List(10, 20, 32), List(17, 29, 45), List(26, 40), List(37), List(50) )
val res5: List[List[Int]] = List(List(2, 8, 18, 32, 50), List(5, 13, 25, 41), List(10, 20, 32), List(17, 29, 45), List(26, 40), List(37), List(50) )

scala は認識した。では flatten メソッドもうまくいくだろうか。

scala> res5.flatten
val res6: List[Int] = List(2, 8, 18, 32, 5, 13, 25, 10, 20, 34, 17, 29, 26, 40, 37)

flatten もうまくいったようだ。あとはソートすればよい。

scala> res6.sorted
val res7: List[Int] = List(2, 5, 8, 10, 13, 17, 18, 20, 25, 26, 29, 32, 32, 37, 40, 41, 45, 50, 50)

やりたいことがおぼろげながらわかったような気がする。

次は、リストのリストを手で作るのではなく、プログラムで作ろう。 まず、平方和のリストに対して、添字をペアにして新たなリストを作る。

scala> res0.zipWithIndex
val res8: List[(Int, Int)] = List((1,0), (4,1), (9,2), (16,3), (25,4), (36,5), (49,6))

このタプルに入った添字を活用しよう。drop(n) が適用できるか、試してみる。

scala> res8.map(x => res0.drop(x._2))
val res9: List[List[Int]] = List(List(1, 4, 9, 16, 25, 36, 49), List(4, 9, 16, 25, 36, 49), List(9, 16, 25, 36, 49), Li st(16, 25, 36, 49), List(25, 36, 49), List(36, 49), List(49))

List の各要素に対して、zip できると思うがどうか。

scala> res9.map(x => x.zip(res0).map(x => x._1 + x._2))
val res10: List[List[Int]] = List(List(2, 8, 18, 32, 50, 72, 98), List(5, 13, 25, 41, 61, 85), List(10, 20, 34, 52, 74), List(17, 29, 45, 65), List(26, 40, 58), List(37, 53), List(50))

ここまでくれば flatten して sorted すればいい。そうだ、50 を超える数はいらないのだった。

scala> res10.flatten.filter(x => x <= 50).sorted
val res11: List[Int] = List(2, 5, 8, 10, 13, 17, 18, 20, 25, 26, 29, 32, 34, 37, 40, 41, 45, 50, 50)

以上で、平方の和をソーティングすることはできた。次の作業は、 この平方和を構成する i と j を表示することである。

一つの考えは、平方和をリストで表現するのでなくマップで表現するというものである。 平方和を構成する i と j のタプルをキーとし、平方和を値とするマップが、値でソーティングできるだろうか。

scala> Map((1,1)->2, (1,2)->5, (2,2)->8, (1,3)->10,(2,3)->13,(3,3)->18)
val res12: scala.collection.immutable.Map[(Int, Int),Int] = Map((1,1) -> 2, (1,3) -> 10, (2,2) -> 8, (3,3) -> 18, (2,3) -> 13, (1,2) -> 5)
scala> res13.sorted
error: value sorted is not a member of scala.collection.immutable.Map[(Int, Int),Int]
res0.sorted
^

キーをタプルにしたマップは可能であるが、マップにソーティングのような順序をつけることはできない。 考えてみれば、マップというデータ構造がキーと値の対応づけを行うものである。 したがって、値の順序を制御することは不要と考え、値のソーティングのようなメソッドはないのだろう。

そうなると、別の考えによるしかない。リストの値を(s, i, j)というタプルで表現し、 リストのソーティングの対象をタプルの第1要素である s とする方法である。

まず、このような考えが可能であることを確認したい。

scala> List((2,1,1), (5,1,2), (8,2,2), (10,1,3), (13,2,3), (18,3,3), (17,4,1), (20,4,2),(25,4,3),(32,4,4))
val res14: List[(Int, Int, Int)] = List((2,1,1), (5,1,2), (8,2,2), (10,1,3), (13,2,3), (18,3,3), (17,4,1), (20,4,2),(25,4,3),(32,4,4))
scala> res14.sortBy(x => x._1)
val res15: val res3: List[(Int, Int, Int)] = List((2,1,1), (5,1,2), (8,2,2), (10,1,3), (13,2,3), (17,4,1), (18,3,3), (20,4,2), (25,4,3), (32,4,4))

sortBy メソッドを使うとよい。

さて、(s, i, j) のタプルを作るにはどうするか。いっそのこと、res8 と res8.drop(n) を zip してみてはどうか。 いきなり、res8 と res8.drop(n) の zip はこわいので、res8 どうしの zip から試してみる。

scala> res8.zip(res8)
val res16: List[((Int, Int), (Int, Int))] = List(((1,0),(1,0)), ((4,1),(4,1)), ((9,2),(9,2)), ((16,3),(16,3)), ((25,4),(2 5,4)), ((36,5),(36,5)), ((49,6),(49,6)))

想定される通り、タプルが入れ子になった。ならば、この入れ子のタプルどうしをやりくりすればよい。

scala> res16.map(x=> (x._1._1 + x._2._1, x._1._2, x._2._2))
val res17: List[(Int, Int, Int)] = List((2,0,0), (8,1,1), (18,2,2), (32,3,3), (50,4,4), (72,5,5), (98,6,6))

うまくいっているようだ。なお、得られたタプルは (s, i, j) ではなく (s, i-1, j-1) であるが、 最終出力のところで加減すればいいので、当面このままとする。 では、res8 と res8.drop(n) の zip を試してみる。n = 5 とする。

scala> res8.zip(res8.drop(5)).map(x=> (x._1._1 + x._2._1, x._1._2, x._2._2))
val res18: List[(Int, Int, Int)] = List((37,0,5), (53,1,6))

もう少しだ。いよいよ今までの総合である。

scala> res8.map(x => res8.zip(res8.drop(x._2) ) )
val res19: List[List[((Int, Int), (Int, Int))]] = List(List(((1,0),(1,0)), ((4,1),(4,1)), ((9,2),(9,2)), ((16,3),(16,3)), ((2 5,4),(25,4)), ((36,5),(36,5)), ((49,6),(49,6))), List(((1,0),(4,1)), ((4,1),(9,2)), ((9,2),(16,3)), ((16,3),(25,4)), ((25,4), (36,5)), ((36,5),(49,6))), List(((1,0),(9,2)), ((4,1),(16,3)), ((9,2),(25,4)), ((16,3),(36,5)), ((25,4),(49,6))), List(((1,0) ,(16,3)), ((4,1),(25,4)), ((9,2),(36,5)), ((16,3),(49,6))), List(((1,0),(25,4)), ((4,1),(36,5)), ((9,2),(49,6))), List(((1,0) ,(36,5)), ((4,1),(49,6))), List(((1,0),(49,6))))

期待した出力のように見える。あとは、いくつか処理をすればよい。

scala> res19.flatten.map(x => (x._1._1+x._2._1, x._1._2, x._2._2)).sortBy(x=>x._1).filter(x => x._1 <= 50)
val res20: List[(Int, Int, Int)] = List((2,0,0), (5,0,1), (8,1,1), (10,0,2), (13,1,2), (17,0,3), (18,2,2), (20,1,3), (25,2,3), (26,0,4), (29,1,4), (32,3,3), (34,2,4), (37,0,5), (40,1,5), (41,3,4), (45,2,5), (50,4,4), (50,0,6))

まだ残りがある。平方数の和の上限 n を決めたとき、平方数の和の構成数の数列をどこまで取ればよいかを決めたい。 一つずつ考えてみよう。平方数の和が 4 以下ならば、数列の最大数は 1 だ。 5 以上だと 2 が最大数だが、10 未満であれば 2 のままでよい。 10 以上だと 3 が最大数だが、17 未満であれば 3 のままでよい。 こうやって考えると、和の最大数を n としたとき、数列の最大数を m とすると m = floor(sqrt(n - 1)) だとわかる。 ここで、sqrt は平方根で、floor は小数点以下を切り捨てて整数にする関数である。 Scala だと次のように書ける。
m = Math.sqrt(n-1).toInt
toInt メソッドは Double 型のインスタンスを整数にする。このとき、小数部は切り捨てる。

備考

REPL で長いリストを表示させると、途中で ... となって出力が抑制されることがある。 これはおそらく REPL 故の制約であろう。 すべてを表示させる方法があるのかもしれないが、私にはわからない。 コンパイルをすればすべてが表示される。

ちなみに、SML/NJ の REPL では、Control.Print.printLength := number; とすることによって、表示数をコントロールできるようだ。 (参考 http://eldesh.hatenablog.com/entry/20120221/1329818398)

なお、Scala 2.13.0 以降ではプログラムの書き方によって次のような警告が出る。res2 のところを参照。 警告を避けるには、res3 を求めるように変えなければならない。

scala> List(1,2,3)
val res0: List[Int] = List(1, 2, 3)

scala> List(1,4,9)
val res1: List[Int] = List(1, 4, 9)

scala> res0.map(x => (res1.drop(x-1),res1).zipped map (_+_))
warning: method zipped in class Ops is deprecated (since 2.13.0): Use xs.lazyZip(ys)
val res2: List[List[Int]] = List(List(2, 8, 18), List(5, 13), List(10))

scala> res0.map(x => (res1.drop(x-1).lazyZip(res1) map (_+_))
val res3: List[List[Int]] = List(List(2, 8, 18), List(5, 13), List(10))

ついでに、2.13.3 では REPL が色つきになった。ところが、 私は背景に濃い色のターミナルを使っているものだから、プロンプトの scala> とか、 結果を格納する変数 val0 などの文字が暗くなってしまい見えなくなってしまった。 したがって、色は不要のオプション -Dscala.color=false をつけて起動している。

% scala -Dscala.color=false
Welcome to Scala 2.13.3 (OpenJDK 64-Bit Server VM, Java 14.0.1). Type in expressions for evaluation. Or try :help. scala>

今まで、リストに対するメソッドを使ってきた。これらのメソッドと関連するメソッドをまとめて紹介する。 Scala Standard Library から、 scala → collection → immutable → List (www.scala-lang.org) の一部である。

Value Members
定義名称説明
defdrop(n: Int): List[A] 初めの n 個の要素を除いたコレクションの要素すべてを返す List(3,10,4).drop(2) // List(4)
deffillter(p: (A) => Boolean): List[A] 述語を満たすこのリストの要素すべてを選ぶ List(3,10,4).drop(x => x < 5) // List(3, 4)
defflatten[B](implicit toIterableOnce: (A) => IterableOnce[B]): List[B] 走査可能な(traversable)コレクションを要素とする反復可能な(iterable)このコレクションを、 反復可能なコレクションに変換する。変換されたコレクションの要素は、 操作可能なコレクションの要素である。 List(Set(3,10,4), Set(3,10,4)).flatten
// List(3,10,4,3,10,4)
Set(List(3,10,4), List(10,4,3)).flatten
// Set(3, 10, 4)
final defmap[B](f: (A) => B): List[B] このリスト要素すべてに関数を適用させて新しいリストを生成する List(3,10,4).map(n => n*80) // List(240,800,320)
deftail: List[A] 最初の要素を除いたコレクションの残りを返す List(3,10,4).tail // List(10,4)
defzip[B](that: IterableOnce[B]): List[(A, B)] 反復可能な(iterable)コレクションを返す。この反復可能なコレクションと、 もう一つの反復可能なコレクションで、対応する要素を対にした形式である。 A と B のサイズが異なるときは、短いほうに合わせられる。 List(3,10,4) zip List("sa","to","si", "kun") // List((3,sa), (10,to), (4,si))
defzipAll[A1 >: A, B](that: collection.Iterable[B], thisElem: A1, thatElem: B): List[(A1, B)] 反復可能なコレクションを返す。この反復可能なコレクションと、 もう一つの反復可能なコレクションで、対応する要素を対にした形式である。 このコレクションともう一つのコレクションでサイズが異なる場合、サイズが大きいほうに合ったコレクションとなる。 このコレクションのサイズが多い場合、足りないペアは B が当てられる。 逆にもう一つのコレクションのサイズが多い場合は、足りないぺアは A が当てられる。 List("sa","to","si").zipAll(List(3, 10, 4, 0, 0), "maru", "yama") // List((sa,3), (to,10), (si,4), (maru, 0), (maru, 0))
defzipWithIndex: List[(A, Int)] この反復可能なコレクションと添字(index)を縫合(zip)する List(3,10,4).zipWithIndex // List((3,0), (10,1), (4,2))
非推奨の Value Members
定義非推奨名称代替など
def/:[B](z: B)(op: (B, A) => B): B (version 2.13.0 から) /: の代わりに foldLeft を使う

まりんきょ学問所コンピュータの部屋 > Scala しい新世界


MARUYAMA Satosi