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, pakkausoliot, ym.

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

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

Pistearvo: A60.

Oheismoduulit: 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/person03.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. 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 sulkeiden 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 pitää 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

Laaditaan kokeeksi pikkuohjelma, joka pyytää käyttäjää syöttämään luvun ja raportoi sitten syötteen numeroiden määrän sekä erään laskutoimituksen tuloksen. Ohjelman pitäisi toimia tekstikonsolissa näin:

Please enter an integer: 2021
Your number is 4 digits long.
Multiplying it by its length gives 8084.

Ensimmäinen versio koodista on tässä:

import io.StdIn._

object InputTest extends App {
  val input = readLine("Please enter an integer: ")
  val digits = input.length
  println(s"Your number is $digits digits long.")
  val multiplied = input * digits
  println(s"Multiplying it by its length gives $multiplied.")
}

Tuo toteutus "toimii" näiden kahden esimerkkiajon tapaan:

Please enter an integer: 2021
Your number is 4 digits long.
Multiplying it by its length gives 2021202120212021.
Please enter an integer: laama
Your number is 5 digits long.
Multiplying it by its length gives laamalaamalaamalaamalaama.

Käsittelemme syötettä merkkijonona, mikä onkin tarpeen, jos haluamme kutsua length-metodia. Merkkijonon kertominen luvulla ei kuitenkaan tuota haluttua tulosta. Tyydyttävää ei ole sekään, että ohjelmamme hyväksyy minkä vain tekstinpätkän muka luvuksi.

Tässä tapauksessa — kuten lukemattomissa muissakin reaalimaailman ohjelmissa — haluamme tulkita numeromerkkejä sisältävän merkkijonon lukuna, jotta sillä voidaan laskea. Haluamme siis esimerkiksi muodostaa merkit '1', '0' ja '5' sisältävästä String-arvosta Int-arvon 105.

Suoraviivaisin ratkaisu on käyttää metodia toInt (tai toDouble):

val tekstiJossaLuku = "105"tekstiJossaLuku: String = 105
tekstiJossaLuku.toIntres11: Int = 105
tekstiJossaLuku.toDoubleres12: Double = 105.0

Sovelletaan ohjelmaamme:

import io.StdIn._

object InputTest extends App {
  val input = readLine("Please enter an integer: ")
  val digits = input.length
  println(s"Your number is $digits digits long.")
  val multiplied = input.toInt * digits
  println(s"Multiplying it by its length gives $multiplied.")
}

Ainoa muutos edelliseen on, että nyt tulkitsemme numeromerkit luvuksi kertolaskua varten.

Mikä tai mitkä seuraavista yllä olevaa ohjelmaa kuvaavista väitteistä pitävät paikkansa?

Turvallisemmin: toIntOption ja toDoubleOption

Äskeisessä ohjelmassa ongelmaksi jäi, että tieto voi puuttua käyttäjän virhesyötteen vuoksi. Vaikka pienissä kokeiluohjelmissa ei tuollaisilla erikoistilanteilla välttämättä olekaan väliä, muissa ohjelmissa usein on.

Puuttuvaa tietoa voi käsitellä mm. Option-tyypin avulla. Yksi helppo tapa tehdä sellainen on käyttää toInt-metodin sijaan toIntOption-metodia; on myös toDoubleOption ja muitakin vastaavia.

"100".toIntOptionres13: Option[Int] = Some(100)
"sata".toIntOptionres14: Option[Int] = None
"100.99".toDoubleOptionres15: Option[Double] = Some(100.99)

toIntOption-tehtävä

Ota esiin äskeinen InputTest-ohjelma Miscellaneous-moduulin o1.inputs-pakkauksesta.

Pelkkä toInt-kutsun korvaaminen toIntOption-kutsulla ei saa koodia toimimaan. Päinvastoin: noin muokattua koodia ei voi edes ajaa. (Miksei?)

