Tämä kurssi on jo päättynyt.

Kurssin viimeisimmän version löydät täältä: O1: 2024

Luku 5.2: Olioita kaikkialla

Tästä sivusta:

Pääkysymyksiä: Mitä merkkijono-olioilla voi tehdä? Miten jäsennän vaikkapa tähtikatalogin sisältämiä merkkijonoja? Ai, merkkijonot ovat olioita — mitä kaikkia muita olioita on?

Mitä käsitellään? Useaa aiemmin käsiteltyä tietotyyppiä olio-ohjelmoinnin näkökulmasta. Sivuteemoja: operaattorinotaatio, merkkijonoupotukset, pakkausoliot, ym.

Mitä tehdään? Luetaan ja tehdään pieniä tehtäviä.

Suuntaa antava työläysarvio:? Kaksi tai kolme tuntia.

Pistearvo: A55.

Oheisprojektit: Miscellaneous, Stars.

../_images/sound_icon3.png

Muuta: Eräissä tämän luvun kohdissa on kaiuttimista tai kuulokkeista hyötyä. Aivan pakolliset ne eivät ole.

../_images/person081.png

Johdanto

Luvussa 2.1 otimme asiaksemme opetella olio-ohjelmointia, ja oletkin sittemmin luonut monia luokkia ja olioita sekä käyttänyt valmiina annettuja. Samalla on osoittautunut, että eräät jo tutuiksi tulleet Scalan perustyökalut ovat myös olioita, joilla on hyödyllisiä metodeita: tällaisia ovat erimerkiksi vektorit ja puskurit (luku 4.2) ja Optionit (luku 4.3).

Scala on huomattavan puhdasoppinen olio-ohjelmointikieli siinä mielessä, että Scalassa kaikki arvot ovat olioita ja noiden arvojen tyyppeihin liitetyt toiminnot metodeita.

Miten niin? Emmekö ole käyttäneet "irrallisia" funktioita kuten max ja min ja laatineet sellaisia itsekin? Eiväthän kai ne olleet mitään olioiden metodeita? Ja eikö olioiden lisäksi ole näitä Int-arvoja ja Booleaneja ja muita vastaavia, joilla on operaattoreita eikä metodeita?

Katsotaan.

Luvut olioina

Int-tietotyyppi edustaa kokonaislukuarvoja. Myös tämä Scalan valmis tietotyyppi on itse asiassa luokka ja yksittäiset kokonaisluvut Int-olioita, vaikka asiaa ei ole kurssilla toistaiseksi näin muotoiltukaan.

On totta, ettei Int ole aivan tavallinen luokka. Se on siinä mielessä erikoinen, ettei siitä tarvitse luoda ilmentymiä new-sanaa käyttäen, vaan uusia Int-arvoja saa ohjelmassa käyttöön yksinkertaisesti käyttämällä literaalimerkintää. Int-luokka on määritelty Scalan ydinpakkaukseen scala, joka on kielen erottamaton osa. Joihinkin tämän pakkauksen luokkiin liittyy "erikoisuuksia" kuten literaalimerkinnät.

Kuitenkin Int on esimerkiksi siinä mielessä ihan tavallinen luokka, että se määrittelee joukon metodeita, joita voi Int-olioille kutsua. Alla on esimerkkejä.

val luku = 17luku: Int = 17
luku.toBinaryStringres0: String = 10001

Tässä kutsuttiin toBinaryString-metodia, joka palauttaa kyseisen luvun kuvauksen binaarilukuna eli kaksikantaisessa lukujärjestelmässä. Esimerkiksi kymmenkantainen luku 17 on kaksikantaisena 10001.

Myös lukuliteraalille voi kutsua metodia:

123.toBinaryStringres1: String = 1111011

to-metodi

Int-olioilla on metodi to, joka palauttaa luettelon luvuista kohdeolion (alla: 2) ja parametriolion (alla: 6) väliltä.

val luvutKakkosestaKuutoseen = 2.to(6)luvutKakkosestaKuutoseen: Range.Inclusive = Range 2 to 6
luvutKakkosestaKuutoseen.toVectorres2: Vector[Int] = Vector(2, 3, 4, 5, 6)
Palautusarvo on Scalan valmista tyyppiä nimeltä Range. Kyseessä on eräänlainen lukuja sisältävä muuttumaton alkiokokoelma, siis samantapainen kuin (luku)vektori.
Esimerkin Range sisältää luvut 2, 3, 4, 5 ja 6 kasvavassa järjestyksessä, mikä näkyy erityisen selvästi kun kopioimme sen sisällön vektoriin. (Vektori vie enemmän muistia kuin Range, koska se varastoi jokaisen alkion erikseen kun taas Range varastoi tiedon ensimmäisestä ja viimeisestä luvusta.)

to-metodista vähän lisää alempana. Löydämme sille ja sen palautusarvoille kunnollista hyötykäyttöä luvussa 5.6.

toDouble-metodi

Tässä esimerkki kätevästä ja usein käytetystä Int-olioiden metodista, joka palauttaa kokonaislukua vastaavan Double-arvon.

val jaettava = 100jaettava: Int = 100
val jakaja = 7jakaja: Int = 7
val kokonaislukutulos = jaettava / jakajakokonaislukutulos: Int = 14
val tarkempi = jaettava.toDouble / jakajatarkempi: Double = 14.285714285714286
val tulosDoubleksi = (jaettava / jakaja).toDoubletulosDoubleksi: Double = 14.0

Saisit toki samat tulokset ilman toDouble-metodiakin: esim. 1.0 * jaettava / jakaja. Kuitenkin toDouble-metodin käyttäminen ilmaisee suoremmin, mitä halutaan.

