読者です 読者をやめる 読者になる 読者になる

日々精進

aikoと旅行とプログラミングと

はじめてのScala【コップ本第3章】

【ステップ7】配列を型でパラメータ化する

パラメータ化とは「生成するインスタンスの構造を設定すること」を指す。Scalaではnew演算子を用いることでインスタンス化することができ、次のように記述する。

val big = new java.math.BigInteger("12345")

このようにカッコの引数として値を与えれば、インスタンスをパラメータ化することができる。

val greetStrings = new Array[String]()
greetStrings(0) = "Hello"
greetStrings(1) = ", "
greetStrings(2) = "world!\n"
for(i <- 0 to 2)
  print(greetStrings(i))

このように角括弧に型を1つ以上書き込むことで、インスタンスのパラメータの型を指定できる。このようにして配列を生成することもできるが、推奨はされていない。
先ほどのコードを、型を明示的に指定して書くと以下のようになる。

val greetStrings: Array[String] = new Array[String](3)

ここで重要なのは、greetStringsの型はArrayStringではなくArray[String]という点である。 さて、先ほどのコードではgreetString(0) = "Hello"のように代入が行われている。しかし変数はvalとして宣言されている。これは、valで宣言した変数自体には代入できないが、参照しているオブジェクト自体は変更される可能性があることを示している。
次のコードを見てみよう。

for(i <- 0 to 2)
  print(greetStrings(i))

for式の先頭行は、「メソッドのパラメータが1つだけならドットやカッコを使わずに呼び出せる」という原則を示している。つまり、0 to 2というコードは、(0).to(2)というメソッド呼び出しと等価である。
Scalaでは+や-などの文字をメソッド名で使用することができる。そのため、1+2という式も「1オブジェクトの持つ+メソッドを引数として2を与えて呼び出している」と考えることができる。

applyメソッド

上式のようにカッコで囲んだ1つ以上の変数を適用したコードは、applyメソッドの呼び出しに書き換えられる。つまり、greetStrings(i)という式がgreetStrings.apply(i)となる。例えば次の式

val numNames = Array("one", "two", "three")

これは配列の生成と初期化のコードである。このコードは次のコードと等価である。

val numNames = Array.apply("one", "two", "three")

このように書くことで、簡潔に配列を生成することができる。

updateメソッド

カッコで囲まれた1個以上の引数を伴う変数の代入は、コンパイラによってupdateメソッドの呼び出しに代わる。たとえば

greetStrings(0) = "Hello"

という式は、次のコードに書き換えられる。

greetStrings.update(0, "Hello")

Scalaでは配列や式などすべてがメソッドを持つオブジェクトと考えることによって概念を抽象化している。

【ステップ8】リストを使う

ScalaにはListクラスというものが存在している。これはイミュータブルなオブジェクトであり、すべての要素が同じ型である。(JavaのListはミュータブルである)
Listは以下のように作成・初期化する。

val oneTwoThree = List(1, 2, 3)

Listはイミュータブルであることから、Javaの文字列と同じような振る舞いをする。例えばリストの内容を書き換えるような操作を行った場合、操作をした新しいリストが返されるのである。
次にコード例を示す。

val oneTwo = List(1, 2)
val threeFour = List(3, 4)
val oneTwoThreeFour = oneTwo ::: threeFour
println(oneTwo + " and " + threeFour + " were not mutated.")
println("Thus, " + oneTwoThreeFour + "is a new list.")

:::は2つのリストを結合するメソッドである。(メソッド名の末尾がコロンの場合に限って、メソッドの結合性は通常と逆になる。)
このプログラムを実行すると

List(1, 2) and List(3, 4) were not mutated.
Thus, List(1, 2, 3, 4)is a new list.

その他にも::というメソッド(cons演算子)も存在する。このメソッドは既存のリストに新しい要素を追加して得られるリストを返す。

val twoThree = List(2, 3)
val oneTwoThree = 1 :: twoThree
println(oneTwoThree)

このようなプログラムを実行すると、

List(1, 2, 3)

となる。

