LoginSignup
2
0

More than 3 years have passed since last update.

オブジェクト指向から関数型プログラミングへ『Scala 入門 (Essential Scala)』第2章 式・型・値

Last updated at Posted at 2020-04-28

CC BY-SA 4.0 で公開されている Essential Scala2 Expressions, Types, and Values を日本語訳したものです。日本語訳においてもライセンスは同じものを継承しています。毎日少しずつ翻訳を進めており、最新の日本語訳は Scala 入門 (Essential Scala) にあります。

原書:underscoreio/essential-scala
訳書:takuya0301/essential-scala-ja

ライセンス

Written by Dave Gurnell and Noel Welsh. Copyright Underscore Consulting LLP, 2015-2017.
Translated by Takuya Tsuchida. Copyright Takuya Tsuchida, 2020.
CC BY-SA 4.0 logo
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

目次

序文
第1章 はじめに
第2章 式・型・値
第3章 オブジェクトとクラス
第4章 トレイトによるデータモデリング
第5章 シーケンス処理
第6章 コレクション
第7章 型クラス

第2章 式・型・値

本章では Scala プログラムの基本的な構成要素である式 (expression)型 (type)値 (value) について見ていきます。それらのコンセプトを理解することが Scala プログラムがどのように動くのかというメンタルモデルを形成するために必要です。

最初のプログラム

Scala コンソールか Scala ワークシートに "Hello world!" と入力し、コンソールでリターンキーを押下するかワークシートを保存してください。このようなインタラクションが見られるはずです。

"Hello world!"
// res0: String = Hello world!

このプログラムについてはいろいろ言えることがあります。それは特別にリテラル式 (literal expression) もしくは略してリテラルと呼ばれる単一の式で構成されていることです。

Scala は私たちのプログラムを実行(評価)します。Scala コンソールや Scala ワークシートでプログラムを評価するとき、プログラムのとプログラムを評価したという2つの情報を受け取ります。この場合は、型が String で、値が "Hello world!" です。

プログラムが生成した出力値 "Hello world!" はプログラムと同じに見えますが、その2つの間には違いがあります。リテラル式は入力したプログラムテキストである一方、コンソールが表示したものはプログラムを評価した結果です。(リテラル (literal) は、評価されたものがその文字通り (literally) に見えるので名付けられました。)

わずかに複雑なプログラムを見てみましょう。

"Hello world!".toUpperCase
// res1: String = HELLO WORLD!

このプログラムはメソッド呼び出し (method call) を加えることによって最初の例を拡張したものです。Scala における評価は左から右へ進みます。この例では、最初にリテラル "Hello world!" が評価されます。次に、その評価された結果に対しメソッド toUpperCase が呼ばれます。このメソッドは文字列値を大文字に変換したものを新しい文字列として返します。これがコンソールによって表示された最終的な値です。

繰り返しになりますが、このプログラムの型は String で、この場合はプログラムが "HELLO WORLD!" に評価されます。

コンパイル時と実行時

Scala プログラムが通過する明確な2つの段階があります。最初がコンパイル (compile) で、コンパイルが成功していれば、次が実行 (run) または評価です。最初のステージをコンパイル時 (compile-time)、次のステージを実行時 (run-time) と呼びます。

Scala コンソールを使用しているとき、プログラムはコンパイルしてすぐに評価されるので、1つの段階だけがあるような印象を抱かせます。型と値の間における違いを正しく理解するためにも、コンパイル時と実行時がまったく異なることを理解するのは重要です。

コンパイルはプログラムが意味をなしているかを検証する工程です。プログラムが「意味をなす」には2つの観点が必要です。

  1. プログラムは文法的に正確 (syntactically correct) でなければなりません。それはプログラムの部品が言語の文法に従っているということを意味します。"on cat mat sat the"(猫の上敷き物座った)は文法的に正確ではない英文の例です。これは文法的に正確ではない Scala プログラムの例です。

    toUpperCase."Hello world!"
    // <console>:2: error: identifier expected but string literal found.
    // toUpperCase."Hello world!"
    //             ^
    
  2. プログラムは型検証 (type check) されなければなりません。意味をなすプログラムであるということは、プログラムがある制約に従っているということを意味します。"the mat sat on the cat"(敷き物は猫の上に座った)は文法的に正確ですが意味がわからない英文の例です。これは数値を大文字に変換しようとして型検証に失敗するシンプルなプログラムです。

    2.toUpperCase
    // <console>:13: error: value toUpperCase is not a member of Int
    //        2.toUpperCase
    //          ^
    

大文字小文字という概念は数値について意味をなさないので、型システムはこのエラーを捕捉します。

プログラムがコンパイル時の検証を通過した場合、次にプログラムは実行されるかもしれません。実行はコンピューターがプログラムにある指示を実行する工程です。

プログラムのコンパイルが成功したとしても、実行時に失敗する可能性が残っています。整数を0で割ると Scala では実行時エラーが発生します。

2 / 0
// java.lang.ArithmeticException: / by zero
//   ... 342 elided

整数の型 Int はプログラムの型検証を通過すれば割り算できます。しかし、割り算の結果を表現できる Int が存在しないので、実行時にプログラムは失敗します。

式・型・値

それでは、式・型・値とは正確には何でしょうか?

式はファイルやコンソール、ワークシートに入力したプログラムテキストの一部です。それは Scala プログラムの主要な構成要素です。のちほど、定義 (definition)文 (statement) と 名付けられた他の構成要素も見ていきます。式はコンパイル時に存在します。

式の特徴を定義すると、それは値に評価されるということです。値はコンピューターのメモリに保持されます。それは実行時に存在します。例えば、式 2 は、コンピューターのメモリの、特定の場所の、特定のビット列に評価されます。

私たちは値を用いて計算します。値はプログラムが受け渡したり操作したりする実体です。例えば、2つの数値の最小値を計算するために、下記のようなプログラムを書くでしょう。

2.min(3)
// res4: Int = 2

ここに2つの値 23 があり、それらを 2 に評価されるより大きなプログラムに結合しています。

Scala において、すべての値はオブジェクト (object) で、のちほど見ていくように特定の意味を持ちます。

さて次に型のことを考えましょう。型はプログラム上の制約で、どのようにオブジェクトを操作できるかを制限します。すでに2つの型 StringInt を、そして型によって異なる操作を実行できることを見てきました。

