Einführung in Scala

Einführung in Scala

1. Einführung

In diesem Tutorial werden wir unsScala ansehen - eine der Hauptsprachen, die auf der Java Virtual Machine ausgeführt werden.

Wir beginnen mit den wichtigsten Sprachfunktionen wie Werten, Variablen, Methoden und Kontrollstrukturen. Anschließend werden einige erweiterte Funktionen wie Funktionen höherer Ordnung, Currying, Klassen, Objekte und Mustervergleich untersucht.

Um einen Überblick über die JVM-Sprachen zu erhalten, lesen Sie unserequick guide to the JVM Languages

2. Projektaufbau

In diesem Tutorial verwenden wir die Standard-Scala-Installation vonhttps://www.scala-lang.org/download/.

Fügen wir zunächst die Abhängigkeit der Scala-Bibliothek zu unserer pom.xml hinzu. Dieses Artefakt enthält die Standardbibliothek für die Sprache:


    org.scala-lang
    scala-library
    2.12.7

Zweitens fügen wir diescala-maven-plugin zum Kompilieren, Testen, Ausführen und Dokumentieren des Codes hinzu:


    net.alchim31.maven
    scala-maven-plugin
    3.3.2
    
        
            
                compile
                testCompile
            
        
    

Maven hat die neuesten Artefakte fürscala-lang undscala-maven-plugin.

Schließlich verwenden wir JUnit für Unit-Tests.

3. Grundfunktionen

In diesem Abschnitt werden die grundlegenden Sprachfunktionen anhand von Beispielen untersucht. Wir werden dieScala interpreter für diesen Zweck verwenden.

3.1. Dolmetscher

Der Interpreter ist eine interaktive Shell zum Schreiben von Programmen und Ausdrücken.

Drucken wir "Hallo Welt" damit:

C:\>scala
Welcome to Scala 2.12.6 (Java HotSpot(TM)
 64-Bit Server VM, Java 1.8.0_92).
Type in expressions for evaluation.
Or try :help.

scala> print("Hello World!")
Hello World!
scala>

Oben starten wir den Interpreter, indem wir in der Befehlszeile 'scala' eingeben. Der Interpreter wird gestartet und zeigt eine Begrüßungsnachricht gefolgt von einer Eingabeaufforderung an.

Anschließend geben wir an dieser Eingabeaufforderung unseren Ausdruck ein. Der Interpreter liest den Ausdruck, wertet ihn aus und gibt das Ergebnis aus. Anschließend wird eine Schleife ausgeführt und die Eingabeaufforderung erneut angezeigt.

Der Dolmetscher bietet sofortiges Feedback und ist der einfachste Einstieg in die Sprache. Lassen Sie uns daher die grundlegenden Sprachfunktionen untersuchen: Ausdrücke und verschiedene Definitionen.

3.2. Ausdrücke

Any computable statement is an expression.

Schreiben wir einige Ausdrücke und sehen wir ihre Ergebnisse:

scala> 123 + 321
res0: Int = 444

scala> 7 * 6
res1: Int = 42

scala> "Hello, " + "World"
res2: String = Hello, World

scala> "zipZAP" * 3
res3: String = zipZAPzipZAPzipZAP

scala> if (11 % 2 == 0) "even" else "odd"
res4: String = odd

Wie wir oben sehen können,every expression has a value and a type.

If an expression does not have anything to return, it returns a value of type Unit. Dieser Typ hat nur einen Wert:(). Es ähnelt dem Schlüsselwortvoidin Java.

3.3. Wertedefinition

Das Schlüsselwortval wird verwendet, um Werte zu deklarieren.

Wir verwenden es, um das Ergebnis eines Ausdrucks zu benennen:

scala> val pi:Double = 3.14
pi: Double = 3.14

scala> print(pi)
3.14

Auf diese Weise können wir das Ergebnis mehrmals wiederverwenden.

Values are immutable. Daher können wir sie nicht neu zuweisen:

scala> pi = 3.1415
:12: error: reassignment to val
       pi = 3.1415
         ^

3.4. Variablendefinition

Wenn wir einen Wert neu zuweisen müssen, deklarieren wir ihn stattdessen als Variable.

