Inleiding tot Scala

1. Inleiding

In deze tutorial gaan we kijken naar Scala - een van de primaire talen die op de Java Virtual Machine draaien.

We beginnen met de kernfuncties van de taal, zoals waarden, variabelen, methoden en controlestructuren. Vervolgens zullen we enkele geavanceerde functies onderzoeken, zoals functies van hogere orde, currying, klassen, objecten en patroonovereenkomst.

Bekijk onze korte handleiding voor de JVM-talen voor een overzicht van de JVM-talen

2. Projectconfiguratie

In deze tutorial gebruiken we de standaard Scala-installatie van //www.scala-lang.org/download/.

Laten we eerst de scala-library-afhankelijkheid toevoegen aan onze pom.xml. Dit artefact biedt de standaardbibliotheek voor de taal:

 org.scala-lang scala-bibliotheek 2.12.7 

Ten tweede, laten we de scala-maven-plug-in toevoegen voor het compileren, testen, uitvoeren en documenteren van de code:

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

Maven heeft de nieuwste artefacten voor scala-lang en scala-maven-plug-in.

Ten slotte gebruiken we JUnit voor het testen van eenheden.

3. Basisfuncties

In dit gedeelte zullen we de basistaalfuncties onderzoeken aan de hand van voorbeelden. Hiervoor gebruiken we de Scala-tolk.

3.1. Tolk

De tolk is een interactieve schil voor het schrijven van programma's en uitdrukkingen.

Laten we "hallo wereld" ermee afdrukken:

C: \> scala Welkom bij Scala 2.12.6 (Java HotSpot (TM) 64-bits server VM, Java 1.8.0_92). Typ uitdrukkingen voor evaluatie. Of probeer: help. scala> print ("Hallo wereld!") Hallo wereld! scala>

Hierboven starten we de tolk door ‘scala 'op de opdrachtregel te typen. De tolk start en geeft een welkomstbericht weer, gevolgd door een prompt.

Vervolgens typen we onze uitdrukking bij deze prompt. De tolk leest de uitdrukking, evalueert deze en drukt het resultaat af. Vervolgens wordt het herhaald en wordt de prompt opnieuw weergegeven.

Omdat het onmiddellijke feedback geeft, is de tolk de gemakkelijkste manier om met de taal aan de slag te gaan. Laten we het daarom gebruiken om de basistaalkenmerken te verkennen: uitdrukkingen en verschillende definities.

3.2. Uitdrukkingen

Elke berekenbare instructie is een uitdrukking.

Laten we enkele uitdrukkingen schrijven en hun resultaten bekijken:

scala> 123 + 321 res0: Int = 444 scala> 7 * 6 res1: Int = 42 scala> "Hallo," + "Wereld" res2: String = Hallo, Wereld scala> "zipZAP" * 3 res3: String = zipZAPzipZAPzipZAP scala > if (11% 2 == 0) "even" else "odd" res4: String = oneven

Zoals we hierboven kunnen zien, elke uitdrukking heeft een waarde en een type.

Als een uitdrukking niets heeft om te retourneren, retourneert deze de waarde van het type Eenheid. Dit type heeft maar één waarde: (). Het is vergelijkbaar met de leegte trefwoord in Java.

3.3. Waarde definitie

Het sleutelwoord val wordt gebruikt om waarden te declareren.

We gebruiken het om het resultaat van een uitdrukking te noemen:

scala> val pi: dubbel = 3.14 pi: dubbel = 3.14 scala> print (pi) 3.14 

Hierdoor kunnen we het resultaat meerdere keren hergebruiken.

Waarden zijn onveranderlijk. Daarom kunnen we ze niet opnieuw toewijzen:

scala> pi = 3.1415: 12: fout: opnieuw toewijzen aan val pi = 3.1415 ^

3.4. Variabele definitie

Als we een waarde opnieuw moeten toewijzen, declareren we deze in plaats daarvan als een variabele.

Het sleutelwoord var wordt gebruikt om variabelen te declareren:

scala> var straal: Int = 3 straal: Int = 3

3.5. Methode Definitie

We definiëren methoden met behulp van de def trefwoord. Na het sleutelwoord specificeren we de naam van de methode, parameterdeclaraties, een scheidingsteken (dubbele punt) en het retourneringstype. Hierna specificeren we een scheidingsteken (=) gevolgd door de body van de methode.