この段階で、型についてもっとも重要な点は式は型を持つが値は持たないということです。私たちはコンピューターのメモリの任意の部分を検査できませんし、それを生成したプログラムを知らずして、どのようにそれが解釈されるのかを予言できません。例えば、Scala で Int 型と Float 型はどちらもメモリの32ビットによって表現されます。しかし、与えられた32ビットを IntFloat として解釈すべきというタグやその他の表示はないのです。

実行時エラーを引き起こす式の型を教えてと、Scala コンソールに尋ねることによって、コンパイル時に型が存在することを明らかにできます。

:type 2 / 0
// Int
2 / 0
// java.lang.ArithmeticException: / by zero
//   ... 486 elided

2 / 0 は、それを評価したときには失敗するにも関わらず、Int 型を持つことを見ました。

コンパイル時に存在する型は、値に一貫した解釈を与えるプログラムを書くように制約します。特定の32ビットが、ある時は Int で、またある時は Float であると断言はできません。プログラムの型が検証されたとき、Scala はすべての値が一貫して使用されることを保証するので、値の表現で型の情報を記録する必要がありません。型の情報を取り除くこの処理を型消去 (type erasure)1と呼びます。

必然的に、型に合致する値について、考え得るすべての情報をその型は含みません。そうしないと、型検証はプログラムを実行することと等価になってしまいます。すでに見てきたように、型システムは Int をゼロで割ることを防ぐことはなく、それは実行時エラーを引き起こします。

Scala コード設計の主要部分は、型システムの利用において、どのエラーケースを無視したいのかを決定することです。型システムでたくさんの便利な制約を表現することで、プログラムの信頼性を向上させられることを見ていきます。もしプログラムにおいて十分に重要であると決定すれば、エラーの可能性を表現する型システムを使用した除算演算子を実装することができます。型システムを上手に使用することは本書における重要なテーマのひとつです。

覚えておいてほしいこと

Scala を使用するのであれば、Scala プログラムのメンタルモデルを構築しなければなりません。このモデルにおける3つの基本的な構成要素はです。

式は値に評価されるプログラムの部品です。Scala プログラムの主要な部品になります。

式は型を持ち、プログラムの制約を表現します。コンパイル時にプログラムの型は検証されます。型に一貫性がない場合、コンパイルは失敗し、プログラムを評価(実行)することはできません。

値はコンピューターのメモリに存在し、実行中のプログラムが操作するものです。Scala におけるすべての値はオブジェクトで、その意味はのちほど議論します。

演習

型と値

Scala コンソールか Scala ワークシートを使用して、下記の式の型と値を特定してください。

1 + 2

解答(クリックして表示)

型は Int で値は 3 です。

"3".toInt

解答(クリックして表示)

型は Int で値は 3 です。

"foo".toInt

解答(クリックして表示)

型は Int ですが、これは値に評価されません。その代わりに例外が発生し、発生した例外は値ではありません。これをどう理解すればいいでしょうか?式の結果による計算を続けられないということです。例えば、それを印字することはできません。
println("foo")
// foo

println("foo".toInt)
// java.lang.NumberFormatException: For input string: "foo"
//   at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
//   at java.lang.Integer.parseInt(Integer.java:580)
//   at java.lang.Integer.parseInt(Integer.java:615)
//   at scala.collection.immutable.StringLike$class.toInt(StringLike.scala:272)
//   at scala.collection.immutable.StringOps.toInt(StringOps.scala:29)
//   ... 730 elided

を比較してみてください。後者でどんな印字も発生しないことは、println が評価されないということを示しています。

オブジェクトとの相互作用

前節では Scala プログラムの基本的な構成要素である式・型・値を見てきました。また、すべての値はオブジェクトであることも学びました。本節では、オブジェクトとオブジェクトに対してどのように相互作用するのかについて学んでいきます。

オブジェクト

オブジェクトはデータとそのデータへの操作のグループです。例えば、2 はオブジェクトです。データは整数の2で、データへの操作はなじみのある +- などです。

オブジェクトのデータと操作について特別な専門用語があります。操作はメソッド (method) として知られています。データはフィールド (field) に保持されます。

メソッド呼び出し

メソッド呼び出し (call) によってオブジェクトを作用させます2。メソッド呼び出しの例をいくつかすでに見てきました。例えば、String の大文字版をその toUpperCase メソッドを呼び出すことによって取得できることを見ました。

"hello".toUpperCase
// res0: String = HELLO

一部のメソッドは引数 (parameter) を受け取り、それはメソッドがどのように動作するかを制御します。例えば、take メソッドは String から文字を取り出します。いくつの文字を取り出したいかを指定する引数を take に渡す必要があります。

"abcdef".take(3)
// res1: String = abc

"abcdef".take(2)
// res2: String = ab

:information_source: メソッド呼び出し文法

メソッド呼び出しのための文法は、

anExpression.methodName(param1, ...)

anExpression.methodName

です。ここで、

  • anExpression はオブジェクトに評価される任意の式
  • methodName はメソッドの名前
  • param1, ... はメソッドの引数に評価される1つ以上の式

とします。


メソッド呼び出しは式なのでオブジェクトに評価されます。これは、より複雑なプログラムをつくるために、メソッド呼び出しを連鎖できるということを意味します。

"hello".toUpperCase.toLowerCase
// res3: String = hello

メソッド呼び出しにおける様々な式はどの順番で評価されるのでしょうか?メソッド引数は、メソッドが呼び出される前に左から右に評価されます。下記の式では、

"Hello world!".take(2 + 3)
// res4: String = Hello

最初に式 "Hello world!" が、次に 2 + 3(これは、最初に 2 を、次に 3 を評価する必要があります。)が、そして最後に "Hello world!".take(5) が評価されます。

演算子

Scala においてすべての値はオブジェクトであるため、IntBoolean のようなプリミティブ型についてもメソッドを呼び出すことができます。これは intboolean がオブジェクトではない Java と対照的です。

123.toShort // これは Scala で `Short` を定義する方法です
// res5: Short = 123

123.toByte // これは `Byte` を定義する方法です
// res6: Byte = 123

