Helsingin yliopisto / tietojenkäsittelytieteen laitos / Ohjelmointitekniikka (Scala) / © Arto Wikla 2016

5 Perustyyppejä ja operaatioita

Muutettu viimeksi 29.2.2016 / Sivu luotu 18.3.2010 / [oppikirjan esimerkit] / [Scala]

Sivun sisältöä:

Numeeriset tyypit

Scalan numeeriset tyypit Byte, Short, Int, Long, Char, Float ja Double vastaavat arvoalueiltaan, literaalivakioesityksiltään, ym. vastaavia Javan pienellä kirjoitettuja alkeistyyppejä. Jos asiaa haluaa kerrata, peruskurssien vanhan materiaalin sivulta Javan kokonais- ja liukulukutyyppejä, tyyppimuunnoksia löytyy tietoja.

Scalan numeeriset tyypit on Bytecode-tasolla toteutettu Javan alkeistyyppeinä, joten ohjelmien suoritustehokkuuden pitäisi säilyä Java-tasoisena.

Scalassa numeeristen tyyppien operaatiovalikoimaa on rikastettu, ks. alla.

String-tyyppi

String-tyyppinä käytetään sellaisenaan Javan tyyppiä java.lang.String. Koko pakkaus java.lang näkyy oletusarvoisesti Scala-käännösyksikköön.

String-literaalivakioihin on tehty kirjoitusvaivoja säästävä lisäys: kolmikertaisin lainausmerkein voi kirjoittaa "raakatekstiä":

println("""Näin voi kirjoittaa tekstiä, joka sisältää vaikkapa
           rivinvaihtoja, "- ja \-merkkejä sellaisinaan

              tyhjiä rivejä yms, yms.
           vain kolminkertaista lainausmerkkiä ei voi olla...""")
Tulostus:
Näin voi kirjoittaa tekstiä, joka sisältää vaikkapa
           rivinvaihtoja, "- ja \-merkkejä sellaisinaan
           
              tyhjiä rivejä yms, yms.
           vain kolminkertaista lainausmerkkiä ei voi olla...
On ehkä tylsää saada nuo rivin alkujen välilyöntijonot "raakoina" mukaan. Apu löytyy pystyviivasta ja stripMargin-metodista:
println("""|Näin voi kirjoittaa tekstiä, joka sisältää vaikkapa
           |rivinvaihtoja, "- ja \-merkkejä sellaisinaan
           |
           |   tyhjiä rivejä yms, yms.
           |vain kolminkertaista lainausmerkkiä ei voi olla...""".stripMargin)

Tulostus:
Näin voi kirjoittaa tekstiä, joka sisältää vaikkapa
rivinvaihtoja, "- ja \-merkkejä sellaisinaan

   tyhjiä rivejä yms, yms.
vain kolminkertaista lainausmerkkiä ei voi olla...
Huom: Javasta poiketen String-olioiden sisältöjen yhtäsuuruutta voi verrata ==-operaatiolla!

Symboliliteraalit

Symboliliteraalin syntaksi on heittomerkillä alkava muuten aakkosnumeerinen tunnus. Siis vaikkapa 'tunnus. Ilmaus on lyhenne factory-metodin kutsulle Symbol("tunnus"), joka palauttaa arvonaan Symbol-olion.

Symbolit ovat, ..., hmm..., ..., no symbolit ovat symboleja:

def sanoJotain(kuka:Symbol) {
  if      (kuka=='kissa) println("Miau")
  else if (kuka=='koira) println("Hauhau")
  else                   println("Ammuu")
}

sanoJotain('possu) // Ammuu
sanoJotain('kissa) // Miau
sanoJotain('koira) // Hauhau
Juuri mitään muuta kuin samuuden kyselyä symboleille ei voi tehdä. Mutta tämähän on oikeastaan aika näppärä tapa toteuttaa erilaisia "ohjauskoodeja". Ei ainakaan tavitse itse koodata asioiden nimiä kokonaisluvuiksi tms. Puhumattakaan että joutuisi jotain Javan kamalaa "enumerated"-tyyppiä käyttämään.

"Operaattori == metodi"

Kuten jo on nähty, Scalassa metodit -- eli oliolle sovellettavat aksessorit -- ovat operaattoreita. Ja myös toisin päin: operaattorit ovat oliolle sovellettavia aksessoreita. Esimerkiksi:
println(1+2)        // 3
println((1).+(2))   // 3

// nyt myös:
println(1.+(2))     // muutama versio sitten tulkittiin doubleksi

println("kukkapenkki".indexOf('p'))  // 5
println("kukkapenkki" indexOf 'p' )  // 5
Esimerkiksi Int-luokka sisältää useita erilaisia kuormitettuja versioita +-metodista: Vaikkapa kutsu 1+2L eli (1).+(2L) käynnistää sellaisen Int-luokassa määritellyn +-metodin, jonka parametri on tyyppiä Long.

Nähdyt esimerkit olivat infix-operaatioita: "operandi-operaatio-operandi". Muitakin on:

// näin kaksiparametrinen metodikutsu "infixataan":
println("kukkapenkki".indexOf('k', 4))   // 8
println("kukkapenkki" indexOf ('k', 4))  // 8
Prefix ja postfix:
// prefix:
println(-7)         // -7
println(7.unary_-)  // -7

//postfix:
import scala.language.postfixOps // nyt tarvitaan tämä!

println("HUUDETAAN".toLowerCase) // huudetaan
println("HUUDETAAN" toLowerCase) // huudetaan
Huom: Ainoastaan merkkejä +, - ja ~ voi käyttää unaarisina prefix-operaatioina! Ne ohjelmoidaan metodeina unary_+, unary_- unary_~.

Esimerkki:

class Kissa {
  def unary_+ = "Miau"
  def unary_- = "Hyrr"
  def unary_~ = "Shhhhs"
}

val a = new Kissa
println(+a)  // Miau
println(-a)  // Hyrr
println(~a)  // Shhhhs

// myös toki tyyliin:
println(a.unary_+) // Miau

Olioiden samuus

Java-ohjelmoijat ovat kovassa koulussa oppineet ja sisäistäneet, että ==- ja != vertailuilla on yleensä järkevää verrata vain alkeistyypin arvoja ja että olioiden vertailu noilla johtaa vain vaikeasti löydettäviin ohjelmointivirheisiin. Ja siis omiin luokkiin ohjelmoidaan ikiomat equals-metodit, jotka polymorfismi taitavasti käy aina noutamassa, kun olioarvojen samuutta kysellään milloin missäkin.

Asiat ovat lopulta aika samoin Scalassa. Tosin kaikki arvot ovat olioita ja ==- ja !=-operaatiot testaavat equals-samuutta! Ne suorastaan itse asiassa kutsuvat equals-metodia. Siispä esimerkiksi seuraavat vertailut ovat true:

"he"+"llo" == "hello" 
List(1, 2, 3) == List(1, 2, 3)
"kukamitaha" != 3.14  // jopa siis keskenään eri tyyppisiä voi
                      // verrata dynaamisten kielten tapaan
Vertailun vasemman puoleinen vertailtava määrää, minkä luokan equals-metodia kutsutaan.

Omiin luokkiin ohjelmoidut equals-metodit käyttäytyvät samaan tapaan:

class Otus (val nimi: String, val pituus: Int) {
  // pitäisi määritellä myös override def hashCode!
  override def equals(that: Any) = 
    that match {
      case that: Otus => this.nimi == that.nimi && this.pituus == that.pituus
      case _ => false
  }
}

val a = new Otus("Aapeli", 22)
val b = new Otus("Beepeli", 22)
val c = new Otus("Aa" + "peli", 30-8)

println(a==b)     // false
println(a==c)     // true
println(a=="kukamitaha") // false
Huom: Esimerkin equals-funktiossa on vasta myöhemmin opittavia asioita ja sen toteutus on myös puutteellinen!

Scalassa on toki myös oliosamuustestit, jotka ovat nimeltään eq ja ne. Siispä esimerkiksi seuraavat vertailut ovat false:

val a="he"; a+"llo" eq "hello"
List(1, 2, 3) eq List(1, 2, 3)
["he"+"llo" eq "hello" näkyy kuitenkin olevan true. Siispä jo kääntäjä näkyy osaavan evaluoida tuon String-vakioiden katenaation, mutta niin viisas kääntäjä ei ole, että osaisi tuon muuttujaan sijoitetun arvonkin oivaltaa. ;-]

Operaattoripresedenssi

Hieno sana "operaattoripresedenssi" tarkoittaa operaattoreiden sitovuutta; siis vaikka sitä jo alakoulusta tuttua juttua että kertolaskut lasketaan ennen yhteenlaskuja, ellei ole sulkeita. Muuten nimittäin opettaja antaa kielteistä palautetta...

Scalassa asia on sikäli kiinostava, että ohjelmoija voi sydämensä kyllyydestä ohjelmoida ikiomia operaattoreitaan.

Ideana on sopia asiat tyyliin: kaikki kertomerkillä (*) alkavat operaatiot (eli metodit) sitovat tiukemmin kuin mikään yhteenlaskumerkillä (+) alkava operaatio (eli metodi). Ja vain ensimmäinen merkki merkitsee. Siis vaikkapa kaikki *-merkillä alkavat operaatiot sitovat yhtä tiukasti eli samalla tiukkuudella.

Sitovuus ensimmäisen merkin perusteella tiukimmasta löysimpään:

muut kuin alla luetellut erikoismerkit
* / %
+ -
:
= !
< >
&
^
|
kirjaimet
kaikki sijoitusoperaatiot
Poikkeus: Kaikki =-merkkiin päättyvät sijoitusoperaatiot siis kuuluvat heikoimmin sitovaan luokkaan riippumatta niiden ensimmäisestä merkistä! Esimerkiksi vaikka ilmauksessa x *= y + 1 ensimmäinen operaatio alkaa kertomerkillä, "kertova sijoitus" sitoo silti löysemmin kuin yhteenlasku.

Huom: Kuten aiemmin jo todettiin, :-merkkiin (kaksoispiste) päättyvät operaatiot assosioituvat oikealta vasemmalle. Kaikki muut operaatiot assosioituvat tuttuun tapaan vasemmalta oikealle.

Rikastettuja operaatioita

Scalan perustyyppien operaatiovalikoimaa on täydennetty implisiittisen tyyppimuunnostekniikan avulla. Tekniikasta lisää myöhemmin, mutta tässä muutamia esimerkkejä lisätyistä operaatioista:
0 max 5			5
0 min 5 		0
-2.7 abs 		2.7
-2.7 round 		-3
1.5 isInfinity 		false
(1.0 / 0) isInfinity 	true
4 to 6 			Range(4, 5, 6)
"bob" capitalize 	"Bob"
"robert" drop 2 	"bert"
...
(Tässä taas tarvitaan tuo import scala.language.postfixOps.)

Rikastuksia löytyy mm. luokista RichInt, RichChar, RichString, RichDouble RichBoolean. Katso Scala-API.