abs, min, jne.

Int-olioille on myös määritelty eräitä tuttuja matemaattisia toimintoja metodeina. Esimerkiksi itseisarvon tai kahdesta luvusta pienemmän voi selvittää tähänkin tapaan:

-100.absres3: Int = 100
val luku = 17luku: Int = 17
luku.min(100)res4: Int = 17
Double-arvot ovat olioita siinä missä Int-arvotkin. Kokeile itse Double-olioiden parametrittomia metodeita floor, ceil ja round. Mitkä kaikki seuraavista väittämistä pitävät paikkansa?

Palataan vielä aiemmin luvussa käsiteltyyn kokonaislukujen to-metodiin ja sen sukulaiseen nimeltä until. Oletetaan, että on olemassa kokoelma (esim. vektori tai puskuri) ja siihen viittaava muuttuja x. Mitkä seuraavista väittämistä pitävät paikkansa?

Kokeile itse REPLissä vastauksia saadaksesi. Voit muuntaa ton tai untilin palauttaman Range-kokoelman vektoriksi toVector-metodikutsulla.

Kokoelmien indekseistä

Wikipediassa on artikkeli Zero-based numbering.

Should indices start at 0 or 1?
My compromise of 0.5 was rejected without, I thought, proper consideration.

Stan Kelly-Bootle

Operaattorit metodeina

Luvuilla on siis metodeitakin, mutta entä operaattorit? Olio-ohjelmoinnissahan olioita olisi tarkoitus käyttää "lähettämällä niille viestejä" eli kutsumalla niiden metodeita.

Int-luokka on kuvattu Scala API -dokumentaatiossa tässä osoitteessa:

Dokumentaatiota selaamalla löytyvät muun muassa yllä mainitut metodit toDouble, to ja abs, jne. Samasta luettelosta löytyy koko liuta metodien kuvauksia, joissa metodien niminä on tutunnäköisiä symboleita: !=, *, +, jne. (Muuta tuosta dokumentaatiosta ei ole välttämätöntä ymmärtää tässä vaiheessa.)

Kokeillaan:

val luku = 10luku: Int = 10
luku.+(5)res5: Int = 15
(1).+(1)res6: Int = 2
10.!=(luku)res7: Boolean = false
Aritmeettiset laskutoimitukset ja vertailutkin on määritelty metodeiksi Int-luokkaan. Niinpä Int-oliota voi pyytää niitä suorittamaan tähän tapaan. Tulokset ovat tutut, mutta käskyjen kirjoitustapa aiemmin käytettyä kömpelömpi.

Onko siis erikseen olemassa operaattori +, jota käytetään esimerkiksi lausekkeessa luku + toinen, sekä metodi +, jota käytetään lausekkeessa luku.+(toinen)?

Ei, vaan kyse on samasta metodikutsusta kahdella eri tavalla kirjoitettuna. Scala-kieli nimittäin sallii pisteen ja sulkujen jättämisen pois metodikutsusta silloin, kun metodi ei ota useampaa kuin yhden parametrin. Ilmaisut luku + toinen ja luku.+(toinen) tarkoittavat siis täsmälleen samaa asiaa. Operaattorit ovatkin metodeita!

Scalassa itse asiassa ei edes virallisesti ole operaattorin käsitettä. (Monissa muissa kielissä on.) Silti on usein kätevää ajatella tiettyjä Scala-metodeita — kuten juuri lukujen aritmeettisia metodeita — operaattoreina ja käyttää niitä ilman välimerkkejä.

Pistenotaatio vs. operaattorinotaatio

Voimme siis käyttää esimerkiksi metodia + ilman pistettä ja parametrisulkeita. Sama toimii myös muille yksiparametrisille metodeille. Esimerkiksi tutussa Category-luokassamme voisi yhtä hyvin olla kumpi tahansa seuraavista lausekkeista:

newExperience.chooseBetter(this.fave)
newExperience chooseBetter this.fave

Ensimmäistä kirjoitustapaa sanotaan joskus pistenotaatioksi (dot notation) ja jälkimmäistä operaattorinotaatioksi (operator notation tai infix operator notation).

Molemmilla notaatioilla on puolensa, ja valinta niiden välillä on osin makuasia.

Tässä kurssimateriaalissa operaattorinotaatiota käytetään erittäin säästeliäästi, koska pistenotaatio toimii johdonmukaisesti kaikilla parametrimäärillä sekä koska pistenotaatiossa kohdeolio ja parametrilausekkeet korostuvat selkeämmin ja notaatio tukee siksi ohjelmoinnin peruskäsitteiden oppimista paremmin. Operaattorinotaatiota käytämme lähinnä aritmetiikka-, vertailu- ja logiikkaoperaattorien yhteydessä sekä tietyissä muissa yhteyksissä, joissa se on poikkeuksellisen luontevaa; esimerkiksi Int-olion to-metodin kutsu lienee useimpien mielestä luontevampaa kirjoittaa 1 to 10 kuin 1.to(10).

Saat itse käyttää omissa ohjelmissasi kumpaa tapaa vain. Suosittelemme varsinkin ohjelmoinnin aloittelijoille kurssin käyttämää tapaa. Joka tapauksessa molemmat notaatiot on syytä tuntea, jotta pystyy lukemaan muiden kirjoittamia Scala-ohjelmia. Kurssin ulkopuolisessa Scala-ohjelmoinnissa operaattorinotaation käyttö on yleisempää kuin kurssilla.

Totuusarvojen ja puskurien "operaattoreita"

Logiikkaoperaattorit (luku 5.1) ovat Boolean-olioiden metodeita.