Das Schlüsselwortvar is wird zum Deklarieren von Variablen verwendet:

scala> var radius:Int=3
radius: Int = 3

3.5. Methodendefinition

Wir definieren Methoden mit dem Schlüsselwortdef. Nach dem Schlüsselwort geben wir den Methodennamen, die Parameterdeklarationen, ein Trennzeichen (Doppelpunkt) und den Rückgabetyp an. Danach geben wir ein Trennzeichen (=) gefolgt vom Methodenkörper an.

Im Gegensatz zu Java verwenden wir nicht das Schlüsselwortreturn, um das Ergebnis zurückzugeben. Eine Methode gibt den Wert des zuletzt ausgewerteten Ausdrucks zurück.

Schreiben wir eine Methodeavg, um den Durchschnitt zweier Zahlen zu berechnen:

scala> def avg(x:Double, y:Double):Double = {
  (x + y) / 2
}
avg: (x: Double, y: Double)Double

Rufen wir dann diese Methode auf:

scala> avg(10,20)
res0: Double = 12.5

Wenn eine Methode keine Parameter akzeptiert, können die Klammern während der Definition und des Aufrufs weggelassen werden. Außerdem können wir die Klammern weglassen, wenn der Körper nur einen Ausdruck hat.

Schreiben wir eine parameterlose MethodecoinToss , die zufällig "Head" oder "Tail" zurückgibt:

scala> def coinToss =  if (Math.random > 0.5) "Head" else "Tail"
coinToss: String

Rufen wir als Nächstes diese Methode auf:

scala> println(coinToss)
Tail
scala> println(coinToss)
Head

4. Kontrollstrukturen

Kontrollstrukturen erlauben es uns, den Kontrollfluss in einem Programm zu verändern. Wir haben folgende Kontrollstrukturen:

  • If-else-Ausdruck

  • While-Schleife und Do While-Schleife

  • Zum Ausdruck bringen

  • Versuchen Sie es mit Ausdruck

  • Übereinstimmungsausdruck

Im Gegensatz zu Java haben wir keine Schlüsselwörter fürcontinue oderbreak. Wir haben das Schlüsselwortreturn. Wir sollten es jedoch vermeiden.

Anstelle derswitch-Anweisung haben wir Pattern Matching über Match-Ausdruck. Zusätzlich können wir unsere eigenen Kontrollabstraktionen definieren.

4.1. ansonsten

Dieif-else expression ähnelt Java. Der Teilelseist optional. Wir können mehrere if-else-Ausdrücke verschachteln.

Since it is an expression, it returns a value. Daher verwenden wir es ähnlich wie den ternären Operator (? :) in Java. Tatsächlich ist die Sprachedoes not have have the ternary operator.

Schreiben wir mit if-else eine Methode, um den größten gemeinsamen Teiler zu berechnen:

def gcd(x: Int, y: Int): Int = {
  if (y == 0) x else gcd(y, x % y)
}

Schreiben wir dann einen Komponententest für diese Methode:

@Test
def whenGcdCalledWith15and27_then3 = {
  assertEquals(3, gcd(15, 27))
}

4.2. While-Schleife

Die while-Schleife hat eine Bedingung und einen Körper. Der Body wird wiederholt in einer Schleife ausgewertet, während die Bedingung wahr ist. Die Bedingung wird zu Beginn jeder Iteration ausgewertet.

Da es nichts Nützliches zum Zurückgeben gibt, gibt esUnit. zurück

Verwenden wir die while-Schleife, um eine Methode zum Berechnen des größten gemeinsamen Divisors zu schreiben:

def gcdIter(x: Int, y: Int): Int = {
  var a = x
  var b = y
  while (b > 0) {
    a = a % b
    val t = a
    a = b
    b = t
  }
  a
}

Überprüfen wir dann das Ergebnis:

assertEquals(3, gcdIter(15, 27))

4.3. Do While-Schleife

Die do while-Schleife ähnelt der while-Schleife, außer dass die Schleifenbedingung am Ende der Schleife ausgewertet wird.

Schreiben wir mithilfe der do-while-Schleife eine Methode zur Berechnung der Fakultät:

def factorial(a: Int): Int = {
  var result = 1
  var i = 1
  do {
    result *= i
    i = i + 1
  } while (i <= a)
  result
}

Als nächstes überprüfen wir das Ergebnis:

assertEquals(720, factorial(6))

4.4. Für den Ausdruck

Der for-Ausdruck ist viel vielseitiger als die for-Schleife in Java.

Es kann über einzelne oder mehrere Sammlungen iterieren. Darüber hinaus können Elemente herausgefiltert und neue Kollektionen erstellt werden.

Schreiben Sie mit dem for-Ausdruck eine Methode, um einen Bereich von Ganzzahlen zu summieren:

def rangeSum(a: Int, b: Int) = {
  var sum = 0
  for (i <- a to b) {
    sum += i
  }
  sum
}

Hier ista to b ein Generatorausdruck. Es wird eine Reihe von Werten vona bisb generiert.

i ← a to b ist ein Generator. Es definiertasval und weist ihm die vom Generatorausdruck erzeugte Werteserie zu.

Der Body wird für jeden Wert in der Reihe ausgeführt.

Als nächstes überprüfen wir das Ergebnis:

assertEquals(55, rangeSum(1, 10))

5. Funktionen

Scala ist eine funktionale Sprache. Funktionen sind hier erstklassige Werte - wir können sie wie jeden anderen Werttyp verwenden.

In diesem Abschnitt werden einige erweiterte Konzepte in Bezug auf Funktionen vorgestellt - lokale Funktionen, Funktionen höherer Ordnung, anonyme Funktionen und Currying.

5.1. Lokale Funktionen

We can define functions inside functions. Sie werden als verschachtelte Funktionen oder lokale Funktionen bezeichnet. Ähnlich wie bei den lokalen Variablenthey are visible only within the function they are defined in.

Schreiben wir nun eine Methode zum Berechnen der Leistung mithilfe einer verschachtelten Funktion:

def power(x: Int, y:Int): Int = {
  def powNested(i: Int,
                accumulator: Int): Int = {
    if (i <= 0) accumulator
    else powNested(i - 1, x * accumulator)
  }
  powNested(y, 1)
}

Als nächstes überprüfen wir das Ergebnis:

assertEquals(8, power(2, 3))

5.2. Funktionen höherer Ordnung

Da Funktionen Werte sind, können sie als Parameter an eine andere Funktion übergeben werden. Wir können auch eine Funktion eine andere Funktion zurückgeben lassen.

We refer to functions which operate on functions as higher-order functions. Sie ermöglichen es uns, auf einer abstrakteren Ebene zu arbeiten. Mit ihnen können wir die Codeduplizierung reduzieren, indem wir generalisierte Algorithmen schreiben.

Schreiben wir nun eine Funktion höherer Ordnung, um eine Zuordnung durchzuführen und die Operation über einen Bereich von Ganzzahlen zu reduzieren:

def mapReduce(r: (Int, Int) => Int,
              i: Int,
              m: Int => Int,
              a: Int, b: Int) = {
  def iter(a: Int, result: Int): Int = {
    if (a > b) {
      result
    } else {
      iter(a + 1, r(m(a), result))
    }
  }
  iter(a, i)
}

Hier sindr undm Parameter vom TypFunction. Durch Übergabe verschiedener Funktionen können wir eine Reihe von Problemen lösen, z. B. die Summe der Quadrate oder Würfel und die Fakultät.

Verwenden Sie als Nächstes diese Funktion, um eine weitere FunktionsumSquares zu schreiben, die die Quadrate von Ganzzahlen summiert:

@Test
def whenCalledWithSumAndSquare_thenCorrectValue = {
  def square(x: Int) = x * x
  def sum(x: Int, y: Int) = x + y

  def sumSquares(a: Int, b: Int) =
    mapReduce(sum, 0, square, a, b)

  assertEquals(385, sumSquares(1, 10))
}

Oben sehen wir, dasshigher-order functions tend to create many small single-use functions. We can avoid naming them by using anonymous functions.

5.3. Anonyme Funktionen