しかし、Int がオブジェクトであれば、+- のような基本的な算術演算子は何でしょうか?それらもまたメソッドでしょうか?そうです。Scala のメソッドは英数字の名前と同じようにシンボルの名前を持つことができます。

43 - 3 + 2
// res7: Int = 42

43.-(3).+(2)
// res8: Int = 42

(Scala 2.10 以前においては、43.Double として解釈されることを防ぐために、(43).-(3).+(2) と書く必要があるので注意してください。)


:information_source: 中置演算子記法

Scala において a.b(c) と書かれた任意の式は a b c と書けます。

a b c d e が等価であるのは a.b(c).d(e) で、a.b(c, d, e) ではないことに注意します。


シンボルの名前を持つか、英数字の名前を持つかにかかわらず、1つの引数を受け取るどんなメソッドでも中置演算子記法 (infix operator notation) を使用できます。

"the quick brown fox" split " "
// res: Array[String] = Array(the, quick, brown, fox)

中置記法は、いくつかの文法的速記のひとつで、冗長なメソッド呼び出しの代わりに、簡潔な演算子式を書くことを可能にします。前置 (prefix)後置 (postfix)右結合 (right-associative)代入演算子 (assignment-style) という記法もありますが、中置記法に比べると一般的ではありません。

中置演算子の結合は何の優先順位規則を支持すべきか?という質問はそれ自身をもたらします。Scala は、数学や論理の直観的知識にならい、メソッド名として使用する識別子から得られる優先順位規則一式を使用します。

2 * 3 + 4 * 5
// res11: Int = 26

(2 * 3) + (4 * 5)
// res12: Int = 26

2 * (3 + 4) * 5
// res13: Int = 70

覚えておいてほしいこと

Scala のすべての値はオブジェクトです。それらのメソッド呼び出しによってオブジェクトを作用させます。Java を背景知識としているのであれば、Int や他の任意のプリミティブ値のメソッドを呼び出せることに注意してください。

メソッド呼び出しのための文法は、

anExpression.methodName(parameter, ...)

anExpression methodName parameter

です。

Scala は非常に少ない演算子を持ち、ほとんどすべてはメソッド呼び出しです。中置演算子記法のような文法規則をコードを簡潔かつ読みやすくするために使用しますが、標準のメソッド記法の方がわかりやすい場合はいつでもそれに戻すことができます。

のちほど見ていくように、式を伴うプログラミングにおける Scala の焦点は、Java で実現するよりさらに短いコードで書くことを可能にすることです。また、値と型を使用する非常に直観的な方法で、コードについての判断を可能にします。

演習

演算子スタイル

演算子スタイルに書き直してください。

"foo".take(1)
// res14: String = f

解答(クリックして表示)
"foo" take 1
// res15: String = f

メソッド呼び出しスタイルに書き直してください。

1 + 2 + 3
// res16: Int = 6

解答(クリックして表示)
1.+(2).+(3)
// res17: Int = 6

置換

下記の2つの式の間にある違いは何ですか?類似点は何ですか?

1 + 2 + 3

6

解答(クリックして表示)

2つの式は同じ結果型と同じ返却値を持ちます。しかし、それらは異なった方法でその結果に辿り着きます。前者は一連の加算によってその結果を計算する一方、後者はただ単にリテラルです。

どちらの式も副作用を持たないので、ユーザー視点からそれらは交換可能です。1 + 2 + 3 と書けるところはどこでも 6 と書け、どんなプログラムの意味も変えることはありません。逆もまた同様です。これは置換 (substitution)(訳注:数学だと代入という訳語が一般的です。)として知られており、あなたは数式を簡単にする原理として学校で記憶しているかもしれません。

プログラマーとしてはコードがどのように動くのかというメンタルモデルを養う必要があります。評価の置換モデルは目に入る式はどれでもその結果と置換して構わないというとりわけ単純なモデルです。副作用がないことで、式の置換モデルはいつでも機能します3。式の各構成要素である型と値を知っていれば、式全体としての型と値を知っています。関数型プログラミングにおいて副作用を避けようと努力している理由は、それがプログラムを容易に理解できるようにするからです。

リテラルオブジェクト

すでに Scala の基本型のいくつかを取り上げました。本節では、Scala におけるすべてのリテラル式を扱うことによって、その知識に磨きをかける予定です。リテラル式は「それ自身」が象徴する固定値を表現します。下記はひとつの例です。

42
// res0: Int = 42

この REPL での相互作用は、リテラル 42Int 42 に評価されることを示しています。

リテラルとそれが評価された値を混同してはいけません。リテラル式はプログラムを実行する前のプログラムテキストにおける表現で、値はプログラムを実行した後のコンピューターのメモリにおける表現です。

これまでにプログラミングの経験、とくに Java の経験があれば、Scala におけるリテラルに見覚えがあるはずです。

数値

数値は Java にある同じ型を共有しており、32ビット整数の Int、64ビット浮動小数点数の Double、32ビット浮動小数点数の Float、そして64ビット整数の Long があります。

42
// res1: Int = 42

42.0
// res2: Double = 42.0

42.0f
// res3: Float = 42.0

42L
// res4: Long = 42

Scala は16ビット整数の Short と8ビットの Byte を持ちますが、それらを生成するためのリテラル文法はありません。その代わりに、toShorttoByte と呼ばれるメソッドを使用することで生成できます。

真偽値

真偽値は Java とちょうど同じで、truefalse です。

true
// res5: Boolean = true

false
// res6: Boolean = false

文字値

Chars は16ビット Unicode 値で、シングルクォートで囲まれた単一の文字として書かれます。

'a'
// res7: Char = a

:information_source: Scala 対 Java の型階層

最初の文字が大文字で書かれていますが、Scala の IntDoubleFloatLongShortByteBooleanChar は、Java の intdoublefloatlongshortbytebooleanchar とちょうど同じものを参照します。

Scala において、それらすべての型はメソッドとフィールドを伴うオブジェクトのように振る舞います。しかしながら、一度コードがコンパイルされたら、Scala の Int は Java の int とちょうど同じです。これが2つの言語間の相互運用を簡単にしてくれるのです。


文字列値

文字列値はちょうど Java の文字列で、同じ方法で書かれます。

"this is a string"
// res8: String = this is a string

"the\nusual\tescape characters apply"
// res9: String =
// the
// usual    escape characters apply

Null 値