Olkoon muuttujan a arvo 0 ja muuttujan b 10. Mikä on lausekkeen (a.>(b).||(b.==(10))).&&(a.<(0)) arvo?

Puskurien "operaattorit" += ja -= ovat metodeja nekin. Niitä voi kutsua operaattori- tai pistenotaatiolla, joista edellinen lienee tässä tapauksessa kaikkien mielestä mukavampi.

val puskuri = Buffer("eka", "toka", "kolmas")res8: Buffer[String] = ArrayBuffer(eka, toka, kolmas)
puskuri += "neljäs"puskuri.+=("viides")

GoodStuff revisited

Aiemmissa luvuissa olet nähnyt esityksiä GoodStuff-ohjelman toiminnasta, joissa oliot viestivät keskenään. Esityksissä ei esimerkiksi puskureita tai lukuja näytetty olioina.

Kuten on käynyt ilmi, myös ne ovat olioita. Esimerkiksi Experience-olio kutsuu Int-olion >-metodia selvittääkseen kumpi kahdesta kokonaisluvusta on suurempi (mikä määrää, kumpi kokemuksista on parempi). Olio-ohjelman suoritus siis muodostuu näiltäkin osin olioiden välisestä viestinnästä.

String-oliot ja niiden metodit

Minkäänlaisena yllätyksenä ei enää tule se, että String on luokka, merkkijonot sen ilmentymiä ja merkkijono-operaattorit metodeita.

"kissa".+("kala")res9: String = kissakala

Vektori sisältää jonkin tyyppisiä alkioita kukin omalla indeksillään. Merkkijono sisältää merkkejä kukin omalla indeksillään. Käsitteet ovat toisilleen sukua, ja on ymmärrettävää, että merkkijonoille on määritelty monia samankaltaisia metodeita kuin vektoreille ja puskureille. Tästä on monia esimerkkejä alla. Toisaalta merkkijonoilla on ihan omiakin, nimenomaan merkkien käsittelyyn liittyviä metodeitaan. Seuraavat esimerkit ja pikkutehtävät tutustuttavat eräisiin String-metodeihin.

Merkkijonojen käsittely on ohjelmissa yleistä, ja monista alla esitellyistä metodeista on hyötyä tulevissa kurssin vaiheissa ja vielä tässä samassa luvussakin. Tarkoitus ei taaskaan ole opetella metodien yksityiskohtia ulkoa, koska riittävän usein tarvitut metodit jäävät kyllä harjoituksen myötä mieleen. Tärkeämpää on saada yleiskuva siitä, millaista kalustoa on tarjolla.

Merkkijonon pituus eli length

length-metodi palauttaa merkkijonon pituuden eli sen sisältämien merkkien lukumäärän.

"kissa".lengthres10: Int = 5

Merkkijonosta luku: toInt ja toDouble

On yleistä, että halutaan tulkita numeromerkkejä sisältävä merkkijono lukuna. Tyypillinen esimerkki on, että vastaanotetaan ohjelman käyttäjän kirjoittamia merkkejä, joiden kuvaamaa lukuarvoa on sitten tarkoitus käyttää jossakin laskutoimituksessa. Vaikkapa merkit '1', '0' ja '5' sisältävästä merkkijonosta haluttaisiin siis saada muodostettua Int-arvo 105.

Tällaisiin tarkoituksiin sopivat metodit toInt ja toDouble:

val tekstiJossaLuku = "105"tekstiJossaLuku: String = 105
tekstiJossaLuku.toIntres11: Int = 105
tekstiJossaLuku.toDoubleres12: Double = 105.0
"sataviis".toIntjava.lang.NumberFormatException: For input string: "sataviis"
  (jne.)

Tyhjät pois: trim-metodi

trim-metodi on myös usein kätevä jostakin ulkoisesta lähteestä luettua dataa käsiteltäessä. Se tuottaa merkkijonon, jossa välilyönnit ja vastaavat tyhjeet (whitespace) on jätetty pois alusta ja lopusta:

var teksti = "  moi vaan "teksti: String = "  moi vaan "
println("Teksti on: " + teksti + ".")Teksti on:   moi vaan .
println("Teksti on: " + teksti.trim + ".")Teksti on: moi vaan.
REPL korostaa merkkijonon alussa ja lopussa olevaa tyhjää lainausmerkein. Nämä lainausmerkit eivät siis kuitenkaan ole osa itse merkkijonoa.

Metodi on hyödyksi esimerkiksi näppäimistösyötettä siistiessä. Sovellusten käyttäjät kun joskus kirjoittavat turhia tyhjiä merkkejä antamiinsa syötteisiin.

Jos tyhjiä merkkejä ei alussa tai lopussa ole, trim palauttaa koskemattoman merkkijonon:

teksti = "puudeli"teksti: String = puudeli
println("Teksti on: " + teksti.trim + ".")Teksti on: puudeli.

Voimme sanoa, että trim-metodi "poistaa merkkijonosta tyhjää". Tarkalleen ottaen siis se ei kuitenkaan muuta alkuperäistä merkkijonoa, vaan tuottaa uuden merkkijonon, jossa on muuten samat merkit kuin alkuperäisessä, paitsi että alun ja lopun tyhjät on jätetty pois. Kaikki merkkijonojen metodit ovat vaikutuksettomia, myös trim. Jokainen String-olio on täysin muuttumaton.

Merkin poimiminen: apply- ja lift-metodit

apply-metodilla voi pyytää tietyn merkin merkkijonosta antamalla sen järjestysnumeron eli indeksin parametriksi. Järjestysnumerot alkavat nollasta, joten tässä esimerkissä katsotaan ensin toinen ja sitten viimeinen merkki merkkijonosta "kissa":