Eine anonyme Funktion ist ein Ausdruck, der zu einer Funktion ausgewertet wird. Es ähnelt dem Lambda-Ausdruck in Java.

Schreiben wir das vorherige Beispiel mit anonymen Funktionen neu:

@Test
def whenCalledWithAnonymousFunctions_thenCorrectValue = {
  def sumSquares(a: Int, b: Int) =
    mapReduce((x, y) => x + y, 0, x => x * x, a, b)
  assertEquals(385, sumSquares(1, 10))
}

In diesem Beispiel empfängtmapReduce zwei anonyme Funktionen:(x, y) ⇒ x + y undx ⇒ x * x.

Scala can deduce the parameter types from context. Daher lassen wir die Art der Parameter in diesen Funktionen weg.

Dies führt zu einem präziseren Code als im vorherigen Beispiel.

5.4. Currying Funktionen

A curried function takes multiple argument lists, such as def f(x: Int) (y: Int). Es wird angewendet, indem mehrere Argumentlisten übergeben werden, wie in f (5) (6).

It is evaluated as an invocation of a chain of functions. Diese Zwischenfunktionen verwenden ein einzelnes Argument und geben eine Funktion zurück.

Wir können auch teilweise Argumentlisten wief(5) angeben.

Lassen Sie uns dies anhand eines Beispiels verstehen:

@Test
def whenSumModCalledWith6And10_then10 = {
  // a curried function
  def sum(f : Int => Int)(a : Int, b : Int) : Int =
    if (a > b) 0 else f(a) + sum(f)(a + 1, b)

  // another curried function
  def mod(n : Int)(x : Int) = x % n

  // application of a curried function
  assertEquals(1, mod(5)(6))

  // partial application of curried function
  // trailing underscore is required to
  // make function type explicit
  val sumMod5 = sum(mod(5)) _

  assertEquals(10, sumMod5(6, 10))
}

Oben nehmensum undmod jeweils zwei Argumentlisten. Wir übergeben die beiden Argumentlisten wiemod(5)(6). Dies wird als zwei Funktionsaufrufe gewertet. Zunächst wirdmod(5) ausgewertet, was eine Funktion zurückgibt. Dies wird wiederum mit dem Argument6. aufgerufen. Als Ergebnis erhalten wir 1.

Es ist möglich, die Parameter teilweise wie inmod(5). anzuwenden. Wir erhalten als Ergebnis eine Funktion.

In ähnlicher Weise übergeben wir im Ausdrucksum(mod(5)) _ nur das erste Argument an die Funktion vonsum. Daher istsumMod5 eine Funktion.

Der Unterstrich wird als Platzhalter für nicht angewendete Argumente verwendet. Da der Compiler nicht darauf schließen kann, dass ein Funktionstyp erwartet wird, verwenden wir den nachgestellten Unterstrich, um den Funktionsrückgabetyp explizit zu machen.

5.5. By-Name-Parameter

Eine Funktion kann Parameter auf zwei verschiedene Arten anwenden: nach Wert und nach Name. Sie wertet Argumente nach Wert nur einmal zum Zeitpunkt des Aufrufs aus. Im Gegensatz dazu werden bei jedem Verweis Argumente nach Namen ausgewertet. Wenn das Argument by-name nicht verwendet wird, wird es nicht ausgewertet.

Scala verwendet standardmäßig By-Value-Parameter. Wenn dem Parametertyp ein Pfeil (⇒) vorangestellt ist, wechselt er zum Parameter by-name.

Verwenden wir es nun, um die while-Schleife zu implementieren:

def whileLoop(condition: => Boolean)(body: => Unit): Unit =
  if (condition) {
    body
    whileLoop(condition)(body)
  }

Damit die obige Funktion korrekt funktioniert, sollten beide Parametercondition undbody jedes Mal ausgewertet werden, wenn sie referenziert werden. Deshalb definieren wir sie als By-Name-Parameter.

6. Klassendefinition

Wir definieren eine Klasse mit dem Schlüsselwortclassgefolgt vom Namen der Klasse.

Nach dem Namenwe can specify primary constructor parameters. Doing so automatically adds members with the same name to the class.