On tarpeen käsitellä eri tavoin tapaukset, joissa käyttäjän syöte on kelvollinen ja kelvoton. Muokkaa ohjelmaa niin, että se toimii näiden kahden ajoesimerkin mukaisesti. (Vinkki: käsittele tapaukset matchillä.)

Please enter an integer: 105
Your number is 3 digits long.
Multiplying it by its length gives 315.
Please enter an integer: sataviis
That is not a valid input. Sorry!

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

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)res16: Char = i
"kissa".apply(4)res17: 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)res18: Option[Char] = Some(i)
"kissa".lift(4) res12: Option[Char] = Some(a)
"kissa".lift(123)res19: 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)res20: 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")res21: 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
play(sevenNationArmy)

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

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

play(sevenNationArmy.toUpperCase)

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

"Little BIG Man".toUpperCaseres22: String = LITTLE BIG MAN
"Little BIG Man".toLowerCaseres23: 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")res24: Boolean = true
"noitapiiri".contains("laama")res25: Boolean = false
"noitapiiri".indexOf("tapiiri")res26: Int = 3
"noitapiiri".indexOf("laama")res27: 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 = "tunnelma".take(5)vasemmalta: String = tunne
val oikealta = "tunnelma".drop(5)oikealta: String = lma
"tunnelma".take(0)res28: String = ""
"tunnelma".take(100)res29: String = tunnelma
"tunnelma".takeRight(5)res30: String = nelma
"tunnelma".dropRight(5)res31: String = tun
val eka = "tunnelma".headeka: Char = t
val loput = "tunnelma".tailloput: String = unnelma
val ekaJosOn = "tunnelma".headOptionekaJosOn: Option[Char] = Some(t)
val tyhjanEka = "tunnelma".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ä.

Selvennys: 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-moduulin 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"
insert("jussi", kohde, 4)res32: String = pupujussipaita
insert("!!!", kohde, 0)res33: String = !!!pupupaita
insert("!!!", kohde, 100)res34: String = pupupaita!!!
insert("!!!", kohde, -2)res35: String = !!!pupupaita

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

Merkkijonoista puheen ollen: 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?"""")

Tehtävä: tempo-funktio

Tehtävänanto

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

tempo("cccedddfeeddc---/180")res36: 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""")res37: 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---")res38: Int = 120

Kirjoita Miscellaneous-moduulin 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.

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-moduulin 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-kansion 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.
  • Osa datasta (z-koordinaatti ja toinen tunnisteluku) ei ole tehtävän kannalta merkityksellistä.
../_images/module_stars_noconstellations.png

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

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. Kun tuon olion metodit on otettu käyttöön import o1.misc._ -käskyllä (jonka REPLimme hoitaa automaattisesti), 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".

Juuri äskettäin vuonna 2021 julkaistu Scala-versio 3 (jota emme vielä käytä tällä kurssilla) muuttaa kieltä jossain määrin. Uudessa kieliversiossa on toisenlainen ratkaisu pakkausolioiden sijaan.

Pakkausolioista ja olio-ohjelmoinnista

Jos ohjelmakokonaisuudessa käytetään pääasiassa pakkausolioita ja niiden metodeita, ei lopputulos 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._
tervehdysres39: 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ä. Tämä pätee monelle Scala-peruskirjaston luokallekin.

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; pakkausolio.

Onko näin muissakin kielissä?

Scala ei suinkaan ole ainoa "kaikki on olioita" -kieli. Se kuitenkin eroaa puhtaalla oliolinjallaan eräistä yleisimmistä 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!

Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.

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 suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee- ja Kelmu.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.

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

Tapa, jolla 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+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki. Pääkehittäjänä on tällä hetkellä Markku Riekkinen, jonka lisäksi A+:aa ovat kehittäneet kymmenet Aallon opiskelijat ja muut.

A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen ovat luoneet Nikolai Denissov, Olli Kiljunen, Nikolas Drosdek, Styliani Tsovou, Jaakko Närhi ja Paweł Stróżański yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden kanssa.

Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.

Lisäkiitokset tähän lukuun

Stars-ohjelma 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...