"kissa".apply(1)res13: Char = i
"kissa".apply(4)res14: Char = a
Huomaa palautusarvon tyyppi. Yksittäisiä merkkejä kuvaa Scalassa Char-luokka (lyhenne sanasta character eli kirjoitusmerkki). String-olio edustaa nollan tai useamman merkin mittaista merkkijonoa, Char täsmälleen yhtä merkkiä.

Merkin katsomiseen indeksin perusteella on myös hieman lyhyempi tapa: kokeile esimerkiksi "kissa"(1).

Ja turvallisempi tapa: muilta kokoelmatyypeiltä tuttu lift-metodi.

"kissa".lift(1)res15: Option[Char] = Some(i)
"kissa".lift(4) res12: Option[Char] = Some(a)
"kissa".lift(123)res16: Option[Char] = None

Osiksi split-metodilla

split-metodilla voi jakaa merkkijonon osiin käyttäen parametriksi annettua erotinta. Tässä erottimena on välilyönti:

val lause = "Parempi kyy pivossa kuin kymmenen."lause: String = Parempi kyy pivossa kuin kymmenen.
val sanat = lause.split(" ")sanat: Array[String] = Array(Parempi, kyy, pivossa, kuin, kymmenen.)
sanat(1)res17: String = kyy
split-metodin palautusarvon tyyppinä esiintyvä Array on vektoria ja puskuria muistuttava alkiokokoelma.
Voit käyttää taulukkoa pitkälti niin kuin vektoria. Käsitellään näiden kokoelmatyyppien eroja luvussa 11.1.

Erotin voi olla mikä vain merkkijono:

lause.split("pi")res18: Array[String] = Array(Parem, " kyy ", vossa kuin kymmenen.)

Kirjainkoko: toUpperCase ja toLowerCase

val sevenNationArmy =
  "[29]<<e--------e--g--. e--. d--. c-----------<h----------->e--------e--g--. e--. d--. c---d---c---<h-----------/360"sevenNationArmy: String = [29]<<e--------e--g--. e--. d--. c-----------<h----------->e--------e--g--. e--. d--. c---
d---c---<h-----------/360
o1.play(sevenNationArmy)

Se oli sen verran hienoa, että voisihan tuon tuutata lujempaakin.

o1.play-funktiomme soittaa isolla kirjoitetut nuotit kovempaa kuin pienellä kirjoitetut. On olemassa helppo tapa tuottaa isoja kirjaimia:

o1.play(sevenNationArmy.toUpperCase)

Tässä lisäesimerkki metodista ja sen ystävästä toLowerCase:

"Little BIG Man".toUpperCaseres19: String = LITTLE BIG MAN
"Little BIG Man".toLowerCaseres20: String = little big man

Vertailua: compareTo-metodi

Luku 3.3 jo näytti, että vertailuoperaattorit toimivat merkkijonoillekin. Myös compareTo-niminen metodi on joskus kätevä:

"mun isä".compareTo("sun isä")

Metodi vertailee merkkijonoja sen mukaan, kumpi niistä on Unicode-merkistön mukaisessa aakkosjärjestyksessä ensin. Mitkä kaikki seuraavista väittämistä pitävät paikkansa?

Merkkien etsimistä: indexOf ja contains

Merkkijonojen indexOf- ja contains-metodit ovat samantapaisia kuin vektorien ja puskurien vastaavat (luvusta 4.2). Niillä voi selvittää, esiintyykö parametriksi annettu pätkä merkkijonossa:

"noitapiiri".contains("tapiiri")res21: Boolean = true
"noitapiiri".contains("laama")res22: Boolean = false
"noitapiiri".indexOf("tapiiri")res23: Int = 3
"noitapiiri".indexOf("laama")res24: Int = -1

take ja drop ja kumppanit

Myös take, drop, head ja tail sukulaisineen ovat analogisia luvun 4.2 esittelemien kokoelmametodien kanssa. Alla on muutama esimerkki:

val vasemmalta = "noitapiiri".take(5)vasemmalta: String = noita
val oikealta = "noitapiiri".drop(6)oikealta: String = iiri
"noitapiiri".take(0)res25: String = ""
"noitapiiri".take(100)res26: String = noitapiiri
"noitapiiri".takeRight(7)res27: String = tapiiri
"noitapiiri".dropRight(7)res28: String = noi
val eka = "noitapiiri".headeka: Char = n
val loput = "noitapiiri".tailloput: String = oitapiiri
val ekaJosOn = "noitapiiri".headOptionekaJosOn: Option[Char] = Some(n)
val tyhjanEka = "noitapiiri".drop(10).headOptiontyhjanEka: Option[Char] = None

Merkkijonoilla on myös substring-metodi. Itse asiassa tuonnimisiä metodeita on kaksi, joista toinen ottaa yhden ja toinen kaksi kokonaislukua parametriksi.

Kokeile mainittuja substring-metodeita REPLissä eri merkkijonoilla ja eri parametriarvoilla. Arvioi kokeilujesi perusteella, mitkä seuraavista väittämistä vaikuttavat pitävän paikkansa. Alla oletetaan, että x on String-tyyppinen muuttuja ja että se viittaa johonkin merkkijonoon, jossa on vähintään neljä merkkiä.

Huomaa, että alla "sama arvo" tarkoittaa, että arvon pitää olla täysin samankaltainen kuin toinen, tietotyypiltään ja sisällöltään.

Lisätehtävä: lisäys merkkijonon keskelle