In tegenstelling tot Java gebruiken we niet de terugkeer trefwoord om het resultaat te retourneren. Een methode retourneert de waarde van de laatste geëvalueerde uitdrukking.

Laten we een methode schrijven gem om het gemiddelde van twee getallen te berekenen:

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

Laten we dan deze methode aanroepen:

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

Als een methode geen parameters aanneemt, kunnen we de haakjes weglaten tijdens het definiëren en aanroepen. Bovendien kunnen we de accolades weglaten als het lichaam maar één uitdrukking heeft.

Laten we een parameterloze methode schrijven coinToss die willekeurig "Head" of "Tail" retourneert:

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

Laten we vervolgens deze methode aanroepen:

scala> println (coinToss) Staart scala> println (coinToss) Head

4. Controlestructuren

Controlestructuren stellen ons in staat om de controlestroom in een programma te veranderen. We hebben de volgende controlestructuren:

  • If-else-uitdrukking
  • While loop en do while loop
  • Voor expressie
  • Probeer expressie
  • Match expressie

In tegenstelling tot Java hebben we geen doorgaan met of breken trefwoorden. We hebben de terugkeer trefwoord. We moeten het echter vermijden.

In plaats van de schakelaar statement, we hebben Pattern Matching via match-expressie. Bovendien kunnen we onze eigen controle-abstracties definiëren.

4.1. als-anders

De als-anders expression is vergelijkbaar met Java. De anders deel is optioneel. We kunnen meerdere if-else-expressies nesten.

Omdat het een uitdrukking is, retourneert het een waarde. Daarom gebruiken we het op dezelfde manier als de ternaire operator (? :) in Java. In feite de taal heeft niet de ternaire operator.

Laten we met if-else een methode schrijven om de grootste gemene deler te berekenen:

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

Laten we vervolgens een eenheidstest voor deze methode schrijven:

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

4.2. Herhalingslus

De while-lus heeft een conditie en een body. Het evalueert herhaaldelijk de body in een lus terwijl de voorwaarde waar is - de voorwaarde wordt aan het begin van elke iteratie geëvalueerd.

Omdat het niets nuttigs heeft om terug te keren, keert het terug Eenheid.

Laten we de while-lus gebruiken om een ​​methode te schrijven om de grootste gemene deler te berekenen:

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}

Laten we vervolgens het resultaat verifiëren:

assertEquals (3, gcdIter (15, 27))

4.3. Doe While Loop

De do while-lus is vergelijkbaar met de while-lus, behalve dat de lusvoorwaarde aan het einde van de lus wordt geëvalueerd.

Laten we met behulp van de do-while-lus een methode schrijven om faculteit te berekenen:

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

Laten we vervolgens het resultaat verifiëren:

assertEquals (720, faculteit (6))

4.4. Voor expressie

De for-expressie is veel veelzijdiger dan de for-lus in Java.

Het kan zich herhalen over enkele of meerdere verzamelingen. Bovendien kan het elementen filteren en nieuwe collecties produceren.

Laten we met de uitdrukking for een methode schrijven om een ​​reeks gehele getallen op te tellen:

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

Hier, A tot B is een generatoruitdrukking. Het genereert een reeks waarden van een naar b.

i <- a tot b is een generator. Het definieert ik net zo val en wijst het de reeks waarden toe die door de generatoruitdrukking worden geproduceerd.

De body wordt uitgevoerd voor elke waarde in de reeks.

Laten we vervolgens het resultaat verifiëren:

assertEquals (55, rangeSum (1, 10))

5. Functies

Scala is een functionele taal. Functies zijn hier eersteklas waarden - we kunnen ze gebruiken zoals elk ander waardetype.

In deze sectie zullen we enkele geavanceerde concepten met betrekking tot functies bekijken: lokale functies, functies van hogere orde, anonieme functies en currying.

5.1. Lokale functies

We kunnen functies binnen functies definiëren. Ze worden geneste functies of lokale functies genoemd. Net als bij de lokale variabelen, ze zijn alleen zichtbaar binnen de functie waarin ze zijn gedefinieerd.

Laten we nu een methode schrijven om kracht te berekenen met behulp van een geneste functie:

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)}

Laten we vervolgens het resultaat verifiëren:

assertEquals (8, macht (2, 3))

