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.
Muuta: Eräissä tämän luvun kohdissa on kaiuttimista tai kuulokkeista hyötyä. Aivan pakolliset ne eivät ole.
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
Option
it (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 Boolean
eja 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)
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
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.
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
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 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.
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: 2020 Your number is 4 digits long. Multiplying it by its length gives 8080.
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: 2020 Your number is 4 digits long. Multiplying it by its length gives 2020202020202020.
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 luvut merkkeinä kertolaskua varten.
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 match
illä.)
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.
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
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.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
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
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
-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äisoliotaSkyFiles
. - Sovelluksen käyttämä kansio on määritelty
StarryApp
-ohjelman alussa. Siellä on nyt valittu käyttööntest
-kansio, jonka voit jättää aluksi käyttöön, kunnes metodisi toimii. Vaihda sitten käyttöönnorthern
-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.
- Ohjelma kaatuu ajonaikaiseen virheeseen,
jos
- 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ä.
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 import
aten 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".
Vuoden 2020 lopussa julkaistava Scala-versio 3.0 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._
.
import
in 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ä import
aaminen
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ä import
aaminen 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:
String
ien manipulaatio vaikuttaisi kätevältä
Scalassa. Hyvä että on vakiona [peruskirjastossa
tarjolla] noin paljon metodeita.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.
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
jaString
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 ja Nikolas Drosdek 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.
Range
. Kyseessä on eräänlainen lukuja sisältävä muuttumaton alkiokokoelma, siis samantapainen kuin (luku)vektori.