Kirjoita Miscellaneous-projektin misc.scala-tiedostoon funktio insert, joka lisää annetun merkkijonon tiettyyn kohtaan kohdemerkkijonoa. Sen tulee toimia tämän esimerkin mukaisesti:

val kohde = "pupupaita"kohde: String = pupupaita"
import o1.misc._import o1.misc._
insert("jussi", kohde, 4)res29: String = pupujussipaita
insert("!!!", kohde, 0)res30: String = !!!pupupaita
insert("!!!", kohde, 100)res31: String = pupupaita!!!
insert("!!!", kohde, -2)res32: String = !!!pupupaita

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Lisää merkkijonoista

Kun nyt on merkkijonoja pyöritelty, niin tässäpä vielä muutama toistuvasti hyödyllinen Scala-merkkijonojen erityispiirre.

Erikoismerkit merkkijonoissa

Luvussa 4.2 sivumainittiin, että rivinvaihtomerkin (newline) saa merkkijonoon ilmaisulla \n. Vastaavia erikoismerkintöjä on muitakin. Tässä oleellisimpia:

Merkki Merkintä merkkijonoliteraalissa
rivinvaihto \n
tabulaattori \t
lainausmerkki \"
kenoviiva \\

Käyttöesimerkki:

println("Ja minä, päässä side kauhun, " +
        "huusin:\n\"Oi Mestari, mik' ääni tuo? " +
        "ja keitä\nnuo, jotk' on niinkuin " +
        "suuren tuskan orjat?\"")

Tämä tuottaa seuraavan tulosteen:

Ja minä, päässä side kauhun, huusin:
"Oi Mestari, mik' ääni tuo? ja keitä
nuo, jotk' on niinkuin suuren tuskan orjat?"

Kuten näet, tulosteen rivitys määräytyy \n-merkkien mukaan eikä koodissa olevien rivitysten.

Scalassa on myös mahdollista muodostaa merkkijonoliteraali käyttäen kolmea lainausmerkkiä literaalin kummassakin päässä. Tällöin koko literaalin sisältö erikoismerkkeineen kaikkineen tulkitaan osaksi merkkijonoa eikä etukenoviivoja tarvita. Seuraava koodinpätkä tulostaa saman hilpeän värssyn kuin edellinenkin.

println("""Ja minä, päässä side kauhun, huusin:
"Oi Mestari, mik' ääni tuo? ja keitä
nuo, jotk' on niinkuin suuren tuskan orjat?"""")

Arvojen upottaminen merkkijonoihin

Oletetaan, että luku-nimisen muuttujan arvo on 10. Seuraavassa esimerkissä liitämme sen arvon osaksi merkkijonoa tuttuun tapaan.

"Lukumuuttujan arvo on " + luku + ", ja sitä yhtä suurempi luku on " + (luku + 1) + "."res33: String = Lukumuuttujan arvo on 10, ja sitä yhtä suurempi luku on 11.

Törmäät ennemmin tai myöhemmin myös Scala-koodiin, jossa merkkijonoliteraaleihin on upotettu lausekkeita plus-operaattorin sijaan s-kirjainta ja dollarimerkkiä $ käyttäen. Esimerkiksi äskeisen merkkijonon saa tuotettua myös seuraavasti.

s"Lukumuuttujan arvo on $luku, ja sitä yhtä suurempi luku on ${luku + 1}."res34: String = Lukumuuttujan arvo on 10, ja sitä yhtä suurempi luku on 11.
Huomaa ensimmäistä lainausmerkkiä edeltävä s-kirjain, joka merkitsee, että kyseessä on lausekkeiden upottaminen merkkijonoliteraaliin eli string interpolation.
Dollarimerkin perään voi kirjoittaa nimen. Nimi korvautuu syntyvässä merkkijonossa vastaavalla arvolla.
Monimutkaisempiakin lausekkeita voi upottaa merkkijonoon. Tällöin on käytettävä aaltosulkeita lausekkeen rajaamiseksi.

Kurssimateriaalissa käytämme jatkossakin enimmäkseen plus-operaattoria.

Tehtävä: tempo-funktio

Tehtävänanto

Alla on käyttöesimerkki funktiosta, jolle annetaan samantapainen musiikkikappaletta kuvaava merkkijono kuin o1.play-funktiolle ja joka palauttaa tuon musiikin tempon kokonaislukuna.

import o1.misc._import o1.misc._
tempo("cccedddfeeddc---/180")res35: Int = 180
tempo(s"""[72]${" "*96}d${"-"*39}e---f---d${"-"*39}e---f---d${"-"*15}e---f---d${"-"*15}e---f---
f#-----------g${"-"*17}&[62]${" "*104}(>c<afd)--(>c<afd)--------(afdc)--(>c<afd)-------(afdc)
${"-"*17}       (>c<abfd)--(>c<abfd)--------(abfdc)--(>c<abfd)-------(abfdc)${"-"*17}      (hbgfd)
${"-"*17}(hbg>fd<)${"-"*22}(>ce<hbg)---- (>d<hbge)---------- (>db<hbge)---------- (>c<hbg#e)-----
&[29]${" "*96}<<<${"c-----------"*11}cb-----------<${"hb-----------"*3}hb-----&P:a-----------
${"a--------a--a-----------"*11}/480""")res36: Int = 480

Varsinaisella kappaleen sisällöllä funktio ei siis tee mitään, vaan se vain poimii tempon merkkijonon lopusta. Jos tempoa ei ole merkkijonoon kirjattu lainkaan, niin funktio palauttaa kokonaisluvun 120:

tempo("cccedddfeeddc---")res37: Int = 120

Kirjoita Miscellaneous-projektin misc.scala-tiedostoon funktio tempo, joka toimii esimerkin mukaisesti.

Ohjeita ja vinkkejä

  • Funktion tulee palauttaa nimenomaan Int-tyyppinen arvo, ei merkkijonoa, jossa on numeromerkkejä. Funktio osittelee annetun merkkijonon, poimii tietyn osan, ja tulkitsee sen sisällön lukuna.
  • Tehtävän voi ratkaista hyvin monella eri tavalla. Pelkästään tässä luvussa mainitut merkkijonojen metodit tarjoavat useita eri mahdollisuuksia. Valitse itse jokin tapa. Keksitkö useita?
  • Voit olettaa, että parametriksi annetussa merkkijonossa on kauttaviivamerkkejä joko nolla tai yksi kappaletta. Voit myös olettaa, että jos kauttaviiva löytyy, niin sen perässä on yksi tai useampi numeromerkki eikä muuta. Sinun ei siis tarvitse tässä käsitellä sen erikoisempia tapauksia kuin mitä REPL esimerkeissä yllä oli.

Palauttaminen

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Musiikkikappaleiden kuvauksia kätevämmin?

Tässä materiaalissa on esiintynyt varsin pitkiä play-funktiolle osoitettuja merkkijonoja, jollaisten laatiminen ja muokkaaminen on pitkäpiimäistä ja vetää puoleensa virheitä. Onneksi sinun ei olekaan pakko.

Jos haluat, voit miettiä, millaisia apufunktioita voisi laatia, jotta kappaleita olisi helpompi määritellä kurssilla käytetyn kaltaisiksi nuottimerkkijonoiksi. Voit myös a) pohtia, millaisessa muussa muodossa nuotit voisivat olla kirjattuna tietokoneen muistissa kuin tuollaisina merkkijonoina; b) ideoida, millaisella sovelluksella loppukäyttäjän olisi kätevää kirjata kokonaisia musiikkikappaleita tietokonetta varten; ja c) selvittää, millaisia ratkaisuja on jo olemassa.