5.2. Functies van hogere orde

Omdat functies waarden zijn, kunnen we ze als parameters doorgeven aan een andere functie. We kunnen ook een functie een andere functie laten retourneren.

Functies die op functies werken, noemen we functies van hogere orde. Ze stellen ons in staat om op een meer abstract niveau te werken. Door ze te gebruiken, kunnen we codeduplicatie verminderen door gegeneraliseerde algoritmen te schrijven.

Laten we nu een functie van hogere orde schrijven om een ​​kaart uit te voeren en de bewerking over een reeks gehele getallen te verminderen:

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) {resultaat} anders {iter (a + 1, r (m (a), resultaat))}} iter (a, i)}

Hier, r en m zijn parameters van Functie type. Door verschillende functies door te geven, kunnen we een reeks problemen oplossen, zoals de som van vierkanten of kubussen en de faculteit.

Laten we vervolgens deze functie gebruiken om een ​​andere functie te schrijven sumSquares dat somt de kwadraten van gehele getallen op:

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

Hierboven kunnen we dat zien functies van hogere orde hebben de neiging om veel kleine functies voor eenmalig gebruik te creëren. We kunnen voorkomen dat ze een naam krijgen door anonieme functies te gebruiken.

5.3. Anonieme functies

Een anonieme functie is een uitdrukking die evalueert naar een functie. Het is vergelijkbaar met de lambda-uitdrukking in Java.

Laten we het vorige voorbeeld herschrijven met anonieme functies:

@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 dit voorbeeld mapReduce ontvangt twee anonieme functies: (x, y) => x + y en x => x * x.

Scala kan de parametertypes uit context afleiden. Daarom laten we het type parameters in deze functies weg.

Dit resulteert in een beknoptere code in vergelijking met het vorige voorbeeld.

5.4. Currying-functies

Een curried-functie heeft meerdere argumentenlijsten nodig, zoals def f (x: Int) (y: Int). Het wordt toegepast door meerdere argumentlijsten door te geven, zoals in f (5) (6).

Het wordt geëvalueerd als een aanroep van een reeks functies. Deze tussenliggende functies gebruiken een enkel argument en retourneren een functie.

We kunnen ook gedeeltelijk argumentlijsten specificeren, zoals f (5).

Laten we dit nu begrijpen met een voorbeeld:

@Test def whenSumModCalledWith6And10_then10 = {// een curried functie def som (f: Int => Int) (a: Int, b: Int): Int = if (a> b) 0 else f (a) + som (f) (a + 1, b) // een andere curried-functie def mod (n: Int) (x: Int) = x% n // toepassing van een curried-functie assertEquals (1, mod (5) (6)) // gedeeltelijk toepassing van curried-functie // achterliggend onderstrepingsteken is vereist om // functietype expliciet te maken val sumMod5 = sum (mod (5)) _ assertEquals (10, sumMod5 (6, 10))}

Bovenstaande, som en mod nemen elk twee argumentenlijsten.

We passeren de twee argumentenlijsten zoals mod (5) (6). Dit wordt geëvalueerd als twee functieaanroepen. Eerste, mod (5) wordt geëvalueerd, wat een functie retourneert. Dit wordt op zijn beurt aangeroepen met argumentatie 6. We krijgen er 1 als resultaat.

Het is mogelijk om de parameters gedeeltelijk toe te passen zoals in mod (5). We krijgen daardoor een functie.

Evenzo in de uitdrukking som (mod (5)) _, we geven alleen het eerste argument door aan som functie. Daarom sumMod5 is een functie.

Het onderstrepingsteken wordt gebruikt als tijdelijke aanduiding voor niet-toegepaste argumenten. Omdat de compiler niet kan concluderen dat een functietype wordt verwacht, gebruiken we het onderste onderstrepingsteken om het functie-retourtype expliciet te maken.

5.5. Parameters op naam

Een functie kan parameters op twee verschillende manieren toepassen - op waarde en op naam - het evalueert op waarde-argumenten slechts één keer op het moment van aanroep. Het evalueert daarentegen op naam argumenten wanneer ze worden verwezen. Als het bij-naam-argument niet wordt gebruikt, wordt het niet geëvalueerd.

Scala gebruikt standaard parameters op basis van waarde. Als het parametertype wordt voorafgegaan door een pijl (=>), schakelt het over naar de parameter op naam.

Laten we het nu gebruiken om de while-lus te implementeren:

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

Om de bovenstaande functie correct te laten werken, moeten beide parameters staat en lichaam moeten worden geëvalueerd elke keer dat ze worden verwezen. Daarom definiëren we ze als bijnaamparameters.

6. Klasse-definitie

We definiëren een klasse met de klasse trefwoord gevolgd door de naam van de klas.

Na de naam, we kunnen primaire constructorparameters specificeren. Hierdoor worden automatisch leden met dezelfde naam aan de klas toegevoegd.

In de hoofdtekst van de klas definiëren we de leden - waarden, variabelen, methoden, enz. Ze zijn standaard openbaar, tenzij gewijzigd door de privaat of beschermd toegang tot modificatoren.

We moeten de overschrijven trefwoord om een ​​methode uit de superklasse te overschrijven.

Laten we een klasse-medewerker definiëren:

class Employee (val name: String, var salaris: Int, jaarlijkseIncrement: Int = 20) {def incrementSalary (): Unit = {salaris + = jaarlijkseIncrement} overschrijven def toString = s "Medewerker (naam = $ naam, salaris = $ salaris ) "}

Hier specificeren we drie constructorparameters: naam, salaris, en jaarlijkse verhoging.

Omdat we verklaren naam en salaris met val en var trefwoorden, de corresponderende leden zijn openbaar. Aan de andere kant gebruiken we geen val of var trefwoord voor de jaarlijkse verhoging parameter. Daarom is het corresponderende lid privé. Omdat we een standaardwaarde voor deze parameter specificeren, kunnen we deze weglaten tijdens het aanroepen van de constructor.

Naast de velden definiëren we de methode incrementSalaris. Deze methode is openbaar.

Laten we vervolgens een eenheidstest voor deze klas schrijven:

@Test def whenSalaryIncremented_thenCorrectSalary = {val werknemer = nieuwe werknemer ("Jan Jansen", 1000) werknemer.incrementSalary () assertEquals (1020, werknemer.salaris)}

6.1. Abstracte klasse

We gebruiken het trefwoord abstract om een ​​klas abstract te maken. Het is vergelijkbaar met dat in Java. Het kan alle leden hebben die een gewone klas kan hebben.

Bovendien kan het abstracte leden bevatten. Dit zijn leden met alleen declaratie en geen definitie, waarbij hun definitie wordt gegeven in de subklasse.

Net als bij Java kunnen we geen instantie van een abstracte klasse maken.

Laten we nu de abstracte klasse illustreren met een voorbeeld.

Laten we eerst een abstracte klasse maken IntSet om de reeks gehele getallen weer te geven:

abstracte klasse IntSet {// voeg een element toe aan de set def incl (x: Int): IntSet // of een element behoort tot de set def bevat (x: Int): Boolean}

Laten we vervolgens een betonnen subklasse maken EmptyIntSet om de lege set te vertegenwoordigen:

class EmptyIntSet breidt IntSet uit {def bevat (x: Int) = false def incl (x: Int) = nieuw NonEmptyIntSet (x, this)}

Dan nog een subklasse NonEmptyIntSet vertegenwoordigen de niet-lege sets:

class NonEmptyIntSet (val head: Int, val tail: IntSet) breidt IntSet uit {def bevat (x: Int) = head == x || (staart bevat x) def incl (x: Int) = if (dit bevat x) {this} else {new NonEmptyIntSet (x, this)}}

Laten we tot slot een eenheidstest schrijven voor NietEmptySet:

@Test def givenSetOf1To10_whenContains11Called_thenFalse = {// Stel een set in met gehele getallen 1 tot 10. val set1To10 = Bereik (1, 10) .foldLeft (new EmptyIntSet (): IntSet) {(x, y) => x incl y} assertFalse (set1To10 bevat 11)}

6.2. Eigenschappen

Eigenschappen komen overeen met Java-interfaces met de volgende verschillen:

  • kunnen uitbreiden vanuit een klas
  • heeft toegang tot superklasse-leden
  • kan initialisatie-instructies hebben

We definiëren ze zoals we klassen definiëren, maar gebruiken de eigenschap trefwoord. Bovendien kunnen ze dezelfde leden hebben als abstracte klassen, met uitzondering van constructorparameters. Bovendien zijn ze bedoeld om als mixin aan een andere klas te worden toegevoegd.

Laten we nu eigenschappen illustreren met een voorbeeld.

Laten we eerst een eigenschap definiëren UpperCasePrinter om ervoor te zorgen dat toString methode retourneert een waarde in hoofdletters:

eigenschap UpperCasePrinter {overschrijven def toString = super.toString toUpperCase}

Laten we deze eigenschap vervolgens testen door deze toe te voegen aan een Werknemer klasse:

@Test gedefinieerd gegevenEmployeeWithTrait_whenToStringCalled_thenUpper = {val werknemer = nieuwe werknemer ("John Doe", 10) met UpperCasePrinter assertEquals ("WERKNEMER (NAME = JOHN DOE, SALARY = 10)", employee.toString)}

Klassen, objecten en eigenschappen kunnen maximaal één klasse erven, maar een willekeurig aantal eigenschappen.

7. Objectdefinitie

Objecten zijn instanties van een klasse. Zoals we in eerdere voorbeelden hebben gezien, maken we objecten van een klasse met behulp van de nieuw trefwoord.

Als een klasse echter maar één instantie kan hebben, moeten we voorkomen dat er meerdere instanties worden gemaakt. In Java gebruiken we het Singleton-patroon om dit te bereiken.

Voor dergelijke gevallen hebben we een beknopte syntaxis genaamd objectdefinitie - vergelijkbaar met de klassendefinitie met één verschil. In plaats van de klasse trefwoord gebruiken we de voorwerp trefwoord. Door dit te doen, wordt een klasse gedefinieerd en wordt op luiheid de enige instantie ervan gemaakt.

We gebruiken objectdefinities om utiliteitsmethoden en singletons te implementeren.

Laten we een Gebruikt voorwerp:

object gebruikt {def gemiddelde (x: dubbel, y: dubbel) = (x + y) / 2}

Hier definiëren we de klasse Gebruikt en ook het creëren van de enige instantie.

We verwijzen naar dit enige exemplaar met zijn naamGebruikt. Dit exemplaar wordt gemaakt de eerste keer dat het wordt geopend.

We kunnen geen ander exemplaar van Utils maken met de nieuw trefwoord.

Laten we nu een eenheidstest schrijven voor de Gebruikt voorwerp:

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

7.1. Companion Object en Companion Class

Als een klasse en een objectdefinitie dezelfde naam hebben, noemen we ze respectievelijk een begeleidende klasse en een begeleidend object. We moeten beide in hetzelfde bestand definiëren. Begeleidende objecten hebben toegang tot privéleden vanuit hun begeleidende klasse en vice versa.

In tegenstelling tot Java, we hebben geen statische leden. In plaats daarvan gebruiken we begeleidende objecten om statische leden te implementeren.

8. Patroonaanpassing

Bij patroonovereenkomst wordt een uitdrukking met een reeks alternatieven vergeleken. Elk van deze begint met het trefwoord geval. Dit wordt gevolgd door een patroon, een scheidingsteken (=>) en een aantal uitdrukkingen. De uitdrukking wordt geëvalueerd als het patroon overeenkomt.

We kunnen patronen bouwen van:

  • case class constructors
  • variabel patroon
  • het jokertekenpatroon _
  • letterlijke
  • constante identifiers

Case-klassen maken het gemakkelijk om patroonmatching op objecten uit te voeren. We voegen toe geval trefwoord bij het definiëren van een klasse om er een case-klasse van te maken.

Patroonaanpassing is dus veel krachtiger dan de instructie switch in Java. Om deze reden is het een veel gebruikte taalfunctie.

Laten we nu de Fibonacci-methode schrijven met behulp van patroonovereenkomst:

def fibonacci (n: Int): Int = n match 1 => 1 geval x als x> 1 => fibonacci (x-1) + fibonacci (x-2) 

Laten we vervolgens een eenheidstest voor deze methode schrijven:

assertEquals (13, fibonacci (6))

9. Conclusie

In deze tutorial hebben we de Scala-taal en enkele van de belangrijkste functies geïntroduceerd. Zoals we hebben gezien, biedt het uitstekende ondersteuning voor imperatief, functioneel en objectgeoriënteerd programmeren.

Zoals gewoonlijk is de volledige broncode te vinden op GitHub.