Null 値は Java と同じであるにもかかわらず、ほとんど頻繁には使用されません。また、Scala の null は独自の Null 型を持ちます。

null
// res10: Null = null

:information_source: Scala で Null 値を使用する

null は Java コードで一般的ですが、Scala では非常に悪い習慣であると考えられています。

Java における null の主な用途は、プログラム実行の異なる時点において、値の有無という任意 (optional) 値を実装することです。しかしながら、null 値はコンパイラーによってチェックできないため、NullPointerException という形で実行時エラーが発生する可能性があります。

のちほど、コンパイラーによってチェックされる任意値を定義する手段を Scala が持つことを見ていきましょう。これは null を使用する必要性を取り除き、プログラムをより安全にしてくれます。


Unit 値

Scala で () と書かれる Unit 値は、Java の void に相当します。Unit 値は、println を使用して標準出力に印字するような、何の面白みもない値に評価される式の結果です。コンソールは Unit 値を印字しませんが、式の型を尋ねることで、実際に何らかの式の結果であることがわかります。

()
:type ()
// Unit
println("something")
// something
:type println("something")
// Unit

Unit 値は Scala において重要な概念です。Scala の文法構造の多くは、型と値を持つです。有用な値を得られない式のためのプレイスホルダーが必要で、Unit 値はまさにそれを提供してくれます。

覚えておいてほしいこと

本節では、基本的なデータ型を評価するリテラル式を見てきました。これらの基本型は、等価なものがない Unit を除いて、ほとんど Java と同じです。

すべてのリテラル式はを持ち、に評価されることに注意してください。これは、より複雑な Scala の式にも当てはまります。

次節では、独自のオブジェクトリテラルを定義する方法を学びます。

演習

文字通りただのリテラル

下記の Scala リテラルの値と型は何ですか?

42

true

123L

42.0

解答(クリックして表示)

42Int です。trueBoolean です。123LLong です。42.0Double です。

この演習は、Scala コンソールや Scala ワークシートを使用した体験をするだけです。

引用と誤引用

下記のリテラルの違いは何ですか?それぞれの型と値は?

'a'

"a"

解答(クリックして表示)

1つ目は Char リテラルであり、2つ目は String リテラルです。

副作用についての余談

下記の式の違いは何ですか?それぞれの型と値は?

"Hello world!"

println("Hello world!")

解答(クリックして表示)

リテラル式 "Hello world!"String の値として評価されます。式 println("Hello world!")Unit として評価され、副作用としてコンソールに "Hello world!" を印字します。

これは、値を評価するプログラムと、値を副作用として印字するプログラムとの間の重要な区別です。前者はより大きな式で使用できますが、後者は使用できません。

失敗から学ぶ

下記のリテラルの型と値は何ですか?REPL や Scala ワークシートに書いてみて、何が起こるか見てみましょう!

'Hello world!'

解答(クリックして表示)

エラーメッセージを見ることになるはずです。開発環境のエラーメッセージを読んで慣れることに時間をかけてください。すぐに他にもたくさん見るようになるので!

オブジェクトリテラル

ここまでは、IntString のような組み込み型のオブジェクトを作成し、それらを式に組み合わせる方法を見てきました。本節では、オブジェクトリテラル (object literals) を使用して独自デザインのオブジェクトを作成する方法を見ていきます。

オブジェクトリテラルを書くとき、式とは別のプログラムの一種である宣言 (declaration) を使います。宣言は値を評価しません。その代わりに名前と値を関連付けます。この名前は他のコードで値を参照するために使用できます。

下記のように空のオブジェクトを宣言できます。

object Test {}

これは値に評価されず式ではありません。むしろ、名前 Test を空のオブジェクト値に結び付けます。

一度 Test という名前に結び付ければ式の中で使用することができ、それは宣言したオブジェクトに評価されます。もっとも単純な式はそれ自身の名前だけで、それ自体が値として評価されます。

Test
// res0: Test.type = Test$@3f447ef3

この式は 123"abc" のようなリテラルを書くことと同じです。オブジェクトの型は Test.type として報告されることに注意してください。これは、これまで見てきた型とは異なり、オブジェクトのためだけに作成された新しい型で、シングルトン型 (singleton type) と呼ばれます。この型における他の値を作成することはできません。

空のオブジェクトはあまり便利ではありません。オブジェクト宣言の本体である中括弧の間には式を入れることができます。しかし、メソッドやフィールド、さらなるオブジェクトを宣言するような宣言を入れるのが一般的です。


:information_source: オブジェクト宣言文法

オブジェクト宣言のための文法は、

object name {
  declarationOrExpression ...
}

です。ここで、

  • name はオブジェクトの名前
  • declarationOrExpression は宣言か式(オプション)

とします。


それではメソッドとフィールドをどのように宣言するか見ていきましょう。

メソッド

メソッドによってオブジェクトを作用させるので、メソッドを伴うオブジェクトを生成してみましょう。

object Test2 {
  def name: String = "Probably the best object ever"
}

ここでは name と呼ばれるメソッドを生成しました。これをいつもの方法で呼び出すことができます。

Test2.name
// res1: String = Probably the best object ever

下記はより複雑なメソッドを伴うオブジェクトです。

object Test3 {
  def hello(name: String) =
    "Hello " + name
}
Test3.hello("Noel")
// res2: String = Hello Noel

:information_source: メソッド宣言文法

メソッドを宣言するための文法は、

def name(parameter: type, ...): resultType =
  bodyExpression

def name: resultType =
  bodyExpression

です。ここで、

  • name はメソッドの名前
  • parameter はメソッドに与えられる引数の名前(オプション)
  • type はメソッド引数の型
  • resultType はメソッドの結果型(オプション)
  • bodyExpression はメソッドを呼び出すことで評価される式

とします。メソッド引数はオプションですが、メソッドが引数を持つのであれば、それらの型は与えられなければなりません。結果型はオプションであるにもかかわらず、それを定義することが、機械的に検証されたドキュメントとしての役割を果たすので良い習慣とされます。

引数 (argument) という用語はパラメーター (parameter) と言い換えることができます。(訳注:本書ではどちらの用語も引数という訳語に統一しています。)



:information_source: 返却は暗黙的

メソッドの返却値は本体を評価することによって決定されます。Java でするように return を書く必要はありません。