Tähtikarttatehtävä, osa 3/4: tähtiä tiedostoista

Luvun 4.4 tähtitehtävän lopuksi totesimme:

Noin niitä yksittäisiä tähtiä tosiaan kuvaan saa, mutta kovin kätevää kuvan luominen ei ole, jos tähtiä pitäisi saada esiin muutamaa enemmän. Kätevämpää olisi, jos tähtien tiedot voisi ladata kerralla jostakin, mihin ne on kirjattu.

Stars-projektin kansioista test ja northern löytyy stars.csv-nimiset tiedostot:

  • test-kansion tiedostoon on kirjoitettu muutaman kuvitteellisen tähden tiedot puolipistein erotettuna. Kohta selviää, mitä kukin merkintä tiedostossa tarkoittaa.
  • northern-kansion tiedostossa on paljon pidempi luettelo, jossa on todellisten tähtien tietoja (alunperin VizieR-palvelusta).

Voit jättää northern-kansiossa olevat muut tiedostot nyt huomiotta.

Tehdään ohjelmastamme versio, joka osaa 1) lukea tähtien tietoja kuvaavat merkkijonot tiedostosta, 2) jäsentää nuo merkkijonot Star-olioiksi ja 3) näyttää tähdet graafisesti. Viimeisen osaongelman olet jo ratkaissut luvussa 4.4. Toisen eli merkkijonojen tulkitsemisen tähdiksi ratkaiset nyt. Ensimmäinen eli varsinainen tiedostonkäsittely on ratkaistu puolestasi annetussa koodissa. (Tiedostonkäsittelystä on tarjolla lisämateriaalia luvussa 11.3.)

Tehtävänanto

Aja annettu StarryApp, pety sen mustaan näkymään ja sulje se. Sinänsä tuo sovellus on jo pitkälti toimiva, mutta ratkaisevaa tehtävää hoitava SkyFiles-yksittäisolion metodi parseStarInfo puuttuu. Sillä on juuri mainittu tehtävä: ottaa sisään yhtä tähteä kuvaava merkkijono (jollaisia löytyy stars.csv-tiedostojen riveiltä) ja luoda vastaava Star-olio.

Lue parseStarInfo-metodin dokumentaatio. Se selittää mitä kukin merkkijonon osa tarkoittaa ja mitkä osista ovat sovelluksemme kannalta merkityksellisiä. Toteuta sitten tuo metodi.

Ohjeita ja vinkkejä

  • Metodi on dokumentoitu osana o1.stars.io-pakkauksen yksittäisoliota SkyFiles.
  • Sovelluksen käyttämä kansio on määritelty StarryApp-ohjelman alussa. Siellä on nyt valittu käyttöön test-kansio, jonka voit jättää aluksi käyttöön, kunnes metodisi toimii. Vaihda sitten käyttöön northern-kansio, niin saat esiin komeamman kuvan.
  • Sinun ei tarvitse huomioida mahdollisuutta, että metodille välitetty merkkijono ei sisältäisi kuutta tai seitsemää puolipistein erotettua osaa tai olisi jotenkin muuten spesifikaation vastainen. Oleta, että saatu merkkijono kelpaa.
    • Ohjelma kaatuu ajonaikaiseen virheeseen, jos stars.csv-tiedostoihin menee kirjoittamaan jotakin kelvotonta. Tämä ei tietenkään olisi hyväksyttävää kunnollisessa tosielämän sovelluksessa, mutta kelvatkoon nyt meille.
  • Löydät hyödyllisiä metodeita mm. tästä samasta luvusta.
  • Huomaa: osa datasta (z-koordinaatti ja toinen tunnisteluku) ei ole tehtävän kannalta merkityksellistä.