Im Klassenkörper definieren wir die Mitglieder - Werte, Variablen, Methoden usw. They are public by default unless modified by the private or protected access modifiers.

Wir müssen das Schlüsselwortoverrideverwenden, um eine Methode aus der Oberklasse zu überschreiben.

Definieren wir eine Klasse Mitarbeiter:

class Employee(val name : String, var salary : Int, annualIncrement : Int = 20) {
  def incrementSalary() : Unit = {
    salary += annualIncrement
  }

  override def toString =
    s"Employee(name=$name, salary=$salary)"
}

Hier geben wir drei Konstruktorparameter an -name,salary undannualIncrement.

Da wirname undsalary mit den Schlüsselwörternval undvardeklarieren, sind die entsprechenden Mitglieder öffentlich. Andererseits verwenden wir nicht das Schlüsselwortval odervar für den ParameterannualIncrement. Daher ist das entsprechende Mitglied privat. Da wir einen Standardwert für diesen Parameter angeben, können wir diesen beim Aufrufen des Konstruktors weglassen.

Zusätzlich zu den Feldern definieren wir die MethodeincrementSalary. Diese Methode ist öffentlich.

Als nächstes schreiben wir einen Komponententest für diese Klasse:

@Test
def whenSalaryIncremented_thenCorrectSalary = {
  val employee = new Employee("John Doe", 1000)
  employee.incrementSalary()
  assertEquals(1020, employee.salary)
}

6.1. Abstrakte Klasse

Wir verwenden das Schlüsselwortabstract, um eine Klasse abstrakt zu machen. Es ist ähnlich wie in Java. Es kann alle Mitglieder haben, die eine reguläre Klasse haben kann.

Darüber hinaus kann es abstrakte Mitglieder enthalten. Dies sind Mitglieder mit reiner Deklaration und keiner Definition, deren Definition in der Unterklasse angegeben ist.

Ähnlich wie in Java können wir keine Instanz einer abstrakten Klasse erstellen.

Lassen Sie uns nun die abstrakte Klasse anhand eines Beispiels veranschaulichen.

Erstellen wir zunächst eine abstrakte KlasseIntSet, um die Menge der Ganzzahlen darzustellen:

abstract class IntSet {
  // add an element to the set
  def incl(x: Int): IntSet

  // whether an element belongs to the set
  def contains(x: Int): Boolean
}

Als nächstes erstellen wir eine konkrete UnterklasseEmptyIntSet, um die leere Menge darzustellen:

class EmptyIntSet extends IntSet {
  def contains(x : Int) = false
  def incl(x : Int) =
  new NonEmptyIntSet(x, this)
}

Dann repräsentiert eine andere UnterklasseNonEmptyIntSet die nicht leeren Mengen:

class NonEmptyIntSet(val head : Int, val tail : IntSet)
  extends IntSet {

  def contains(x : Int) =
    head == x || (tail contains x)

  def incl(x : Int) =
    if (this contains x) {
      this
    } else {
      new NonEmptyIntSet(x, this)
    }
}

Zum Schluss schreiben wir einen Unit-Test fürNonEmptySet:

@Test
def givenSetOf1To10_whenContains11Called_thenFalse = {
  // Set up a set containing integers 1 to 10.
  val set1To10 = Range(1, 10)
    .foldLeft(new EmptyIntSet() : IntSet) {
        (x, y) => x incl y
    }

  assertFalse(set1To10 contains 11)
}

6.2. Züge

Merkmale entsprechen Java-Schnittstellen mit folgenden Unterschieden:

  • in der Lage, von einer Klasse zu verlängern

  • kann auf Mitglieder der Oberklasse zugreifen

  • Kann Initialisierungsanweisungen haben

Wir definieren sie so, wie wir Klassen definieren, aber das Schlüsselworttraitverwenden. Außerdem können sie mit Ausnahme der Konstruktorparameter dieselben Member wie abstrakte Klassen haben. Außerdem sollen sie als Mixin zu einer anderen Klasse hinzugefügt werden.

Lassen Sie uns nun die Merkmale anhand eines Beispiels veranschaulichen.