フィールド

オブジェクトはフィールド (field) と呼ばれる他のオブジェクトを含めることもできます。def によく似た valvar というキーワードを使用して導入します。

object Test4 {
  val name = "Noel"
  def hello(other: String): String =
    name + " says hi to " + other
}
Test4.hello("Dave")
// res3: String = Noel says hi to Dave

:information_source: フィールド宣言文法

フィールドを宣言するための文法は、

val name: type = valueExpression

var name: type = valueExpression

です。ここで、

  • name はフィールドの名前
  • type はフィールドの型(オプション)
  • valueExpressionname に束縛されるオブジェクトに評価される式

とします。


val を使用することで不変 (immutable) フィールドを定義でき、これは名前に束縛された値の変更ができないことを意味します。var可変 (mutable) フィールドで、束縛された値の変更ができます。

常に var より val を選びましょう。 置換を維持するため、Scala プログラマーは可能な限り不変フィールドを使用することを選びます。アプリケーションコードで時折可変フィールドを生成することは間違いありませんが、本書の大部分では var を使用しないようにしており、普段の Scala プログラミングにおいてもそれに倣いましょう。

メソッド対フィールド

同じ動作をするように見える引数のないメソッドを持つことができるにも関わらず、なぜフィールドが必要なのか不思議に思うかもしれません。その違いはわずかで、フィールドは値に名前を与えますが、一方のメソッドは値を算出する計算に名前を与えます。

その違いを明らかにするオブジェクトがこちらです。

object Test7 {
   val simpleField = {
     println("Evaluating simpleField")
     42
   }
   def noParameterMethod = {
     println("Evaluating noParameterMethod")
     42
   }
}

ここでは、コンソールに何かを印字するために println 式を、式をグループにするためにブロック式({} によって囲まれた式)を使用しています。なお、ブロック式については、次節でより詳しく見ていきます。

オブジェクトを定義したとコンソールに表示されているのに、いずれの println 文も実行されていないことに注意してください。これは遅延読み込み (lazy loading) と呼ばれる Scala と Java の特性によるものです。

オブジェクトやクラス(後述)は、他のコードによって参照されるまで読み込まれません。これが、単純な "Hello world!" アプリを実行するために、Scala が標準ライブラリ全体をメモリに読み込むことを防いでいます。

式の中で Test7 を参照することで、Scala にオブジェクトの本体を強制的に評価させてみましょう。

Test7
// Evaluating simpleField
// res4: Test7.type = Test7$@3a39aa95

オブジェクトが最初に読み込まれるとき、Scala はその定義を実行し、各フィールドの値を計算します。その結果、コードの副作用として "Evaluating simpleField" が印字されます。

フィールドにおける本体の式は一度だけ実行されます。 その後、オブジェクトにその最終的な値が格納されます。下記で println 出力が欠けていることからわかるように、その式は二度と評価されることはありません。

Test7.simpleField
// res5: Int = 42

Test7.simpleField
// res6: Int = 42

一方、下記で println 出力が繰り返されていることからわかるように、メソッドの本体はメソッドを呼び出すたびに評価されます。

Test7.noParameterMethod
// Evaluating noParameterMethod
// res7: Int = 42

Test7.noParameterMethod
// Evaluating noParameterMethod
// res8: Int = 42

覚えておいてほしいこと

本節では、独自のオブジェクトを作成し、メソッドとフィールドを与え、式の中で参照しました。

文法としてオブジェクトの宣言、

object name {
  declarationOrExpression ...
}

メソッドの宣言、

def name(parameter: type, ...): resultType = bodyExpression

そしてフィールドの宣言、

val name = valueExpression
var name = valueExpression

を見てきました。これらすべては宣言で、名前と値を束縛します。宣言は式とは異なります。宣言は値を評価しませんし型も持ちません。

また、メソッドとフィールドの違いを見てきました。フィールドはオブジェクトの中に格納された値を参照し、一方のメソッドは値を算出する計算を参照します。

演習

キャット・オ・マティック

下記の表は、3匹の猫の名前 (name)・色 (colour)・好きな食べ物 (food) を示しています。それぞれの猫についてのオブジェクトを定義してください。(経験豊富なプログラマーの方へ:クラスについてはまだ説明していません。)

名前 (Name) 色 (Colour) 食べ物 (Food)
オズワルド (Oswald) 黒 (Black) ミルク (Milk)
ヘンダーソン (Henderson) 茶トラ (Ginger) カリカリ (Chips)
クエンティン (Quentin) トラ (Tabby and white) カレー (Curry)

解答(クリックして表示)

これはオブジェクトを定義する文法に慣れるための指の運動にすぎません。下記コードのような解答が得られているはずです。

object Oswald {
  val colour: String = "Black"
  val food: String = "Milk"
}

object Henderson {
  val colour: String = "Ginger"
  val food: String = "Chips"
}

object Quentin {
  val colour: String = "Tabby and white"
  val food: String = "Curry"
}

スクウェア・ダンス!

calc と呼ばれるオブジェクトを定義します。それは、square という Double を引数として受け取る、あなたが予想するとおり入力を2乗 (square) するメソッドを伴います。そして、計算の一部として square メソッドの呼び出し を含む cube と呼ばれる、入力を3乗 (cube) するメソッドを追加してください。

解答(クリックして表示)

これが解答です。cube(x)square(x) を呼び出し、その値を x によってもう一度乗算します。各メソッドの結果型はコンパイラーによって Double として推論されます。

object calc {
  def square(x: Double) = x * x
  def cube(x: Double) = x * square(x)
}

精密なスクウェア・ダンス!

前の演習から calc をコピー&ペーストして、Int と同様に Double でも動作するよう一般化された calc2 を作成してください。Java の経験を持っていれば、これはかなり簡単にできるはずです。そうでなければ、下記の解答を読んでください。

解答(クリックして表示)

Java のように Scala は特に IntDouble の間でうまく一般化できるわけではありません。しかしながら、引数の型ごとに square メソッドと cube メソッドを定義することでオーバーロード (overload) することができます。

object calc2 {
  def square(value: Double) = value * value
  def cube(value: Double) = value * square(value)

  def square(value: Int) = value * value
  def cube(value: Int) = value * square(value)
}