../_images/project_stars_noconstellations.png

Kaavio Stars-projektin rakenteesta. Tässä tehtävässä toteutat vain yhden metodin SkyFiles-yksittäisolioon.

Merkkijonoupotukset tässä tehtävässä

Jos haluat, voit kokeilla toteuttaa Star-luokan toString-metodin merkkijonoupotusta käyttäen. Voit myös etsiä muista laatimistasi ohjelmista kohtia, joihin merkkijonoupotus tuntuisi sopivan.

Huomaa, että jos haluat upottaa lausekkeen this.muuttuja arvon, on sinun joko käytettävä aaltosulkeita tai jätettävä this pois: s"muuttujan arvo: ${this.muuttuja}" tai s"muuttujan arvo: $muuttuja".

Lisäksi voit tutustua StarCoords-luokan toString-metodiin, joka muotoilee koordinaatteja kahden desimaalin tarkkuuteen. Lisätietoja siellä käytetystä f-alkuisesta merkinnästä löydät merkkijonoupotuksen eri muotoja kuvailevasta artikkelista.

Palauttaminen

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Lisämateriaalia pakkausolioista ja import-käskystä

Seuraavat asiat eivät ole tämän kurssin tai yleisen ohjelmointiosaamisen kannalta välttämättömiä, mutta niiden tuntemisesta on hyötyä Scala-kielellä ohjelmoivalle.

Oliot pakkauksina, funktiot metodeina

Entäs sitten ne "irralliset funktiot" kuten sqrt (luku 1.6) tai itse laadittu metreiksi (luku 1.7) tai tempo (äsken)? Nehän eivät liittyneet mihinkään olioon, joten edustavatko ne olio-ohjelmointia lainkaan?

Tavallaan.

Teknisesti ottaen jopa nuo funktiot olivat metodeita, vaikkei siltä ole vaikuttanutkaan. Tämä perustuu kahteen ajatukseen.

Ensinnäkin: Scalassa voi ottaa käyttöön olion metodeita import-käskyllä. Esimerkiksi import jokuyksittaisolio._ ottaa käyttöön kaikki kyseisen olion metodit siten, että niitä voi kutsua kirjoittamatta alkuun nimeä jokuolio ja pistettä. Metodeita kutsuessa ei tällöin näytä siltä, että kutsuttaisiin tuon olion metodia.

Toiseksi: Voidaan määritellä yksittäisolio, jota on tarkoitus käyttää juuri mainitulla tavalla importaten ja joka sisältää valikoiman enemmän tai vähemmän toisiinsa liittyviä metodeita. Tällaista oliota sanotaan pakkausolioksi (package object).

Esimerkiksi tässä luvussa kirjoitit funktioita valmiiseen tiedostoon, joka oli muotoiltu tähän tapaan:

package o1
object misc {

  // Oma koodi tänne:
  // ...

}

Itse asiassa siis kirjoitit funktiosi misc-nimisen pakkausolion metodeiksi. Otettuasi tuon olion metodit käyttöön import o1.misc._ -käskyllä saatoit käyttää esimerkiksi tempo-funktiota REPLissä ikään kuin mitään oliota ei olemassa olisikaan.

Scala API:in on määritelty samalla ajatuksella useita pakkausolioita. Yksi niistä on math, joka sisältää sqrt:n ja muut tutut matemaattiset funktiot.

Teknisessä mielessä myös mainitut funktiot ovat siis yksittäisolioiden metodeita. Tämä edustaa Scalan puhdasta olio-ohjelmointilinjaa.

Kuitenkaan käytännössä pakkausolioiden sisältämiä metodeita ei usein ajatella olioiden metodeina. Pakkausolion idea on tavallaan juuri se, että kyseisen olion oliouden saa "unohtaa".

Vuoden 2020 Scala-versio 3.0 muuttaa kieltä jossain määrin. Uudessa kieliversiossa on toisenlainen ratkaisu pakkausolioiden sijaan.

Pakkausolioista ja olio-ohjelmoinnista

Jos ohjelmakokonaisuus rakennetaan siten, että käytetään pääasiassa pakkausolioita ja niiden metodeita, niin lopputulos ei ole kovin "olio-ohjelmoinnillinen". Kun Scalaa käytetään olio-ohjelmointikielenä, on pakkausolioiden metodeita tapana käyttää suhteellisen niukasti.

Esimerkiksi scala.math-pakkausolio edustaa näinollen poikkeusta eikä sääntöä; iso osa Scala API:sta perustuu luokkiin, joista luomme ilmentymiä. Ja Scala API:han tarjoaa tuonkin pakkausolion sisällölle vaihtoehtoja mm. Int-olioiden metodien muodossa: voit esimerkiksi selvittää luvun itseisarvon "funktiotyylillä" abs(a) tai "oliotyylillä" a.abs. Valitse itse.

println, readLine ja pakkausoliot

Usein käyttämämme println-funktio on itse asiassa Predef-nimisen yksittäisolion metodi. Tuo yksittäisolio, jonka nimi tulee sanasta predefined, on Scalassa erikoisasemassa sikäli, että sen metodeita voi aina käyttää missä vain Scala-ohjelmassa ilman erillisiä import-käskyjä ja mainitsematta olion nimeä.

Luvussa 2.7 käytimme puolestaan StdIn-nimistä yksittäisoliota pakkausoliona, kun poimimme käyttöön mm. readLine-metodin käskyllä import scala.io.StdIn._.

importin käyttö muun koodin seassa

Nähdyissä esimerkeissä olemme tavanneet sijoittaa import-käskyt Scala-kooditiedostojen alkuihin, mikä onkin varsin yleistä muutenkin. import-käskyä voi käyttää Scalassa muuallakin, esimerkiksi paikallisesti luokan tai yksittäisen metodinkin sisällä:

import pakkaus1._

class X {
  import pakkaus2._

  def metodiA = {
    // Täällä voi käyttää pakkauksia 1 ja 2.
  }

  def metodiB = {
    import pakkaus3._
    // Täällä voi käyttää pakkauksia 1, 2 ja 3.
  }

}

class Y {
  // Täällä voi käyttää vain pakkausta 1.
}

Tämä joskus selkiyttää koodia.

Ilmentymistä importaaminen

Yllä selvisi, että yksittäisoliota voi käyttää kuin pakkausta ja siitä voi siis poimia metodeita käyttöön import-käskyllä. Itse asiassa saman voi tehdä myös luokan ilmentymälle, jos sattuu haluamaan.

class Ihminen(val nimi: String) {
  val onKuolevainen = true
  def tervehdys = "Moi, olen " + this.nimi
}defined class Ihminen
val sokke = new Ihminen("Sokrates")sokke: Ihminen = Ihminen@1bd0b5e
import sokke._import sokke._
tervehdysres38: String = Moi, olen Sokrates

Yllä siis viimeinen käsky on lyhennysmerkintä ilmaisusta sokke.tervehdys.

Tällainen ilmentymästä importaaminen kylläkin helposti lähinnä sekavoittaa koodia.

Lisämateriaalia: runsaat vs. niukat rajapinnat

Toisiaan muistuttavia metodeita — hyvä vai huono?

Opiskelijoiden kommentteja tämän luvun esittelemistä merkkijonometodeista ja aiemmin esitellystä kokoelmametodien kirjosta:

Stringien manipulaatio vaikuttaisi kätevältä Scalassa. Hyvä että on vakiona [peruskirjastossa tarjolla] noin paljon metodeita.
Tämä luku avasi Scalan perusolemusta lisää, mutta samalla pisti miettimään miksi sama asia tarvitsee saada ilmaistua kaksin eri tavoin. Mielestäni tämä aiheuttaa lähinnä mahdollisuuden sekaantua...
Onpas Scalan perusolioilla paljon valmiita metodeita, joista suuri osa vaikuttaa suhteellisen turhilta ts. tekevän samoja asioita vain eri nimillä.
Scalan merkkijonoja käsittelevät valmiit metodit vaikuttavat varsin kattavilta verrattuna moneen muuhun kieleen.

Ei tosiaan ole harvinaista, että luokalla on paljon metodeita, jotka tekevät osin saman asian ja joista kaikki eivät ole ehdottoman välttämättömiä. Näin on monen Scala-peruskirjaston luokankin kohdalla.

Nuo luokat eivät toisin sanoen tarjoa käyttäjilleen niukkaa rajapintaa (thin interface) vaan runsaan (rich). Teemaa on puitu mm. Kirjoja ja linkkejä -sivun suosittelemassa kirjassa Programming in Scala:

Thin versus rich interfaces represents a commonly faced trade-off in object-oriented design. The trade-off is between the implementers and the clients of an interface. A rich interface has many methods, which make it convenient for the caller. Clients can pick a method that exactly matches the functionality they need. A thin interface, on the other hand, has fewer methods, and thus is easier on the implementers. Clients calling into a thin interface, however, have to write more code. Given the smaller selection of methods to call, they may have to choose a less than perfect match for their needs and write extra code to use it.

Odersky, Spoon & Venners

Toki runsaan rajapinnan käyttäjä voi olla ensin hieman ymmällään vaihtoehtojen moninaisuuden kanssa, mutta se osoittautuu kyllä yleensä pian käteväksi.

Yhteenvetoa

  • Scala on puhdas olio-ohjelmointikieli, jossa kaikki arvot kuvataan olioina.
  • Esimerkiksi Int, Double ja String ovat luokkia ja näidentyyppiset arvot olioita. Niillä on monia käteviä ja yleishyödyllisiä metodeita.
  • Niinsanotut operaattorit ovat metodeita. Scalassa metodeita voi kutsua kahdella tavalla: pistenotaatiolla ja operaattorinotaatiolla.
  • Lukuun liittyviä termejä sanastosivulla: olio-ohjelmointi; pistenotaatio, operaattorinotaatio; merkkijonoupotus; pakkausolio.

Onko näin muissakin kielissä?

Scala ei suinkaan ole ainoa "kaikki on olioita" -kieli. Se kuitenkin eroaa puhtaalla oliolinjallaan eräistä hyvin yleisistä olio-ohjelmointikielistä. Esimerkiksi Java-kielessä monet asiat mallinnetaan luokkina ja olioina mutta toiset (esim. luvut, totuusarvot) ns. alkeistyyppien (primitive types) avulla. Scalan edustama linja vähentää erikoistapauksia ja johdonmukaistaa kielen "pelisääntöjä".

Palaute

Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.

Tekijät

Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!

Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.

Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.

Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.

Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.

Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.

Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.

O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.

Opetustapa, jossa käytämme O1Libraryn työkaluja (kuten Pic) yksinkertaiseen graafiseen ohjelmointiin on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.

Oppimisalusta A+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.

Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 1.1.

Lisäkiitokset tähän lukuun

Stars-projekti on mukaelma Karen Reidin suunnittelemasta ohjelmointiharjoituksesta. Se käyttää tähtidataa VizieR-palvelusta.

Luvussa tehdään vääryyttä Glenn Millerin ja The White Stripesin musiikille. Kiitos ja anteeksi.

Päässä side kauhun huusi Dante Alighieri.

a drop of ink
Palautusta lähetetään...