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

Luku 5.4: Katsaus Scala-työkalujen sisään

Tästä sivusta:

Pääkysymyksiä: Kuinka Scala toimii? Mitä IntelliJ tekee huomaamatta puolestani? Miksi eräät laskutoimitukset toimivat hassusti?

Mitä käsitellään? Datan esittäminen (binaari)lukuina. Vähän tiedostoformaateista. Kääntäjä, virtuaalikone, tavukoodi ja konekieli. Roskankeruu. Lukuja kuvaavien tietotyyppien rajoituksia. Scala-työkaluja komentorivillä.

Mitä tehdään? Lähinnä luetaan. Jokunen minitehtävä.

Suuntaa antava työläysarvio:? Tunti. (Sisältää runsaasti valinnaista materiaalia, jonka parissa menee enemmänkin.)

Pistearvo: A5.

Oheismoduulit: Ei ole.

../_images/sound_icon.png

Muuta: Sisältää upotetun videon, joka on tosin täysin valinnaista asiaa.

../_images/person03.png

Johdanto: Scala-koodin vaiheet

Pidetään pieni tauko uusien käskyjen opettelussa. Tässä luvussa tutkimme, mitä vaiheita liittyy siihen, että ohjelmakoodi muuttuu konkreettiseksi toiminnaksi tietokoneessa. Seuraava esitys johdattelee aiheeseen:

../_images/mammuttikirja.png

Tämän luvun kuvitus on kunnianosoitus klassikkokirjalle.

Muun muassa IntelliJ-ympäristö huolehtii esityksessä mainituista vaiheista automaattisesti ja huomaamattomasti. IntelliJ’kin kyllä käyttää useita eri työkaluja, esimerkiksi kääntäjää ja virtuaalikonetta, joiden yhteistoiminta vain on siinä määrin saumatonta, ettei IntelliJ’n käyttäjän ole pakko ajatella käyttävänsä useaa työkalua. Nämä vaiheet onkin tähän mennessä kurssilla jätetty puhtaasti IDE:n huoleksi. Kuitenkin nyt perehdymme tähän aiheeseen hieman.

Miksi?

Jos kerran vaikkapa IntelliJ tekee asiat automaattisesti, niin miksi niistä pitäisi nyt oppia?

Ensinnäkin on huomattava, että ohjelmointia voi tarkastella useilla eri abstraktiotasoilla. Esimerkiksi olio-ohjelman suoritusta voi kuvata korkealla abstraktiotasolla olioiden viestintänä. Tämä perustuu matalammalla tasolla ohjelmointikielen käskyjen suorittamiseen peräjälkeen. Vielä matalammalla tasolla kyse on bitteihin liittyvistä operaatioista prosessoreissa.

../_images/mammoth_in_scala.png

Tietyllä abstraktiotasolla työskennellessäsi on avuksi, että ymmärrät jonkin verran myös alla olevasta tasosta. Abstraktion sisään katsomisesta on apua etenkin ongelmatilanteissa, kun käyttämäsi käskyt eivät toimikaan kuten halusit tai luulit.

Sama pätee ohjelmoijan apuohjelmiin. IDE:nkin käytössä on avuksi ymmärtää jotain sen sisäisestä toiminnasta.

Toinen hyvä syy on se, että "oikeassa maailmassa" kurssin ulkopuolella ohjelmoidaan muuallakin kuin IntelliJ’ssä. Työkalumme taustalla olevia perusperiaatteita on siis hahmotettava, jotta voit joustavammin vaihtaa työkaluja tarvittaessa.

Aiheeseen perehtyminen on perusteltua jo tietoteknisen yleissivistyksenkin vuoksi.

Ja tietysti siksikin, että joidenkin ohjelmoijien on kyettävä kehittämään työkaluja ohjelmoijien käyttöön. (Mutta se ei ole tämän luvun tai kurssin aihe.)

Katsotaan siis, kuinka Scala toimii.

Erillisiä työkaluja

Yksittäisiä Scala-koodin työstämisen vaiheita on helpompi hahmottaa, kun käytämme IDE:n sijaan irrallisia työkaluja.