「オーバーロードされた」メソッドとは、異なる引数型で複数回定義したメソッドのことです。オーバーロードされたメソッド型を呼び出すたびに、Scala は引数の型を見ることによって、どの変種が必要かを自動的に判断します。

calc2.square(1.0) // `square` の `Double` 版を呼び出す
// res11: Double = 1.0

calc2.square(1)   // `square` の `Int` 版を呼び出す
// res12: Int = 1

Scala コンパイラーは、低い精度から高い精度が必要な場合に、数値型間の自動変換を挿入できます。例えば、calc.square(2) と書けば、コンパイラーは calc.square の唯一のバージョンが Double を受け取ると判断し、本当のところは calc.square(2.toDouble) という意図であると自動的に推論します。

高い精度から低い精度への逆方向の変換は、丸め誤差につながる可能性があるため、自動的には処理されません。例えば、下記のコードは、xInt であり、その本体の式が Double であるため、コンパイルされません!(実際に試してみてください。)

val x: Int = calc.square(2) // コンパイルエラー
// <console>:13: error: type mismatch;
//  found   : Double
//  required: Int
//        val x: Int = calc.square(2) // コンパイルエラー
//                                ^

これは、DoubletoInt メソッドを手動で使用することで回避できます。

val x: Int = calc.square(2).toInt // toInt は切り捨て
// x: Int = 4

:warning: 文字列連結の危険性

Java と似た振る舞いを維持するために、Scala もまた必要に応じて任意のオブジェクトを自動的に String に変換します。これは、println("a" + 1) のようなものを簡単に書けるようにするためで、Scala は println("a" + 1.toString) に自動的に書き換えます。

文字列連結と数値加算が同じ + メソッドを共有しているという事実が、予期せぬバグを引き起こすことがあるので注意しましょう!


評価の順番

コンソールで入力したとき、下記プログラムの出力は何で、最終的な式の型と値は何になるでしょうか?各フィールドやメソッドの型や依存関係、評価の振る舞いをよく考えてみてください。

object argh {
  def a = {
    println("a")
    1
  }

  val b = {
    println("b")
    a + 2
  }

  def c = {
    println("c")
    a
    b + "c"
  }
}
argh.c + argh.b + argh.a

解答(クリックして表示)

これが解答です。

argh.c + argh.b + argh.a
// b
// a
// c
// a
// a
// res13: String = 3c31

評価の完全な順番は下記のとおりです。

- プログラムの最後にあるメインの合計を計算するには、
  - `argh` を読み込み、
    - `argh` のすべてのフィールドを計算するので、
      - `b` を計算すると、
        - `"b"` を印字し、
        - `a + 2` を評価するので、
          - `a` を呼び出し、
            - `"a"` を印字し、
            - `1` を返却し、
          - `1 + 2` を返却し、
        - `b` に値 `3` を格納し、
  - `argh.c` を呼び出し、
    - `"c"` を印字し、
    - `a` を評価し、
      - `"a"` を印字し、
      - `1` を返却するのですが、それは破棄され、
    - `b + "c"` を評価し、
      - `b` から値 `3` を取得し、
        - 値 `"c"` を取得し、
        - `+` を評価し、それが実際のところ文字列の連結を参照していると判断し、
          `3` を `"3"` に変換し、
        - 文字列 `"3c"` を返却し、
  - `argh.b` を呼び出し,
    - `b` から値 `3` を取得し、
  - 最初の `+` を評価し、それが実際のところ文字列の連結を参照していると判断し、
    `"3c3"` を生成し、
  - `argh.a` を呼び出し、
    - `"a"` を印字し、
    - `1` を返却し、
  - 最初の `+` を評価し、それが実際のところ文字列の連結を参照していると判断し、
    `"3c31"` を生成する

ふぅ、こんな簡単なコードにしてはたくさんありますね。

やぁ、人間

person というオブジェクトを定義します。それは、firstNamelastName というフィールドを含みます。alien という2番目のオブジェクトを定義します。それは、引数として person を受け取り、その firstName を使用して挨拶する greet というメソッドを含みます。

greet メソッドの型は何でしょうか?このメソッドを使用して他のオブジェクトに挨拶することはできますか?

解答(クリックして表示)
object person {
  val firstName = "Dave"
  val lastName = "Gurnell"
}

object alien {
  def greet(p: person.type) =
    "Greetings, " + p.firstName + " " + p.lastName
}
alien.greet(person)
// res15: String = Greetings, Dave Gurnell

greet の引数 p の型 person.type に注目してください。これは、先ほど言及したシングルトン型 (singleton type) のひとつです。この場合は person オブジェクトに固有の型なので、他のオブジェクトで greet を使用することはできません。これは、Scala のすべての整数で共有されている Int のような型とは大きく異なります。

これでは、Scala でプログラムを書く能力に大きな制限を課してしまいます。組み込み型か独自に作成した単一のオブジェクトで動作するメソッドしか書けないのです。有用なプログラムを構築するために、独自の型を定義し、それぞれの型で多様な値を生成する機能が必要です。クラス (class) を使用してこれを実現できるのですが、それは次節で扱います。

メソッドの真価

メソッドは値ですか?式ですか?なぜそうなるのでしょうか?

解答(クリックして表示)

まず、メソッドと式の等価性を扱いましょう。すでに知っているように、式は値を生成するプログラムの断片です。何かが式であるかどうかの簡単なテストは、それをフィールドに代入できるかどうかを確認することです。

object calculator {
  def square(x: Int) = x * x
}
val someField = calculator.square
// <console>:15: error: missing argument list for method square in object calculator
// Unapplied methods are only converted to functions when a function type is expected.
// You can make this conversion explicit by writing `square _` or `square(_)` instead of `square`.
//        val someField = calculator.square
//                                   ^

このエラーメッセージをまだ完全に理解はできません(「部分適用関数 (partially applied function)」については後ほど学びます。)が、square式ではないことを示しています。しかしながら、square呼び出しは値を生成するのです。

val someField = calculator.square(2)
// someField: Int = 4

引数を持たないメソッドは異なる振る舞いをしているように見えます。しかしながら、これは文法のトリックです。

object clock {
  def time = System.currentTimeMillis
}
val now = clock.time
// now: Long = 1586352189787

clock.time を値として now に割り当てているように見えるにも関わらず、実際にそれは clock.time の呼び出しによって返された値を代入しています。これは、もう一度メソッドを呼び出すことで実証できます。