からのリストはNilと書く。Listは上記の方法以外にも、cons演算子(::)を用いることでも初期化することができる。

val oneTwoThree = 1 :: 2 :: 3 :: Nil
println(oneTwoThree)

と書くことで、上記のプログラムと同様の結果を得られる。
Listには多数の便利なメソッドが存在している。気になったものだけ書いておく

コード 意味
Nil or List() 空リスト
obj.count(s => s.length == 4) 文字列の長さが4の要素の数を返す
obj.drop(2) 先頭の2要素を取り除いたリストを返す
obj.filter(s => s.length == 4) 文字列の長さが4のすべての要素を順に並べたリストを返す
obj.foreach(s => println(s)) objリストに含まれる1つ1つの文字を出力する
obj.map(s => s + "y") objリストの各要素に"y"を付加したものから構成されるリストを返す
obj.mkString(", ") リストの要素を並べた(引数とした文字列で結合した)要素を作る
obj.reverse リストの要素を逆順にする
println(thrill.sortWith( (s, t) => s.charAt(0).toLower < t.charAt(0).toLower) ) リストのすべての要素を小文字にした際の辞書順にソートを行ったリスト

【ステップ9】タプルを使う

タプルはイミュータブルなオブジェクトであるが、リストとは異なり異なる型の要素を持つことができる。

val pair = (226, "Tarea")
println(pair._1)
println(pair._2)

1行目で226という整数とTareaという文字列を持つタプルを生成している。タプルで要素にアクセスするには_(アンダーバー)の後に数字を書くことでアクセスできる。ゼロインデックスではないので注意が必要だ。(HaskellやMLなどで、静的に型付けされたタプルというのはindexが1から始まるという伝統が築かれているらしい。)

【ステップ10】集合とマップを使う。

Scalaには集合(set)とマップ(map)という構造が有り、それぞれミュータブルなものとイミュータブルなものが用意されている。トレイトという概念が出てきたが、後で詳しくやるようなのでとりあえずスルー(Javaでいうインタフェースに似たものらしい)
とりあえずコードを見てみる。以下はイミュータブルなセットである。

集合(set)
var jetSet = Set("Airbus", "Boeing")
jetSet += "Lear"
println(jetSet.contains("Cessna"))

1行目もapplyファクトリメソッドを呼び出しているので、jetSet = Set.apply("Airbus", "Boeing")と等価である。集合に新たな要素を追加するためには、新しい要素を引数として+メソッドを呼び出す。2行目は実質的に

jetSet = jetSet + "Lear"

であることから、(jetSet).+("Lear")と呼び出し、それによって作られた新たなセットをjetSetに代入しているのである。

続いてミュータブルなセットを紹介する。

import scala.collection.mutable.Set
val movieSet = Set("Hitch", "Poltergeist")
movieSet += "Shrek" //(movieSet).+=("shrek")
println(movieSet)

今回は集合movieSetの+=メソッドを呼び出すことで要素を追加している。つまり、3行目のコードは

(movieSet).+=("Shrek")

と等価である。

マップ(map)
import scala.collection.mutable.Map
val treasureMap = Map[Int, String]()
treasureMap += (1 -> "Go to island. ") 
treasureMap += (2 -> "Find big X on ground. ")
treasureMap += (3 -> "Dig.")
println(treasureMap(2))

1行目はミュータブルなマップをインポートしている。2行目は、整数をキー、値を文字列としたMapを生成している。3〜5行目は、(1).->("Go to island. ")のように->メソッドを呼び出し、その戻りであるタプルをMapに追加している。
続いてはイミュータブルなマップの生成・初期化例である。

val romanNumeral = Map(
  1 -> "Ⅰ", 2 -> "Ⅱ", 3 -> "Ⅲ", 4 -> "Ⅳ", 5 -> "Ⅴ"
)
println(romanNumeral(4))

インポート文がないので、イミュータブルなMap(scala.collection.immutable,Map)が作られる。

【ステップ11】関数型のスタイルを見分ける