Definieren wir zunächst ein MerkmalUpperCasePrinter, um sicherzustellen, dass die MethodetoString einen Wert in Großbuchstaben zurückgibt:

trait UpperCasePrinter {
  override def toString =
    super.toString toUpperCase
}

Testen wir dieses Merkmal dann, indem wir es einerEmployee-Klasse hinzufügen:

@Test
def givenEmployeeWithTrait_whenToStringCalled_thenUpper = {
  val employee = new Employee("John Doe", 10) with UpperCasePrinter
  assertEquals("EMPLOYEE(NAME=JOHN DOE, SALARY=10)", employee.toString)
}

Klassen, Objekte und Merkmale können höchstens eine Klasse, aber eine beliebige Anzahl von Merkmalen erben.

7. Objektdefinition

Objekte sind Instanzen einer Klasse. Wie wir in den vorherigen Beispielen gesehen haben, erstellen wir Objekte aus einer Klasse mit dem Schlüsselwortnew.

Wenn eine Klasse jedoch nur eine Instanz haben kann, müssen wir die Erstellung mehrerer Instanzen verhindern. In Java verwenden wir das Singleton-Muster, um dies zu erreichen.

Für solche Fälle haben wir eine kurze Syntax namens Objektdefinition - ähnlich der Klassendefinition mit einem Unterschied. Instead of using the class keyword, we use the object keyword. Auf diese Weise wird eine Klasse definiert und träge ihre einzige Instanz erstellt.

Wir verwenden Objektdefinitionen, um Dienstprogrammmethoden und Singletons zu implementieren.

Definieren wir das Objekt einesUtils:

object Utils {
  def average(x: Double, y: Double) =
    (x + y) / 2
}

Hier definieren wir die KlasseUtils und erstellen auch ihre einzige Instanz.

We refer to this sole instance using its nameUtils. Diese Instanz wird beim ersten Zugriff erstellt.

Wir können keine weitere Instanz von Utils mit dem Schlüsselwortnewerstellen.

Schreiben wir nun einen Komponententest für das ObjektUtils:

assertEquals(15.0, Utils.average(10, 20), 1e-5)

7.1. Companion-Objekt und Companion-Klasse

Wenn eine Klasse und eine Objektdefinition denselben Namen haben, nennen wir sie Begleitklasse bzw. Begleitobjekt. Wir müssen beide in derselben Datei definieren. Companion-Objekte können über ihre Companion-Klasse auf private Mitglieder zugreifen und umgekehrt.

Im Gegensatz zu Java verwendenwe do not have static members. Stattdessen verwenden wir Begleitobjekte, um statische Elemente zu implementieren.

8. Mustervergleich

Pattern matching matches an expression to a sequence of alternatives. Jedes davon beginnt mit dem Schlüsselwortcase. Darauf folgen ein Muster, ein Trennpfeil (⇒) und eine Reihe von Ausdrücken. Der Ausdruck wird ausgewertet, wenn das Muster übereinstimmt.

Wir können Muster bauen aus:

  • case class Konstruktoren

  • variables Muster

  • das Wildcard-Muster _

  • Literale

  • konstante Bezeichner

Fallklassen erleichtern den Musterabgleich für Objekte. Wir fügen das Schlüsselwortcasehinzu, während wir eine Klasse definieren, um sie zu einer Fallklasse zu machen.

Daher ist der Mustervergleich viel leistungsfähiger als die switch-Anweisung in Java. Aus diesem Grund ist es eine weit verbreitete Sprachfunktion.

Schreiben wir nun die Fibonacci-Methode mit Pattern Matching:

def fibonacci(n:Int) : Int = n match {
  case 0 | 1 => 1
  case x if x > 1 =>
    fibonacci (x-1) + fibonacci(x-2)
}

Als nächstes schreiben wir einen Komponententest für diese Methode:

assertEquals(13, fibonacci(6))

9. Fazit

In diesem Tutorial haben wir die Scala-Sprache und einige ihrer wichtigsten Funktionen vorgestellt. Wie wir gesehen haben, bietet es eine hervorragende Unterstützung für die imperative, funktionale und objektorientierte Programmierung.

Wie üblich kann der vollständige Quellcodeover on GitHub gefunden werden.