val aBitLaterThanNow = clock.time
// aBitLaterThanNow: Long = 1586352189933

上で見たように、Scala では「フィールドへの参照」と「引数のないメソッド呼び出し」は同じように見えます。これは、他のコードに影響を与えることなく、フィールドの実装をメソッドに置き換えることができる(逆も然り)ように設計されています。それは、統一アクセス原理 (uniform access principle) と呼ばれるプログラミング言語の機能です。

つまり、要約すると、メソッド呼び出しは式であるメソッド自体は式ではないということです。Scala は関数 (function) と呼ばれる概念を持っており、それはメソッドのように呼び出せるオブジェクトです。オブジェクトが値であることをすでに知っているように、関数は値であり、データとして取り扱うことができます。お気付きのとおり、関数は関数型プログラミング (functional programming) の重要な部分であり、Scala の大きな強みのひとつです。関数と関数型プログラミングについては少しずつ学んでいきましょう。

メソッドの書き方

前節では、メソッドの文法について見てきました。本書の主な目的のひとつは、文法を超えて、Scala プログラムを構築するための体系的な方法を提供することです。本節はそのような問題を扱う最初の節です。本節では、体系的にメソッドを構築する方法を見ていきます。Scala の経験を積んでいくうちに、この方法のいくつかの段階を省略することができますが、本書の中ではこの方法に従うことを強く推奨します。

アドバイスを具体的にするために、前節の演習を例として使用していきましょう。

calc と呼ばれるオブジェクトを定義します。それは、square という Double を引数として受け取る、あなたが予想するとおり入力を2乗 (square) するメソッドを伴います。そして、計算の一部として square メソッドの呼び出しを含む cube と呼ばれる、入力を3乗 (cube) するメソッドを追加してください。

入力と出力を特定する

最初の段階は、入力となる引数がもしあれば、その型とメソッドの結果型を特定することです。

多くの場合、演習では型を教えてくれるので、説明文からそのまま読み取ることができます。上の例で、入力型は Double になっています。結果型もまた Double になることが推論できます。

テストケースを準備する

型だけでは物語のすべてを語れません。Double から Double への関数はたくさんありますが、2乗を実行しているものは少数です。そのため、メソッドの期待される振る舞いを例証するいくつかのテストケースを準備しましょう。

外部依存を避けたいので、本書ではテストライブラリを使用しないことにします。Scala が提供する assert 関数を使用することで手軽なテストライブラリを実装できます。square の例については、下記のようなテストケースが考えられます。

assert(square(2.0) == 4.0)
assert(square(3.0) == 9.0)
assert(square(-2.0) == 4.0)

宣言を書く

型とテストケースが準備できたので、メソッド宣言を書くことができます。その本体はまだ書けないので、その場所に Scala の便利な機能である ??? を使用しておきます。

def square(in: Double): Double =
  ???

この段階は、前の段階で収集した情報から機械的に与えられるべきです。

コードを実行する

コードを実行し、それがコンパイルされていること(つまり、タイプミスをしていないこと)と、また、テストが失敗すること(つまり、何らかのテストが実行されていること)を確認してください。なお、メソッド宣言の後にテストを配置する必要があるかもしれません。

本体を書く

メソッドの本体を書く準備が整いました。本書を通じて、いくつかのテクニックを修得していきます。現時点では、2つのテクニックを見ていきましょう。

結果型を考える

最初のテクニックは、結果型を見ることです。この場合は Double です。どうやって Double の値を生成するのでしょうか?リテラルを書くこともできますが、この場合は明らかに正しくありません。Double を生成する他の方法は、何らかのオブジェクトのメソッドを呼び出すことで、それは次のテクニックがもたらします。

入力型を考える

次のテクニックは、メソッドの入力引数の型を見ることです。この場合は Double です。Double を生成する必要があることは確立していますが、入力から Double を生成するためにはどのようなメソッドを呼び出せばいいのでしょうか?そのようなメソッドはたくさんありますが、ここで呼び出すべき正しいメソッドとして * を選択するためにはドメイン知識を活用しなければなりません。

完全なメソッドは下記のように書くことができます。

def square(in: Double): Double =
  in * in

コードを再実行する

最後に、コードを再実行して、テストがすべて通過することを確認します。

これはとても単純な例でしたが、今のプロセスを実践することで、今後遭遇するであろうより複雑な例にうまく対応できるようになります。


:information_source: メソッドを書く方法

体系的にメソッドを書くための6段階の方法があります。

  1. メソッドの入力型と出力型を特定する。
  2. 入力例を与えられたメソッドの期待される出力について、いくつかのテストケースを書く。これらのケースを書き出すために assert 関数を使用することができる。
  3. 下記のようにメソッドの本体に ??? を使用してメソッドの宣言を書く。
def name(parameter: type, ...): resultType =
 ???
  1. コードを実行し、テストケースが実際に失敗することを確認する。
  2. メソッドの本体を書く。現在のところ、ここで2つのテクニックを適用することができる。
    • 結果型とそのインスタンスをどのように生成できるかを考える
    • 入力型とそれを結果型に変化させるために呼び出せるメソッドを考える
  3. コードを再実行し、テストケースが通過することを確認する。

複合式

これで Scala の基本的な入門編はほぼ終了しました。本節では、より複雑なプログラムで必要になるであろう、条件式 (conditional)ブロック (block) という2種類の特別な式について見ていきます。

条件式

条件式は、ある条件にもとづいて評価する式を選択することを可能にします。例えば、2つの数値のうちどちらが小さいかにもとづいて文字列を選択することができます。

if(1 < 2) "Yes" else "No"
// res0: String = Yes

:information_source: 条件式は式である

Scala における if の記述は Java と同じ構文を持ちます。重要な違いのひとつは、Scala の条件式は式であるということで、それは型と返却値を持ちます。


選択されない式は評価されません。これは、副作用を伴う式を使用すると明らかです。

if(1 < 2) println("Yes") else println("No")
// Yes

No がコンソールに出力されないので、println("No") という式は評価されていないことがわかります。


:information_source: 条件式文法

条件式のための文法は、

if(condition)
  trueExpression
else
  falseExpression