方針として - varじゃなくvalを使おう。 - varを使ってはいけないというわけではなく、状況に応じて適当に使い分けよう。(でもなるべくvalにしよう) という感じらしい。コード例を見ながら学んでいくことにする。

def printArgs(args: Array[String]): Unit = {
  var i = 0
  while(i < args.length){
    println(args(i))
    i += 1
  }
}

いわゆる命令形で書かれた関数である。とりあえずこの関数からvarを取り除いてみる。

def printArgs(args: Array[String]): Unit = {
  for(arg <- args){
    println(arg)
  }
}

少し関数型のコードに近づいた。forを取り除きたいのなら、

def printArgs(args: Array[String]): Unit = {
  args.foreach(println)
}

と書くこともできる。varがなくなると、簡潔明快になりエラーを起こしにくくなる。これでも良いように見えるが、まだ関数型になりきれていない。というのも、このコードには副作用が存在しているためである。
Scalaにおいて、結果型がUnitであるものは副作用があると考えて良い。上記のコードにおいてより関数型的なアプローチを行うのなら、標準関数の出力するのではなく文字列を関数の結果値としてそのまま返すように実装すべきである。

def printArgs(args: Array[String]): Unit = args.mkString("\n")

このようにすることで、varも副作用も存在していない関数型的なコードになった。
副作用を持たないメソッドを心がけることは、プログラムをテストしやすくなるというメリットを得られる。

【ステップ12】ファイルから行を読みだす

ファイルから1行ずつ読みだし、行頭に各行の文字数を加えて出力するコードを書いていく。まずはじめに以下の様なプログラムを考える。

import scala.io.Source

if(args.length > 0){
  val lines = Source.fromFile(args(0)).getLines().toList

  for(line <- lines)
    println(line.length + " " + line)
}else{
    Console.err.println("Please enter filename")
}

Source.fromFile()はファイルオープンを行い、getLinesメソッドで1行ずつイテレータを返す。各行について処理し、文字数+空白 + 文字列を出力する。結果は

22 import scala.io.Source
0
60 def widthOfLength(s: String): Int = s.length.toString.length
0
20 if(args.length > 0){
56   val lines = Source.fromFile(args(0)).getLines().toList
0
20   for(line <- lines)
37     println(line.length + " " + line)
0
6 }else{
48     Console.err.println("Please enter filename")
1 }

となる。見難いので数字を右寄せにして、文字と数字の間にパイプを追加して出力することにする。改善したコードは以下のようになる。

import scala.io.Source

def widthOfLength(s: String): Int = s.length.toString.length

if(args.length > 0){
  val lines = Source.fromFile(args(0)).getLines().toList

  val longestLine = lines.reduceLeft(
    (a, b) => if(a.length > b.length) a else b
  )
  val maxWidth = widthOfLength(longestLine)

  for(line <- lines){
    val numSpaces = maxWidth - widthOfLength(line)
    val padding = " " * numSpaces
    println(padding + line.length + " | " + line)
  }
}else{
    Console.err.println("Please enter filename")
}

順を追って説明する。

def widthOfLength(s: String): Int = s.length.toString.length

引数の文字列の長さを何文字で表示できるかを返す。

val lines = Source.fromFile(args(0)).getLines().toList

先ほどとは違い、最後にtoListを呼び出している。これによってオープンしたファイルの各行がListとして保存される。

val longestLine = lines.reduceLeft(
  (a, b) => if(a.length > b.length) a else b
)
val maxWidth = widthOfLength(longestLine)

ここでは最も長い文字列の長さを何文字で表示できるか取得している。(たとえば5文字なら1, 20文字なら2である)
reduceLeftは渡された関数を最初の2行に適用し、この時の結果をlinesの次の要素と比較していく。最終的にlongestLineには最も長い文字列が入っているので、これをwidthOfLengthに渡すことで「何文字で文字列の長さを表現できるのか」を知ることができる。

for(line <- lines){
  val numSpaces = maxWidth - widthOfLength(line)
  val padding = " " * numSpaces
  println(padding + line.length + " | " + line)
}

あとは適切に整形し出力するだけ。