Scala-kielen viralliseen työkalupakkiin (ks. http://www.scala-lang.org/) kuuluu joukko toisistaan erillisiä työkaluohjelmia:

  • Kääntäjäohjelma nimeltä scalac (Scala compiler).

  • Virtuaalikoneen käynnistävä ohjelma, jonka nimi on yksinkertaisesti scala.

  • Scaladoc-työkalu dokumenttien tuottamiseen (luku 3.2).

  • Muita osia, joita ei käsitellä tässä.

Seuraava esitys näyttää, miten luodaan ja ajetaan pikkuruinen Scala-ohjelma käyttäen editoria, kääntäjää ja virtuaalikonetta kutakin erikseen.

Onko komentorivi tuttu?

Esityksen seuraamisessa auttaa, jos työskentely komentoriviympäristössä on sinulle edes vähän tuttua — siis tekstikäskyjen käyttö esimerkiksi Windowsin Command Promptissa tai PowerShellissä tai Linuxin Terminal-ikkunan komentotulkissa. Jos ei ole, löydät aiheesta ohjeita netistä. Esityksen pääpointit avautunevat kyllä kokemattomallekin.

Alempana on kerrottu lisää näistä työvaiheista ja -kaluista sekä siitä, miten vaiheet toteutuvat IntelliJ’ssä.

Koska kyse on tiedon muokkaamisesta eri muotoihin, tarvitset vähän pohjatietoja siitä, missä muodoissa tietoa voidaan tietokoneessa esittää. Aloitetaan siis biteistä.

Luvut bitteinä, data lukuina

Binaariluvut eli binääriluvut muodostuvat ykkösistä ja nollista. Yhtä binaariyksikköä — nollaa tai ykköstä — kutsutaan bitiksi (bit eli binary digit).

Esimerkiksi kymmenjärjestelmän lukuja voi kuvata bitteinä seuraavasti:

14kymmen = 8 + 4 + 2 = 1*23 + 1*22 + 1*21 + 0*20 = 1110binaari

5kymmen = 4 + 1 = 1*22 + 0*21 + 1*20 = 101binaari

Tietokone on digitaalinen järjestelmä. Sen muistissa kaikki data on esitetty binaarilukuina tai muuten bittijonoina.

Kuvat ja teksti bitteinä

Kuva on väripisteiden eli pikselien yhdistelmä. Pisteiden värisävyt voidaan ilmaista lukuina ja nämä luvut bitteinä.

Teksti puolestaan muodostuu kirjoitusmerkeistä. Voidaan sopia, että kutakin erilaista merkkiä vastaa yksiselitteisesti jokin luku. Scalassa merkkijonojen kuvaamiseen käytetty Unicode (http://www.unicode.org/) on eräs tällainen merkistösopimus eli -standardi. Alla on esimerkkejä Unicode-standardin määräämistä lukuarvoista eräille merkeille:

Unicode-arvo

Merkki

32

välilyönti

38

&

48

0

49

1

65

A

66

B

97

a

98

b

122

z

945

α (alfa)

Kun meillä on merkille lukuarvo, voimme esittää merkin bitteinä (tavalla tai toisella, esim. suora binaarilukuesitys kuten yllä). Merkkijono voidaan siis kuvata jonona binaarilukuja.

Viittaukset bitteinä

Viittauksen käsite on jo tullut tutuksi: pieni datajyvänen, jonka perusteella voidaan käsitellä toista dataa (eli jotakin oliota). Kurssimateriaalin animaatioissa olet nähnyt olioihin kohdistuvia viittauksia, joita on esimerkiksi tallennettu muuttujiin ja välitetty parametreiksi. Pohjimmiltaan Scala-ohjelmassa esiintyvä viittauskin on bittijono (binaariluku), joka tavalla tai toisella identifioi tietokoneen muistista kohdan, josta löytyvät tietyn olion tiedot.

Ohjelma bitteinä

Ei vain ohjelman käsittelemä data vaan myös itse ohjelma pidetään tallessa bitteinä, yleensä tiedostoon tallennettuna. Itse asiassa: ohjelmakin on dataa ja ohjelmat voivat käsitellä ohjelmia!

Scalan tapauksessa ja muutenkin useimmiten ohjelman lähdekoodi on ns. plain text -muodossa, joka koostuu vain kirjoitusmerkkien bittikuvauksista. Kääntäjällä ohjelma muutetaan toiseen muotoon, ja samalla myös tapa, jolla ohjelma on kuvattu bitteinä, muuttuu.

Mutta mitä bitit oikeasti ovat?

Miten kone on saatu "ymmärtämään" bitit (1 ja 0)?

Olen aina miettinyt mitä "ykköset ja nollat" oikein tarkoittavat.

Ennen vanhaan bittien tallennusmuoto oli helpommin havaittavissa kuin nykyisissä tietokoneissa, joissa niiden fyysinen muoto on kauempana arkitodellisuudesta.

../_images/jm_jacquard.png

J.M. Jacquard

Vielä 1970-luvulla tietokoneita ohjelmoitiin aivan yleisesti reikäkorteilla (punched card) eli pahviläpysköillä, joihin tehtyjen reikien sijainnit määrittivät ohjelman. Saatettiin esimerkiksi määritellä, että reikä tai sen puuttuminen tietyssä kohdassa määräsi yhden bitin ja usea bitti sitten yhdessä tietyn käskyn.

Reikäkortteja käytettiin jo Joseph Marie Jacquardin vuonna 1801 keksimien kangaspuiden ohjelmointiin. Jacquardin kangaspuita pidetään ensimmäisten varsinaisten tietokoneiden esiasteena.

Nykyaikaisissa tietokonelaitteissa yksittäinen bitti voidaan pitää tallessa esimerkiksi sähkövarauksena, joka tietyssä komponentissa joko on tai ei ole. Tai pikkuruisena kuoppana tai kuopattomuutena DVD-levyn pinnassa. Tai magneettisesti. Tapoja on monia, ja bittien fyysinen muoto vaihtelee laitteesta toiseen.

Vuonna 2016 julkaistiin tutkimus uudesta muistityypistä, joka käyttää yksittäisten bittien tallentamiseen yksittäisiä klooriatomeita kuparipinnalla. Artikkeli World’s Smallest Hard Drive Writes Data Atom-By-Atom kertoo näin: "When a chlorine atom is on top with a hole beneath it, it’s a one, the binary digit and when it’s the other way around it’s a zero — thus creating a hard drive." Bittien varastoimiseksi yksittäisiin atomeihin on saatu edistysaskelia sittemminkin.

Tulevaisuudessa biteistä kubiteiksi?

Jos haluat ravistella aivojasi, selvitä internetin avulla, mitä ovat kvanttitietokoneet (quantum computer) ja miten ne käyttävät kubitteja (qubit) bittien sijaan.

Palataan ohjelmoijan aputyökaluihin.

Tekstieditoreista

../_images/mammutti_ja_sorsa.png

Plain text -tiedosto — tai lyhyesti sanoen vain tekstitiedosto — on tiedosto, johon on tallennettu bittien sarja, jonka pätkät vastaavat lukuja, jotka puolestaan vastaavat kirjoitusmerkkejä jonkin tietyn standardin mukaisesti. Tekstitiedostoja voi lukea ja muokata sovelluksella, joka on ohjelmoitu tulkitsemaan tiedostoon tallennetut bitit merkeiksi. Tällaisia ohjelmia ovat esimerkiksi IntelliJ’n editori, Emacs, Windowsin Notepad ja Macien TextEdit.

Muunlaiset tiedostoformaatit noudattavat eri standardeja kuin tekstitiedostot. Esimerkiksi Excel-taulukon, GIF-kuvatiedoston tai Word-dokumentin käsittelyyn tarvitaan sellainen ohjelma, joka osaa käsitellä tallennetut bitit tiedostoformaatin mukaisella logiikalla.

Voit tutkia itsekin

Jos haluat, voit avata esimerkiksi kuvatiedoston Notepadissä (tai muun ei-tekstitiedoston jossakin muussa tekstitiedostoille tarkoitetussa editorissa). Koska kuvatiedoston sisältämät bitit eivät kuvaa kirjoitusmerkkejä, ei Notepad osaa tulkita niitä oikein. Se tulkitsee bitit kuten parhaiten osaa eli merkkeinä. Tällöin editorissa näkyy "sotkua".

Koodia voi IntelliJ’n sijaan kirjoittaa missä tahansa muussakin tekstitietoja tallentavassa ohjelmassa. Esimerkiksi Notepad on mahdollinen koodausympäristö; se ei vain muilta ominaisuuksiltaan tue ohjelmointia hyvin, joten juuri kukaan ei halua sitä ohjelmointiin käyttää. On olemassa monia erillisiä tekstieditoreja, joissa on ohjelmakoodin editointia helpottavia ominaisuuksia.

IntelliJ’llä kirjoitetut kooditiedostot tallennetaan koneen tiedostojärjestelmään aivan samaan tapaan kuin muidenkin sovellusten tuottamat tallenteet. Kooditiedostot löytyvät IntelliJ-moduulien kansioista pakkausten mukaan nimetyistä alikansioista.

Konekielestä

Scala on korkean tason ohjelmointikieli (high-level language). Tämä tarkoittaa sitä, että kielen abstraktiotaso on korkea eivätkä sen ilmaisut ole suoraan sidoksissa esimerkiksi binaarilukuihin ja tietokoneen fyysisten osien toimintaan.

Minkään normaalin tietokoneen prosessori ei osaa Scalaa eikä mitään muutakaan korkean tason ohjelmointikieltä. Prosessorit aikaansaavat tietokoneiden toiminnan suorittamalla suuria määriä alkeellisia käskyjä, jotka on kirjoitettu konekielellä (machine code tai machine language).

Konekielen käskyt ovat bittijonoja, jotka vastaavat matalan abstraktiotason toimintoja kuten binaariluvuilla laskemista, bittien vertailua, nollien vaihtamista ykkösiksi ja muuta hyvin alkeellista toimintaa. Eri prosessorimalleilla on omat konekielensä, joissa erilaiset bittijonot vastaavat erilaisia käskyjä.

Konekieliohjelmointi on työlästä ja altista virheille. Hyvin suurten ohjelmistojen laatiminen konekielellä on käytännössä mahdotonta; voit verrata tätä vaikkapa täyskokoisen pilvenpiirtäjän rakentamiseen Lego-palikoista.

Kääntäminen, välikielet ja tavukoodi

Ohjelmoijan laatima lähdekoodi pitää muuntaa muotoon, joka soveltuu tietokoneen ajettavaksi.

../_images/mammutti_kaantaa_sorsan.png

Perinteisesti on erotettu kaksi tapaa muuntaa korkean tason kieltä suoritettavaan muotoon. Kääntämiseksi (compiling) sanotaan ohjelman muuntamista ennen kuin se määrätään suoritettavaksi. Tulkkaamiseksi (interpreting) taas sanotaan ohjelman suorittamista siten, että muunnos konekieleksi tehdään vähitellen suorittamisen yhteydessä.

Ensimmäinen Scala-ohjelmien käsittelyyn liittyvä vaihe on kääntäminen.

Kääntäminen on systemaattinen prosessi, ja se voidaan tehdä koneellisesti apuohjelmalla. Kääntämisen tuloksena syntyy koneen luettavaksi tarkoitettua koodia (ns. "binäärejä"). Kääntäessä samalla varmistetaan etukäteen, että ohjelma on kieliopin mukainen.

IntelliJ’llä on käytössään kääntäjäapuohjelma. Normaaliasetuksilla IntelliJ kääntää Scala-lähdekoodia automaattisesti "tarpeen mukaan" eli esimerkiksi kun käynnistät ohjelman, jota on muokattu ja joka täytyy siis kääntää uudelleen. Olet jo oppinut erikseen pyytämään käännöksen IntelliJ’n Build-valikosta tai F10:llä. (Kääntäminen on tämän "rakentamisen"→build keskeinen osa.)

Osa virheilmoituksista syntyy juuri käännöstyön yhteydessä. Aiemmissa luvuissahan on ollut puhetta käännösaikaisista virheilmoituksista.

Monia ohjelmointikieliä käännetään suoraan aidoksi konekieleksi ja annetaan käyttäjille konekielisessä muodossa. Esimerkiksi Windowsista tutut .exe-tiedostot sisältävät konekieltä, joka on yleensä saatu jostakin korkean tason kielestä kääntämällä. Kuitenkaan tämä ei ole ainoa vaihtoehto:

Välikielet ja tavukoodit

Viime vuosikymmeninä yleistynyt vaihtoehto suoraan aidoksi konekieleksi kääntämiselle on käyttää välikieltä (intermediate language). Syistä lisää alempana; perusajatus on seuraava:

  • Käytetään kääntäjätyökalua, jolla muunnetaan lähdekoodi välikielelle ennen ohjelman ajamista.

  • Käytetään ajamisessa apuna sellaista apuohjelmaa, joka osaa muuntaa välikielisen ohjelman konekieleksi. Tämä muunnos tehdään ohjelma-ajon yhteydessä, ja tietokone suorittaa konekielisen version.

Esimerkiksi Scala-ohjelmat käännetään aluksi välikielelle.

Joistakin välikielistä käytetään nimitystä tavukoodi (bytecode), niin myös siitä välikielestä, joksi Scala-ohjelmia tavallisesti käännetään. Scala-kääntäjä tuottaa class-päätteisiä luokkatiedostoja, jotka sisältävät tavukoodia. Yhdestä .scala-tiedostosta syntyy kääntäessä yksi tai useampia .class-tiedostoja.

Etsi omat tavukooditiedostosi

Selaa oman kurssikansiosi sisältöä. Siellä on out-kansio, jonka alla on production-kansio. Sen sisältä löytyvät IntelliJ’n tuottamat käännetyt versiot kunkin moduulin Scala-koodista class-päätteisissä tavukooditiedostoissa.

Tavukoodi koostuu varsin alkeellisista käskyistä ja muistuttaa konekieltä. Konekielen tavoin ja lähdekoodista poiketen tavukoodi ei ole tarkoitettu ensisijaisesti ihmisen luettavaksi tai muokattavaksi. class-päätteiset tavukooditiedostot eivät ole tekstitiedostoja, eivätkä tekstieditorit osaa niiden sisältöä mielekkäällä tavalla näyttää.

../_images/notepad_class.png

Eräs tavukooditiedosto tarkasteltuna Notepad-tekstieditorissa, joka on tähän tarkoitukseen kelvoton.

Tavukooditiedoston hyödyntämiseen tarvitaan siihen erikoistunut ohjelma. Tärkein tällainen ohjelma on virtuaalikone.

Virtuaalikoneet ja JVM

../_images/mammoth_virtual.png

Sanalla virtuaalinen (virtual) on monia merkityksiä, muiden muassa nämä:

  • keinotekoinen,

  • matkittu,

  • simuloitu,

  • ei todellinen vaikka todellista muistuttaakin,

  • ikään kuin — vaikkei varsinaisesti tai virallisesti — tietynlainen.

Virtuaalikone on tietokoneohjelma, joka toimii ikään kuin tietokone olematta kuitenkaan oikeasti fyysinen tietokone.

Kun käsket IntelliJ’tä ajamaan ohjelman, vaikuttaa pinnallisin puolin siltä kuin IntelliJ ottaisi Scala-kooditiedoston ja ajaisi sen. IntelliJ ei kuitenkaan oikeastaan itse aja ohjelmaasi, vaan käynnistää kulissien takana apuohjelman — virtuaalikoneen, joka huolehtii ohjelman suorittamisesta.

IntelliJ antaa virtuaalikoneelle ajettavaksi tavukoodiksi käännetyn version kirjoittamastasi Scala-lähdekoodista. Se myös välittää virtuaalikoneelle tiedon siitä, mistä kohdasta ohjelmaa kuuluu lähteä suorittamaan (eli missä ensiksi suoritettava koodi on). IntelliJ toimii siis samaan tapaan kuin komentorivin scala Ave.scala -käsky yllä näkemässäsi esityksessä.

Käyttämäsi virtuaalikone on ikään kuin tietokone, jonka prosessori osaa suorittaa tavukoodikäskyjä; sen virtuaalista konekieltä on tavukoodi. Virtuaalikone määrittää, mitä todellisia konekielikäskyjä kukin tavukoodikäsky vastaa ja välittää ne todellisen tietokoneen suoritettavaksi. Käytännössä siis kyseinen ohjelma tulee ajetuksi.

Virtuaalikone hallinnoi ohjelman ajamista. Se huolehtii ohjelman suorituksen etenemisestä ja muistiresurssien hallinnasta. Ne lukuisat animaatiot ohjelmien suorituksesta, joita olet kurssimateriaalissa nähnyt, kuvaavat juuri eräitä virtuaalikoneen vastuualueelle kuuluvia seikkoja: kutsupinon kehysten luominen ja poistaminen, olioiden luominen muistiin ja niin edelleen.

Yleisintä Scala-ohjelmien ajamiseen käytettyä virtuaalikonetta sanotaan JVM:ksi. Tämä on lyhenne sanoista Java Virtual Machine eli Java-virtuaalikone.

Hetkinen! Java-virtuaalikone?!

../_images/mammoth_in_java.png

Eikö Java ole erään ihan toisen ohjelmointikielen nimi?

Onhan se, mutta silti Scala-ohjelmia ajetaan Java Virtual Machine -nimisellä apuohjelmalla.

Java-virtuaalikone on ohjelma, jolla voi ajaa tietynlaiseksi tavukoodiksi käännettyjä ohjelmia. Alun perin kyseinen tavukoodityyppi kehitettiin Java-kielistä ohjelmointia varten, ja siksi virtuaalikonetta sanotaan JVM:ksi. Kuitenkin tuota virtuaalikonetta voi käyttää minkä tahansa sellaisten ohjelmien ajamiseen, jotka on käännetty sen kanssa yhteensopivaksi tavukoodiksi, riippumatta siitä, millä kielellä lähdekoodi on kirjoitettu.

Java-virtuaalikoneella voi siis ajaa vaikkapa Scala-ohjelmia, jos ne vain on käännetty sopivalla kääntäjällä .class-tiedostoiksi. Nykyään JVM on suosittu alusta paitsi Javalle myös sitä kehittyneemmille ohjelmointikielille, joista Scala on yksi esimerkki.

Myös yllä komentoriviesimerkissä annettu scala-käsky käynnisti nimestään huolimatta nimenomaan Java-virtuaalikoneen. Käsky ei olisi toiminut, jos tietokoneelle ei olisi asennettu Scala-työkalujen lisäksi myös Java-virtuaalikonetta.

Scala, Java, JavaScript

Käytetyin Scala-toteutus, jota käytämme tälläkin kurssilla, on rakennettu juuri "Javan päälle". Käyttämiemme Scala-kielen kirjastojen toteutuksessa on käytetty apuna olemassa olevia Java-kielen laajoja kirjastoja. Onkin niin, että Java paistaa läpi Scala-ohjelmoinnissa sieltä täältä. Olet ehkä kiinnittänytkin huomiota esimerkiksi seuraaviin asioihin.

  • Kurssimateriaalin ohjeissa IntelliJ’n ja Scalan asentamiseksi ohjeistetaan nappaamaan netistä Scalan lisäksi myös Java.

  • Kun käynnistät Scala-sovelluksen tai REPLin IntelliJ’ssä, ruudulla näkyy teksti, jossa mainitaan Java.

  • Luvussa 4.2 saimme ilmoituksen NullPointerException-virheestä. Tarkemmin sanoen virheilmoituksessa luki java.lang.NullPointerException, mikä johtuu siitä, että kyseinen virhetyyppi on määritelty Java-kielen peruskirjastossa ja Scala-kirjastot hyödyntävät sitä.

Scala-kielen määrittely ei kuitenkaan sinänsä ole Java-riippuvainen eikä estä muunlaisia toteutuksia Scala-kirjastoille ja -työkaluille. Sellaisia onkin kehitetty. Tärkein vaihtoehtoinen toteutus on Scala.js, joka nojautuu JavaScript-kieleen ja mahdollistaa Scala-ohjelmien ajamisen web-selaimessa.

Perusteluja välikielelle ja virtuaalikoneelle

Mitä hyötyä on siitä, että lähdekoodi käännetään välikielelle, joka sitten suoritetaan virtuaalikoneen avulla? Eikö olisi suoraviivaisempaa ja helpompaa kääntää suoraan konekielelle?

Vertaillaan eräillä keskeisillä kriteereillä:

Laatukriteeri

Välikieli & virtuaalikone

Suoraan konekieleksi

siirrettävyys (portability)

Välikielelle käännettyjä ohjelmia voi käyttää missä tahansa ympäristössä, jossa on tarjolla kyseiseen ympäristöön sopiva virtuaalikone. Esimerkiksi JVM:lle käännettyjä Scala-ohjelmia voi suorittaa millä tahansa alustalla, jolle on tehty JVM-toteutus. (Tämä on osin myös haitta: virtuaalikone on oltava.)

Ohjelmia voi suorittaa vain tietynlaisessa ympäristössä, jossa kyseinen konekieli toimii. Eri ympäristöihin on tuotettava erilaiset käännökset ohjelmasta.

yhdisteltävyys (interoperability)

Samalle välikielelle voidaan kääntää useita kieliä. Tämä mahdollistaa eri kielillä kirjoitettujen ohjelman osien yhteistoiminnan. Voidaan myös käyttää yhden kielen kirjastoja toisesta kielestä. Esimerkiksi JVM:ssä ajettavista kielistä voi käyttää Java-kielen moninaisia kirjastoja.

Eri kielillä kirjoitettujen ohjelmakomponenttien sovittaminen yhteen voi vaatia erityisjärjestelyjä.

suoritustehokkuus (efficiency)

Välikielen käytöstä seuraa jonkin verran tehokkuushaittoja kuten ohjelmien hidastumista. Haittoja voidaan lieventää erilaisilla optimoinneilla. Kun tietty virtuaalikone on laajasti käytetty (kuten JVM on), niin siihen tehdyistä optimoinneista hyötyvät usealla eri kielellä lähdekoodinsa kirjoittavat ohjelmoijat.

Kääntäjäohjelman voi laatia nojaten nimenomaisen lähdekieli–kohdekieli-parin ominaisuuksiin. Käännöksenä saadaan tehokkuusoptimoitua konekieltä. Riippuu tilanteesta, onko näin saavutettu tehokkuusetu käytännössä merkityksellinen.

Virtuaalikoneet ovat nykyään aiempaa suositumpia, ja kehitys näyttäisi jatkuvan samaan suuntaan.

Tavukoodin suorittamisesta

Joidenkin erikoistietokoneiden laitteisto tukee JVM-tavukoodin suorittamista "suoraan". Useimpien kuitenkaan ei, vaan tavukoodi muunnetaan ensin konekieleksi.

Roskaa virtuaalikoneessa

Jotkin oliot luodaan väliaikaiseen tarkoitukseen. Olio voi olla käytössä esimerkiksi vain yhdellä ohjelman rivillä. Esimerkki: mitä tapahtuu luodulle puskurioliolle tällaisen tulostuskäskyn suorittamisen jälkeen?

println(Buffer(5, 2, 7, 32, 9)) // Huom. viittausta puskuriin ei jää mihinkään talteen
// tänne muita käskyjä, jotka eivät käytä äsken luotua puskuria

Olioita, jotka "leijuvat" muistissa ilman, että niihin pääsee ohjelmasta käsiksi, kutsutaan roskaksi (garbage). Roska vain vie muistitilaa turhaan.

Ohjelman tulisi vapauttaa roskalle varattu muisti muuhun käyttöön jo ohjelma-ajon aikana. Aina näin ei kuitenkaan käy. Klassinen bugityyppi on muistivuoto (memory leak): ohjelma varaa itselleen käyttöön tilaa tietokoneen muistista, mutta ei vapautakaan varaamaansa muistia myöhemmin, kun ei enää tarvitse sitä. Vähitellen vuotava ohjelma saattaa varata aina vain enemmän muistia kunnes resurssit ehtyvät ja ohjelma kaatuu.

Muistinhallinnasta

Joissakin ohjelmointiympäristöissä ohjelmoija huolehtii muistiresurssien vapauttamisesta nimenomaisin käskyin. Esimerkiksi C++ -ohjelmointikielellä olion voi luoda seuraavasti. Tämä käsky, joka varaa olion tiedoille muistia ja alustaa olion, muistuttaa aika lailla Scalaa:

JokuLuokka* testi = new JokuLuokka();    // Tämä ei ole Scalaa vaan C++:aa.
var testi = JokuLuokka()                 // Tämä on vastaava Scala-käsky.

Kun oliota ei enää tarvita, C++ -ohjelmoija voi poistaa testi-muuttujan osoittaman olion muistista näin:

delete testi;                            // Tämä ei ole Scalaa vaan C++:aa.

Näin tarpeeton olio ei jää roskaksi tietokoneen muistiin.

Emme ole oppineet deleteä vastaavaa Scala-käskyä. Eikä sellaista olekaan.

Jos Scala-ohjelma on käynnissä pitkään, ja uusia olioita luodaan lisää, niin viekö se aina vain enemmän ja enemmän muistia?

Ei:

Automaattinen roskankeruu

../_images/mammoth_collects_garbage.png

Java-virtuaalikoneen toimenkuvaan kuuluu automaattinen roskankeruu (garbage collection). Virtuaalikone valvoo sitä, mitkä luoduista olioista ovat roskaa, ja vapauttaa roskalle varatun muistin muuhun käyttöön. Ohjelmoijan ei tarvitse puuttua asiaan.

JVM katsoo roskaksi kaikki sellaiset oliot, joihin osoittavia viittauksia ei ole enää mihinkään tallennettuna.

Automaattinen roskankeruu ehkäisee ison osan vuotovaarasta. Silti esimerkiksi Scala-ohjelmakaan ei ole immuuni muistivuodoille. Esimerkki: Ohjelma säilöö puskuriin viittauksia, jotka osoittavat olioihin. Jos näitä olioita ei poisteta puskurista, kun niistä on tullut ohjelman toiminnan kannalta tarpeettomia, niin JVM:n roskankerääjä ei siivoa niitä pois. Kuitenkin jos puskuriolio itse on roska, eikä siihen tallennettuihin olioihin viitata muualta kuin puskurista, niin myös puskurin sisältö tulkitaan roskaksi ja tulee kyllä automaattisesti siivotuksi. On ohjelmoijan vastuulla laatia ohjelma siten, että se ei jätä turhia viittauksia tallennetuiksi muuttujiin tai kokoelmiin.

Lisämateriaalia: oman ohjelman jakaminen

Miten voin jakaa Scalalla kirjoitetun sovellukseni toisten käytettäväksi?

Vaihtoehtoja on monta. Niistä lyhyesti alla ja lisää keväisellä Ohjelmointistudio 2 -kurssilla.

  1. Jos käyttäjän koneella jo on Java-virtuaalikone (JVM) peruskirjastoineen, niin helpohko vaihtoehto on paketoida ohjelma suoritettavaksi JAR-tiedostoksi (executable JAR). Se on tiedosto, johon paketoidaan sekä lähdekoodista käännetyt .class-tiedostot että Scalan vaatimat kirjastot. Lisäksi tiedostoon kirjataan tieto siitä, mistä ohjelma lähtee käyntiin. Suoritettavien JAR-pakettien muodostaminen onnistuu esimerkiksi SBT-työkalulla (Simple Build Tool).

  2. ../_images/scala_js.png

    Scala.js:n logo.

    Scala.js. Scalaa voi nyttemmin kääntää JVM-tavukoodin sijaan myös JavaScript-kieleksi, joka on nimestään huolimatta ihan eri asia kuin Java ja jota nykyaikaiset web-selaimet osaavat suorittaa. Tämä onnistuu Scala.js-työkalulla.

  3. Natiivikääntäjät tuottavat tavukoodin sijaan tiettyyn ajoympäristöön sopivia käännöksiä; eri ympäristöihin täytyy tuottaa omat versionsa. Käännöksen mukaan voidaan liittää kirjasto, joka huolehtii mm. roskankeruusta. Scalaa ei useimmiten käännetä natiivikääntäjällä, mutta sekin onnistuu: Scala Native on tuore Scala-natiivikääntäjä. Se perustuu LLVM-kääntäjätyökaluihin.

  4. "Java-wrapper" on ohjelma, jossa käännetty Java- tai Scala-koodipaketti on "kääritty" pienen suoritettavan ohjelman mukaan. Ajettaessa apuohjelma joko käyttää järjestelmään jo asennettua Java-virtuaalikonetta sovelluksen suorittamiseksi tai asentaa tarvittavan ympäristön.

  5. Java Web Start on tekniikka, jossa ohjelman käynnistys tapahtuu käytännössä klikkaamalla linkkiä web-sivulla. Java Web Start -työkalu huolehtii ohjelman ja sen vaatiman Java-virtuaalikoneen asennuksista ja lopulta suorittaa ohjelman.

Osaa yllä mainituista vaihtoehdoista on esitelty tarkemmin esimerkiksi seuraavissa lähteissä:

Ole nettilähteitä tutkiessasi tarkkana, koska — kuten sanastosivummekin kertoo — "Java"-sanalla voidaan viitata joko Java-kieleen tai (Scala-ohjelmoijan kannalta relevantimmin) JVM:ään ja siihen liittyviin teknologioihin, eivätkä kirjoittajat aina ilmaise selvästi, kumpaa tarkoittavat.

Tärkeä maininta vielä loppuun: yleisiä ovat myös sellaiset web-sovellukset, joiden sisäisestä toimintalogiikasta huolehtii palvelinkoneessa ajettava ohjelma, vaikka käyttöliittymä toimii loppukäyttäjän omassa web-selaimessa. Scalaa käytetään myös tällaisten palvelinsovellusten laatimiseen, jolloin riittää, että tarvittavat työkalut (JVM yms.) on asennettu palvelinkoneelle eikä loppukäyttäjälle. Web-palvelimien ohjelmoinnista kerrotaan jatkokursseilla.

Lisämateriaalia: virtuaalikoneet ja kääntäminen

JVM eri ympäristöissä

Miten laiteriippuvainen JVM siis oli? Löydänkö sellaisen PC:stä, mäkistä ja Mersun E-sarjan autoista?

JVM-toteutuksia on olemassa erittäin moniin erilaisiin ympäristöihin. Niitä löytyy paitsi erilaisista läppäreistä ja pöytäkoneista myös pienistä sulautetuista laitteista sekä palvelinfarmeista, joilla pyöritetään isoja verkkosivustoja. Vähäresurssisissa sulautetuissa järjestelmissä käytetään usein kevennettyä Java-versiota, mutta toiset järjestelmät pyörittävät samaa Java-versiota kuin pöytäkoneetkin.

Elikkä JVM:n avulla voisi vaikka suorittaa Scala- tai Java-ohjelmia parkkilippukoneella?

Kyllä. Parkkilippukone onkin Javan takapiruna toimivan Oracle-yhtiön mainoksissa usein toistunut esimerkki.

Java Technology can be found in a broad spectrum of products across a diverse set of industries that produce anything from RFID readers to parking meters to ATMs to in-flight video systems to POS terminals to wearable systems (just to name a few) — — across a large number of hardware and OS platform configurations.

—Oraclen mainosteksti

JVM ei ole ainoa virtuaalikonetyyppi, jolla voi suorittaa Java- tai Scala-kielellä kirjoitettuja ohjelmia. Mm. Android-puhelimissa on JVM-standardista poikkeava virtuaalikonetoteutus.

Kaikissa yhteyksissä JVM:n kaltainen virtuaalikone ei ole hyvä ratkaisu. Tässä yksi esimerkki:

Aikoinaan opin, ettei Javaa saa käyttää ydinvoimaloissa tai muissa suurta turvallisuutta vaativissa kohteissa. Syy liittyi jotenkin roskien keruuseen. Koskeeko sama Scalaa?

Kyllä. Taustalla toimiva roskankeruu vie silloin tällöin aikaa — vain hetken, mutta silti. Tämä yhdessä muiden seikkojen kanssa tekee vaikeaksi tai mahdottomaksi ennustaa erittäin tarkasti, kuinka kauan tietyn ohjelma-ajon vaiheen suorittaminen kestää. Ydinvoimalan kaltaisissa elintärkeissä järjestelmissä (life-critical system) tällainen ratkaisu ei kelpaa.

Elintärkeiden järjestelmien tarpeisiin on omia käyttöjärjestelmiään ja ohjelmointikieliään (ks. esim. real-time operating system, real-time computing, SPARK-ohjelmointikieli). Niissä käytetään muodollisempia ja varmempia työskentelytapoja kuin tavanomaisessa ohjelmointityössä (ks. esim. model checking, formal verification).

Voiko käännettyä ohjelmaa kääntää takaisin lähdekoodiksi?

Periaatteessa onnistuu ja osin ehkä käytännössäkin.

Kun käännetään korkean tason kielestä konekieleksi, osa lähdekoodin sisältämästä informaatiosta menetetään. Esimerkiksi muuttujien nimiä, kommentteja ja korkean tason kielen muita rakenteita, jotka jäsentävät kokonaisuutta, voidaan menettää.

Takaisinpäin käännetty koodi voi olla erittäinkin vaikeaselkoista, mikä riippuu käännettävästä ohjelmasta, ohjelmointikielestä ja kääntäjistä. Koodi ei välttämättä ole ymmärrettävissä niin helposti, että yritys olisi käytännössä vaivan arvoista.

On olemassa työkaluja, joilla takaisin päin kääntämistä voi tieten taiten hankaloittaa.

Hakusanoja: decompiler, reverse engineering, code obfuscation.

Kääntäminen vs. tulkkaaminen

Tulkkauksen ja kääntämisen raja on hämärä, varsinkin nykyään. Aina ei ole helppoa — tosin ei ehkä tarpeellistakaan — sanoa kummasta on kysymys. Välikielten ja virtuaalikoneiden käyttö sotkee asiaa osaltaan. Lisäksi soppaa hämmentää ajonaikainen kääntäminen (just-in-time compilation eli JIT), josta voit hakea lisätietoa internetistä vapaaehtoisena lisäharjoituksena.

Joihinkin ohjelmointiympäristöihin ei ollenkaan kuulu rajanvetoa lähdekoodin kirjoittamisen, kääntämisen/tulkkaamisen ja ajamisen välillä. Yksi esimerkki tästä on tuttu Scala-REPL.

JVM:n tavukoodin tutkiminen

(Tämä vapaaehtoinen tehtävä sopinee parhaiten opiskelijoille, jotka ovat jo ohjelmoineet ennen tätä kurssia. Tehtävän aiheeseen palataan Ohjelmointi 2 -kurssilla.)

Käännetyn Scala-ohjelman sisältämiä tavukoodikäskyjä voi tutkia javap- ja scalap-nimisillä apuohjelmilla. Selvitä internetistä, miten näitä apuohjelmia käytetään, ja tutki jotakin käännettyä Scala-luokkaa niillä.

Eri virtuaalikoneita

Tutki internetin avulla, mitä muita yleisesti käytettyjä virtuaalikoneita ja välikieliä on kuin JVM tavukoodeineen. Selvitä esimerkiksi, mikä on CLR sekä miten JavaScript-kieltä nykyään käytetään muiden ohjelmointikielten välikielenä WWW-selaimissa.

Lisämateriaalia: avoin lähdekoodi

Avoin lähdekoodi

Avoin lähdekoodi (open source) on jälleen yksi termi, jonka täsmällinen merkitys vaihtelee puhujasta riippuen. Keskeistä joka tapauksessa on, että ohjelman lähdekoodi on julkisesti saatavilla ja että sallitaan muidenkin kuin alkuperäisen tekijän hyödyntää sitä omissa ohjelmistoprojekteissaan.

On paljon erilaisia avoimen lähdekoodin lisenssejä, joilla koodin voi julkaista. Lisenssit eroavat toisistaan esimerkiksi sen suhteen, sallitaanko kaupallinen käyttö, ja vaaditaanko, että hyödyntäjä myös vastaavasti julkaisee oman alkuperäistä versioivan tai muuten käyttävän ohjelmansa.

Lähdekoodin avoimuus voi helpottaa ohjelmistojen kehittämistä eri tahojen — firmojen ja/tai yksityishenkilöiden — yhteistyössä. Lähdekoodin parissa työskentelevä yhteisö voi esimerkiksi ideoida ja toteuttaa muutoksia koodiin sekä arvioida yhteisön jäsenten tuotoksia laadun varmistamiseksi.

Avoimeen lähdekoodiin liittyy myös arvoja ja ideologioita. Lisää niistä ja teemasta muutenkin voi lukea esimerkiksi Wikipediasta.

Avoimen lähdekoodin projekteja on vähintään satoja tuhansia. Kuuluisin niistä lienee suomalaislähtöinen Linux-käyttöjärjestelmä. Muita esimerkkejä ovat ilmaisten ohjelmistojen puolesta liputtavan Mozilla-järjestön selain Firefox ja sähköpostiohjelma Thunderbird. Ja IntelliJ. Ja Scala.

Kunkin Scala APIn luokkaa kuvaavan Scaladoc-sivun alkupäässä on Source-linkki, jonka kautta voit tarkastella kyseisen luokan koodia. Scalan lisenssi on hyvin salliva.

Lisämateriaalia: eettisiä kysymyksiä

Ohjelmoinnin etiikkaa

Edellä mainittiin avoin lähdekoodi. Se on yksi monesta teemasta, joita käsitellään alle upotetussa konferenssiesitelmässä. Videon loppupuolella esitellään eräs avoimen lähdekoodin hanke, ja yleisemminkin ottaen videolla oleva esitys toimii esimerkkinä siitä, millaisiin tavoitteisiin jotkut avoimen lähdekoodin kannattajat pyrkivät.

Muita esityksessä esille nousevia teemoja ovat: ohjelmoijan eettinen vastuu tuottamistaan ohjelmista; liike-elämän, ohjelmoinnin ja maailman parantamisen yhteen sovittaminen; joukkovalvonnan merkitys niillekin, joilla ei ole mitään salattavaa; turvallinen sähköposti ja miten sen leviämistä voi edistää.

Esitys on pääosin seurattavissa ilman ihmeempiä esitietoja, kunhan englannin kuuntelu sujuu. Harmi kyllä, videon alun lyhyt johdanto on varmaankin monelle kurssilaiselle sen epäselvin osa, joten selvennetään tuota alkua vähän: Kahdesta puhujasta ensimmäisenä äänessä pääsevä Martin Fowler viittaa aluksi useasti ketterään ohjelmistokehitykseen (agile software development), josta on tällä vuosituhannella muodostunut keskeinen ohjelmistokehityksen muoto ja jonka julkkiksiin hän itse lukeutuu. Tiiviisti sanoen ketteryys tarkoittaa tässä ohjelmistojen tuottamista siten, että muuttuviin tilanteisiin ja vaatimuksiin pystytään reagoimaan nopeasti ja asiakkaiden kanssa tehdään jatkuvaa yhteistyötä (lisää esim. Wikipediassa). Tämä esitys ei varsinaisesti käsittele ketterää ohjelmistokehitystä, vaan Fowler käyttää tuota teemaa johdatellessaan puheen kohti edellä mainittuja aiheita. Loppuosa on ymmärrettävissä vaikkei ketteryydestä mitään tietäisikään.

Vielä biteistä, luvuista ja tietotyypeistä

Mieti, mitä käsky println(2147483647 + 1) tulostaa. Anna käsky sitten REPLissä. Vastasiko tuloste odotuksiasi?

Kirjoita tuloste tähän:

Mieti, mitä käsky println(100.0 / 11.0 * 11.0) tulostaa. Anna käsky sitten REPLissä. Vastasiko tuloste odotuksiasi?

Kirjoita tuloste tähän:

Luvun alkupäässä nousi aiheeksi se, että kaikki data, mm. luvut, esitetään tietokoneelle bitteinä. Monissa yhteyksissä datan sisäisestä binaarisesta esitysmuodosta ei kuitenkaan tarvitse välittää. Scala-ohjelma on melko korkean tason abstraktio tietokoneen sisäisestä toiminnasta, ja usein tällainen korkea abstraktiotaso riittää. Kuitenkin on tilanteita, joissa on tarpeen hahmottaa jotain Scala-ohjelmienkin toiminnasta bittitasolla. Äskeiset kaksi koodinpätkää ovat tästä esimerkkeinä. Selvitetään, mistä niiden oudoissa tulosteissa on kyse.

Bittimääristä

Luvun esittämiseen tarvittavan bittimäärän voi laskea muuntamalla luvun binaarimuotoon. Toisaalta voidaan tarkastella, montako erilaista arvoa tietyllä bittimäärällä voi kuvata:

  • Nollalla bitillä ei voi kuvata kuin yhden arvon. (Ja yhdellä arvolla ei voi ilmaista informaatiota; vrt. Unit luvusta 1.6.)

  • Yhdellä bitillä voi kuvata korkeintaan kaksi eri arvoa. Bitti riittää kuvaamaan esimerkiksi yhden totuusarvon, tai sen, onko luku 0 vai 1.

  • Kahdella bitillä voi kuvata korkeintaan neljä eri arvoa. Voidaan esimerkiksi sopia, että: 00=0, 01=1, 10=2 ja 11=3, tai että 00=0, 01=1, 10=-2 ja 11=-1.

  • Kolmella bitillä voi kuvata korkeintaan kahdeksan eri arvoa.

  • Kun bittejä on jokin määrä n, niillä voi kuvata korkeintaan 2n eri arvoa.

Int-arvoista

Scala-kieleen on suoritustehokkuussyistä määritelty, että kullekin Int-arvolle varataan tietty vakiomäärä bittejä — 32 kpl — joilla sen arvo varastoidaan. Scala on tässä aivan tyypillinen kieli; monissa muissakin ohjelmointikielissä lukutyypeillä on tietty bittimäärä vastaavasti.

Int-tyyppisen muuttujan koko tietokoneen muistissa on siis aina 32 bittiä eli neljä tavua (byte); yksi tavu on kahdeksan bittiä.

32 bitistä saa 232 erilaista nollan ja ykkösen yhdistelmää. Näin ollen erilaisia mahdollisia Int-arvoja on 232 kappaletta. Mielivaltaista määrää lukuja ei tällä tyypillä voi esittää.

On määritelty, että Scalassa nämä 232 arvoa kattavat kokonaisluvut välillä:

-2147483648 ... +2147483647

eli

-231 ... +231-1

Kääntäjä ei pidä huolta lukujen "mahtumisesta" tyyppeihin kuten ei virtuaalikonekaan. On ohjelmoijan vastuulla, että hän ei käytä tämän kokonaislukuvälin ulkopuolisia arvoja Int-tyypin kanssa.

Itseisarvoltaan liian suurilla luvuilla laskemisella voi olla yllättäviä seurauksia, kuten esimerkkilausekkeemme 2147483647 + 1 evaluoiminen osoitti. Kuten huomaat, ensimmäinen laskettava ei ole arvalla valittu vaan juuri se suurin luku, joka Int-tyyppiin mahtuu. Seurauksena on tuloksen "pyörähtäminen ympäri".

Aina ei suju ammattilaisiltakaan

Lukutyyppien rajat voivat näkyä sovellusten käyttäjillekin, ellei ohjelmoija tätä ehkäise. Tälle kurssille aiemmin osallistuneet opiskelijat ovat havainneet ilmiön peleissä:

2 147 483 647 sattui olemaan kultalimitti World of Warcraftissa ennen vanhaan. Sittemmin uusissa lisäosissa rahan keruu tuli niin helpoksi, että kultalimitti nostettiin.

Olen huomannut Hearthstonessa, että jos jonkin "minionin" elämät tai hyökkäysvahinko ylittää 2147483647 niin se tuhoaa kyseisen minionin. Nyt tiedän paremmin, miksi.

Hyvä toiseen suuntaan menevän errorin esimerkki on Gandhin käyttäytyminen Civilization I:ssä.

Vuonna 2014 Gangnam Style rikkoi YouTuben: "YouTuben ylläpitäjät joutuivat päivittämään koko palvelun, jotta se osaisi jatkaa laskemista. Gangnam Stylen katselulaskurin ylittämä kriittinen raja oli 32-bittinen kokonaisluku eli 2 147 483 647."

Vakavammasta tapauksesta oli kyse vuonna 1996 Cluster-avaruusaluksen räjähtäessä heti lähdön jälkeen. Englanninkielinen Wikipedia kertoo:

[The launch] ended in failure due to an error in the software design [which] caused inadequate protection from integer overflow. This resulted in the rocket veering off its flight path 37 seconds after launch, beginning to disintegrate under high aerodynamic forces, and finally self-destructing by its automated flight termination system. The failure has become known as one of the most infamous and expensive software bugs in history. The failure resulted in a loss of more than US$370 million.

Lentopelkoiset laittakoot silmät kiinni tämän kappaleen ajaksi. Vuonna 2015 nimittäin uutisoitiin, että Boeing 787 Dreamliner -lentokoneissa on ominaisuus, joka voi kytkeä koko koneesta virrat pois ellei konetta "buutata" vähintään 248 päivän välein. Mitähän 248 päivää on muissa mittayksiköissä?

Joissakin yhteyksissä ohjelmoijan virheestä voi myös muodostua tietoturvakysymys.

Double-arvoista ja liukuluvuista

Kullekin Double-arvolle on Scalassa varattu kahdeksan tavua eli 64 bittiä. Näiden 64 bitin avulla kukin Double-arvo esitetään etumerkkibitin, kertoimen ja eksponentin yhdistelmänä ns. liukulukuna (floating-point number; Double on double precision floating-point number).

Liukulukuesityksen yksityiskohdat eivät ole tämän kurssin kannalta kiinnostavia; niihin voit halutessasi tutustua jonkin digitaalitekniikan alkeiskirjan tai internetistä löytyvän materiaalin avulla. Nyt tärkeintä on hahmottaa, että koska resurssit ovat rajalliset (eli bittejä on tarjolla vakiomäärä) Double-arvoa ei voi esittää muistissa mielivaltaisen tarkasti. Bittimäärä rajoittaa sitä, kuinka suuria ja tarkkoja Double-arvot voivat olla. 64 bitillä saavutetaan noin viidentoista merkitsevän numeron tarkkuus.

Liukulukumuodon rajallisen tarkkuuden vuoksi Double-arvoilla laskiessa syntyy usein pieniä "epätarkkuuksia", jollaisesta näit esimerkin, kun evaluoit lausekkeen 100.0 / 11.0 * 11.0. Osamäärää ei voitu esittää "loputtoman tarkasti", ja kun epätarkka osamäärä kerrottiin taas yhdellätoista, ei lopputulos ollutkaan täsmälleen sata.

Rajallista tarkkuutta havainnollistaa myös tämä esimerkki:

2.999999999999999res0: Double = 2.999999999999999
2.9999999999999999res1: Double = 3.0

Rajallisen tarkkuuden vuoksi liukulukuarvoja ei usein ole järkevää esimerkiksi vertailla keskenään ==-operaattorilla.

Ilmiön seuraukset voivat muutenkin olla joskus hämmentävän näköisiä. Kokeillaan lähimpään kokonaislukuun pyöristävää round-metodia:

5.499999999999999.roundres2: Long = 5
5.4999999999999999.roundres3: Long = 6

Tässä siis jälkimmäinen literaali tarkoittaa itse asiassa liukulukua 5.5, joka pyöristyy ylöspäin.

Mikäs tuo Long tuossa äskeisessä REPL-esimerkissä muuten oli?

Scalan muista lukutyypeistä

Lukujen esittämiseen käytetään Scalassa tyypillisimmin Int- ja Double-tietotyyppejä. Aina ne eivät kuitenkaan ole tarkoitukseen riittäviä tai sopivimpia, joten muitakin vaihtoehtoja on tarjolla. Ohjelmoijan tulee valita sopiva tietotyyppi, johon käsiteltävät luvut "mahtuvat". Joissain tilanteissa ohjelmoija haluaa optimoida resurssien käyttöä ja käyttää Intiä ja Doublea vähäbittisempiä tietotyyppejä.

Tässä eräitä Scalan tietotyyppejä:

  • Suuria kokonaislukuja voi esittää käyttäen kahdeksantavuista tietotyyppiä Long (long integer), jonka puhti riittää 1018-suuruusluokkaan.

  • Resursseja voi säästää käyttämällä kokonaislukujen kuvaamiseen tietotyyppiä Short (short integer; 2 tavua) tai Byte (1 tavu). Liukuluvuille on vastaavasti käytettävissä tyyppi Float (floating-point number; 4 tavua). Tämä kannattaa vain silloin, kun tietää resurssioptimoinnin tarpeelliseksi kyseisessä yhteydessä.

  • Kun mikään ei riitä, voi käyttää scala.math.BigInt- ja scala.math.BigDecimal-tyyppejä, joilla voi kuvata mielivaltaisen kokoisia lukuja. Niillä ei ole vakiobittimäärää, vaan tilaa varataan muistista joustavasti kyseisen arvon perusteella. Haittapuolena on suuremman muistinkäytön lisäksi se, ettei tietokone laske näillä tyypeillä yhtä nopeasti kuin vakiobittisillä lukutyypeillä.

Tällä kurssilla sinun ei tarvitse käyttää muita lukutyyppejä kuin Intiä ja Doublea. Voit hakea muista tyypeistä lisää tietoa internetistä, jos aihe kiinnostaa.

Tehtävä: värejä lukuina

Yllä mainittiin, että kukin kuvan pikseli voidaan esittää lukuina, jotka vastaavat pikselin värisävyä.

Yksi tapa kuvata pikseli on RGB-muoto, jossa väri muodostuu punaisen (Red), vihreän (Green) ja sinisen (Blue) komponentin yhdistelmänä. Kukin komponenteista on luku. Lukujen suuruusluokka riippuu siitä, montako värisävyä on käytössä. Esimerkiksi näin:

  • Kukin pikselin kolmesta RGB-komponentista on yksi 256:sta lukuarvosta nollan ja 255:n väliltä. Komponentit kertovat "paljonko punaista", "paljonko vihreää" ja "paljonko sinistä" kyseisessä pikselissä on.

  • Tällöin kullakin pikselillä on yksi 2563:sta eli noin 16,8 miljoonasta värisävystä. Kutakin pikseliä kuvaavien lukujen tallentamiseen tarvitaan 3 * 8 bittiä eli kolme tavua muistia, koska kahdeksalla bitillä eli yhdellä tavulla voi esittää juuri 28 eli 256 eri arvoa.

Kurssin o1-kirjaston tuttu tyyppi Color mallintaa värejä juuri tuollaisessa RGB-muodossa. Asia on helppo havaita:

Tähän asti olemme käyttäneet vain vakiovärejä kuten Red ja LightBlue. Voimme myös luoda olion, joka kuvaa erikseen määräämäämme värisävyä:

val omaSavy = Color(220, 150, 220)omaSavy: Color = Color(220, 150, 220)

Parametrit (välillä 0–255) kertovat, millainen sävy on kyseessä. Esimerkissä luodaan marjapuuromainen sävy, jossa on erityisen paljon punaista ja sinistä.

Jokaisella Color-oliolla on muuttujat red, green ja blue:

omaSavy.redres4: Int = 220
omaSavy.greenres5: Int = 150

Samat muuttujat ovat vakioväreilläkin. Tutki, millaiset arvot esimerkiksi White-, Black- ja Brown-värien komponenteilla on.

Entä paljonko punaista on värissä Brown?

Pic-olioilla on metodi pixelColor, jolla voi kysyä tietyn pikselin väriä: jokuKuva.pixelColor(x, y). Kokeile sitä.

Kuinka paljon punaista on siinä pisteessä, joka on kuvassa Pic("defense.png") kohdassa (100,100)?

(Kyseinen kuvatiedosto tulee O1Libraryn mukana ja on käytettävissäsi REPLissä.)

Lisää väreistä

Väriolioilla voi tehdä muutakin. Lisää voit lukea dokumentaatiosta, mutta tässä pari esimerkkiä.

Väristä on helppoa tehdä vähän tummempi tai vaaleampi versio:

val vahanVaaleampi = Brown.lightervahanVaaleampi: Color = Color(174, 64, 64)
val kahdestiTummempi = Brown.darker.darkerkahdestiTummempi: Color = Color(133, 33, 33)
val testikuva = rectangle(100, 100, Brown).leftOf(rectangle(100, 100, vahanVaaleampi))
                                          .leftOf(rectangle(100, 100, kahdestiTummempi))testikuva: Pic = combined pic
testikuva.show()
../_images/pic_lighter_darker.png

Värillä on R-, G- ja B-komponenttien lisäksi myös ns. alfakanava eli läpinäkymättömyyttä (opacity) kuvaava tieto:

Blue.opacityres6: Int = 255
val lapikuultavaPunainen = Color(255, 0, 0, 100)lapikuultavaPunainen: Color = Color(255, 0, 0, 100)
rectangle(300, 100, lapikuultavaPunainen).onto(circle(200, Blue)).show()

Ellei toisin määritellä, värit ovat läpinäkymättömiä eli niiden opacity on suurin mahdollinen.

Läpinäkyvyyttä voi säädellä neljännellä komponentilla. Väri, jonka opacity on vain 100, on varsin läpikuultava. Nolla olisi tarkoittanut täysin läpinäkyvää.

../_images/pic_opacity.png

Yhteenvetoa

  • Tietokoneohjelmien käsittelemä data — mukaan lukien ohjelmat itse — esitetään tietokoneen muistissa bitteinä.

  • Scala-ohjelmakoodia ei ajeta sellaisenaan. Tyypillisimmin Scala-lähdekoodi käännetään ensin tavukoodiksi kutsutulle välikielelle. Kääntämisestä vastaa erillinen apuohjelma.

  • Tavukoodi puolestaan muunnetaan kohdekoneelle sopivaksi konekieleksi ohjelma-ajon yhteydessä. Tavukoodiohjelman ajamisesta vastaa virtuaalikoneeksi kutsuttu apuohjelma.

    • Scala-ohjelmien ajamiseen käytetään tavallisesti JVM-nimistä virtuaalikonetta, joka on alun perin luotu Java-ohjelmoijia varten.

    • Virtuaalikoneen käyttö parantaa ohjelmien siirrettävyyttä ja erilaisten komponenttien yhdisteltävyyttä.

  • Usein käytetyille lukutyypeille kuten Int ja Double on määritelty tietty bittimäärä, jolla niiden arvot kuvataan tietokoneen muistissa. Tämä asettaa tiettyjä rajoituksia näiden tyyppien käytölle.

  • Lukuun liittyviä termejä sanastosivulla: IDE eli sovelluskehitin; lähdekoodi, korkean tason kieli, konekieli, kääntäjä, prosessori; välikieli, tavukoodi; virtuaalikone, JVM eli Java-virtuaalikone; Java, JavaScript, Scala.js, komentorivi, roskankeruu; bitti, tavu; liukuluku; RGB.

Syvälle Scala-työkalujen tai tietokoneen laitteiston sisään ei tässä menty. Tässä luvussa luotu yleiskuva syventyy jatkokurssilla CS-A1120 Ohjelmointi 2, jolla bitit viuhuvat.

P.S.

Tässä vielä yksi:

0.1 + 0.1 + 0.1res7: Double = 0.30000000000000004

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, Antti Immonen, Jaakko Kantojärvi, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Anna Valldeoriola Cardó 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 nyt 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 suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.

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

Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.

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