です。ここで、

  • conditionBoolean 型の式
  • trueExpressionconditiontrue に評価されるときに評価される式
  • falseExpressionconditionfalse に評価されるときに評価される式

とします。


ブロック

ブロックは、計算をまとめて並べることができる式のことです。それは、セミコロンもしくは改行によって区切られた部分式を含む中括弧のペアとして書かれます。

{ 1; 2; 3 }
// <console>:13: warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses
//        { 1; 2; 3 }
//          ^
// <console>:13: warning: a pure expression does nothing in statement position; you may be omitting necessary parentheses
//        { 1; 2; 3 }
//             ^
// error: No warnings can be incurred under -Xfatal-warnings.

ご覧のように、このコードを実行すると、コンソールはいくつかの警告を発生させ、Int3 を返します。

ブロックは、中括弧によって囲まれた一連の式や宣言のことです。ブロックもまた式であり、各部分式を順番に実行し、最後の式の値を返します。

12 の値を捨ててしまうなら、なぜそれらを実行するのでしょうか?これはいい質問で、上記で Scala コンパイラーが警告を発生させた理由にもなります。

ブロックを使用するひとつの理由は、最終的な値を計算する前に副作用を生成するコードを使用したいからです。

{
  println("This is a side-effect")
  println("This is a side-effect as well")
  3
}
// This is a side-effect
// This is a side-effect as well
// res3: Int = 3

また、下記のように中間結果に名前をつけたいときにブロックを使用することもできます。

def name: String = {
  val title = "Professor"
  val name = "Funkenstein"
  title + " " + name
}
name
// res4: String = Professor Funkenstein

:information_source: ブロック式文法

ブロック式の文法は、

{
   declarationOrExpression ...
   expression
}

です。ここで、

  • declarationOrExpression は宣言か式(オプション)
  • expression はブロック式の型と値を決定する式

とします。


覚えておいてほしいこと

条件式は Boolean の条件にもとづいて評価する式を選択することを可能にします。その文法は下記のとおりです。

if(condition)
  trueExpression
else
  falseExpression

式である条件式は、型を持ち、オブジェクトに評価されます。

ブロックは、式や宣言を順番に並べることを可能にします。それは、副作用を伴う式を順番に並べたり、計算における中間結果に名前をつけたりしたいときによく使用されます。その文法は下記のとおりです。

{
   declarationOrExpression ...
   expression
}

ブロックの型と値は、ブロックにおける最後の式になります。

演習

模範的なライバル関係

下記の条件式における型と値は何ですか?

if(1 > 2) "alien" else "predator"

解答(クリックして表示)

それは、値 "predator" を伴う String です。明らかにプレデターは最高です。
if(1 > 2) "alien" else "predator"
// res6: String = predator

型は then 式と else 式における型の上限境界によって決まります。この場合、両式は String なので、その結果もまた String になります。

値は実行時に決まります。21 より大きいので、条件式は else 式の値を評価します。

あまり知られていないライバル関係

この条件式はどうでしょうか?

if(1 > 2) "alien" else 2001

解答(クリックして表示)

それは値 2001 を伴う Any です。
if(1 > 2) "alien" else 2001
// res8: Any = 2001

これは前の演習と似ていますが、その違いは結果型です。先ほど、型は真偽両式の上限境界 (upper bound) であることを見ました。"alien"2001 は全く異なる型なので、最も近い共通の祖先は、すべての Scala 型における最高位の基底型 Any になります。

これは、型がプログラムを実行する前のコンパイル時に決まるという重要な観測結果です。コンパイラーはプログラムを実行する前に 12 のどちらが大きいかを知らないので、条件式の結果型から最良の推量をするしかないのです。前の演習では String に至るまでの道のりを得ることができましたが、このプログラムでは Any が限りなく近いものになっています。

Any については以降の節で詳しく説明します。なお、それは IntBoolean のような値型を包含するので、Java プログラマーは Object と混同してはいけません。

else のない if

この条件式はどうでしょうか?

if(false) "hello"

解答(クリックして表示)

結果の型と値はそれぞれ Any() です。
if(false) "hello"
// res10: Any = ()

すべてのコードが等しくあるべきですが、else 式のない条件式の半数のみ値に評価されます。Scala は、else への分岐が評価されるべき場合に Unit 値を返すことでこの問題を回避しています。これらの式はたいてい副作用がある場合にのみ使用します。

まとめ

本章では、Scala の基礎をとても簡単に紹介しました。

  • 値に評価される式
  • 値に名前を与える宣言

たくさんのオブジェクトについてリテラルで書く方法や、既存のオブジェクトから新しいオブジェクトを生成するためにメソッド呼び出しや複合式を使用する方法を見てきました。

また、独自のオブジェクトを宣言し、メソッドやフィールドを構築しました。

次章では、新しい種類の宣言であるクラスが、どのようにオブジェクトを生成するためのテンプレートを提供するのかを見ていきます。クラスは、コードを再利用したり、共通の型で類似のオブジェクトを統一したりすることを可能にします。


  1. これは完全に正しくはありません。Scala コードを実行するプログラムである Java 仮想マシンは2種類のオブジェクトを識別します。プリミティブ型は、値の表現と一緒にどんな型の情報も保持しません。オブジェクト型は型の情報を保持します。しかし、この型の情報は完全ではなく失われる場合もあります。それゆえに、コンパイル時と実行時の間にある区別を曖昧にすることは危険です。実行時にある型の情報を頼りにしない(本書ではそういうパターンを明らかにしていきます。)のであれば、それらの問題に遭遇することはないでしょう。 

  2. パターンマッチングと呼ばれるオブジェクトを作用させる別の方法があります。パターンマッチングはのちほど紹介します。 

  3. 副作用とは正確には何でしょうか?ひとつの実用的な定義は、間違った結果を置換によって生じさせてしまう何かのことです。副作用がなければ、置換は必ず機能するのでしょうか?Scala の本当に正しいモデルを示すには、置換を適用する順番を定義する必要があります。いくつかの考えうる順番があります。(例えば、置換を左から右へ実行するのか、右から左へ実行するのか?置換をできるだけ早くするのか、値が必要になるまで遅延するのか?)ほとんどいつも置換の順番は問題になりませんが、それが問題になる場合もあります。Scala はいつも「左から右へ」「できるだけ早く」置換を適用します。 

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0