Luku 12.1: Taulukoita ja rikkinäinen juna

../_images/robot_fight.png

Johdanto: taulukot mainittu

Taulukkoa tarkoittava array-sana on jo vilahtanut kurssilla:

val lukupuskuri = Buffer(1, 2, 3)lukupuskuri: Buffer[Int] = ArrayBuffer(1, 2, 3)
val sanataulukko = "yksi,kaksi,kolme".split(",")sanataulukko: Array[String] = Array(yksi, kaksi, kolme)

REPLin tulosteissa on läpi kurssin näkynyt ArrayBuffer, kun olemme käyttäneet puskureita.

split-metodi (luku 5.2) jakaa merkkijonon osiin ja palauttaa ne — ei vektorissa eikä puskurissa vaan taulukossa.

Taulukot (array) ovat alkiokokoelmia ja muistuttavat puskureita ja vektoreita:

  • Kuten puskurit ja vektorit, taulukot sisältävät alkioita tallennettuina eri indekseille.

  • Taulukon alkion voi vaihtaa toiseksi. Tässä suhteessa taulukko muistuttaa puskuria ja eroaa vektorista.

  • Taulukon koko on muuttumaton kuten vektorinkin. Koko määrätään taulukkoa luodessa, eikä taulukossa voi myöhemmin olla eri määrää indeksejä kuin aluksi.

Kovin tutunsorttinen rakenne on siis kyseessä. Katsotaan ensin, miten taulukkoja käytetään, ja palataan sitten siihen, millaisissa tilanteissa niitä voi haluta käyttää.

Taulukot Scalalla

Array-kokoelmatyyppi

Taulukot, samoin kuin vektorit ja toisin kuin puskurit, ovat käytettävissä kaikissa Scala-ohjelmissa ilman import-käskyä. Taulukon voi luoda muista kokoelmista tuttuun tyyliin:

val taulukko = Array("eka", "toka", "kolmas", "neljäs")taulukko: Array[String] = Array(eka, toka, kolmas, neljäs)
val toinenTaulukko = Array.tabulate(5)( indeksi => 2 * indeksi )toinenTaulukko: Array[Int] = Array(0, 2, 4, 6, 8)

Yhtä tuttuun tapaan toimivat myös taulukon indeksit ja metodit:

taulukko(2)res0: String = kolmas
taulukko(3) = "vika"taulukkotaulukko: Array[String] = Array("eka", "toka", "kolmas", "vika")
taulukko.sizeres1: Int = 4
taulukko.indexOf("kolmas")res2: Int = 2
taulukko.mkString(":")res3: String = eka:toka:kolmas:vika
taulukko.map( _.length )res4: Array[Int] = Array(3, 4, 6, 4)

Kokonaan uuden alkion lisääminen (ja taulukon koon kasvattaminen) ei onnistu:

taulukko += "vielä yksi?"-- Error:
taulukko += "vielä yksi?"
^^^^^^^^^^^
value += is not a member of Array[String]

Alustamattoman taulukon luominen: ofDim

Joskus on kätevää luoda tietynkokoinen kokoelma, jonka sisältö asetetaan vasta myöhemmin erillisillä käskyillä. Scala API tarjoaa tähän tarkoitukseen Array.ofDim-metodin (jota ei löydy vektoreille tai puskureille).

val taulukko = Array.ofDim[Int](5)taulukko: Array[Int] = Array(0, 0, 0, 0, 0)

ofDim-metodille tulee antaa tyyppiparametri hakasulkeissa. Tässä taulukon alkioina on Int-arvoja.

Tavalliseksi parametriksi annetaan luku, joka määrää taulukon koon. Tässä luodaan viisipaikkainen taulukko.

Metodi luo ja palauttaa halutunkokoisen taulukon, jonka sisältönä on oletusarvoja, tässä tapauksessa nollia.

Luomalla taulukon tähän tapaan saamme varattua halutun määrän paikkoja (muistitilaa) alkioille, jotka eivät ole tiedossa vielä taulukonluontikäskyä suoritettaessa, mutta joiden (enimmäis)lukumäärä on asetettu.

Kutsun Array.ofDim voi lukea array of dimensions eli vapaasti kääntäen "taulukko, jolla on eri ulottuvuuksissa mitat". Nimensä mukaisesti metodi sopii myös "moniulotteisten" eli sisäkkäisten taulukkojen luomiseen (vrt. "moniulotteiset" vektorit luvusta 6.1):

val kaksiulotteinen = Array.ofDim[Int](2, 3)kaksiulotteinen: Array[Array[Int]] = Array(Array(0, 0, 0), Array(0, 0, 0))
val kolmiulotteinen = Array.ofDim[Double](2, 2, 2)kolmiulotteinen: Array[Array[Array[Double]]] =
Array(Array(Array(0.0, 0.0), Array(0.0, 0.0)), Array(Array(0.0, 0.0), Array(0.0, 0.0)))

Huomaa: ofDim vain luo taulukon, se ei luo sille mitään merkityksellistä sisältöä. On luodussa taulukossa silti aina jotain. Alkioiden oletusarvo riippuu niiden tyypistä:

  • lukutyypeillä (Char mukaan lukien) nolla,

  • totuusarvoilla false, ja

  • kaikilla muilla tyypeillä, myös esim. String-tyypillä, "olematon arvo" null (joka on virheiden kutualusta; luku 4.3).

Oletusarvot voi ja yleensä onkin tarpeen korvata muilla arvoilla joko ennemmin tai myöhemmin.

Tarkkana oletusarvojen kanssa!

Olkoon käytössä luokka nimeltä Footballer. Luodaan taulukko näin:

val helmarit = Array.ofDim[Footballer](11)

On yleinen aloittelijan ajatusvirhe unohtaa, ettei tuollainen käsky luo ensimmäistäkään ilmentymää taulukon alkiotyypiksi mainitusta luokasta Footballer, vaikka luokin Array[Footballer]-tyyppisen arvon. Luoduksi tulee ainoastaan yksitoista null-arvoa sisältävä taulukko. Taulukkoon voi sijoittaa erikseen luotuja Footballer-olioita näin:

helmarit(0) = Footballer("Tinja-Riikka Korpela")
helmarit(1) = // etc.

Vain jos taulukkoon todella on sijoitettu pelaajaolioita, voi niiden metodeita kutsua kuten alla synnyttämättä NullPointerException-virhetilannetta.

helmarit(9).score()
val keeper = helmarit(0)
println(keeper.name)

Eikö kuulosta hyödylliseltä?

Taulukko on ominaisuuksiltaan rajoittuneempi kokoelmatyyppi kuin puskuri, koska sen kokoa ei voi muuttaa. Minkä vain toiminnon, jonka saa toteutettua taulukkoja käyttäen, saa toimimaan myös puskureilla. (Ja toisinkin päin kyllä.) Silloin, kun halutaan kuvata täysin muuttumatonta indeksoitua alkiokokoelmaa, voi käyttää vektoria. Alustamattomissa taulukon alkioissa vaanii null.

Miksi siis vaivautuisit opettelemaan vielä yhden kokoelmatyypin? Tässä eräitä syitä:

  • Yleisyys: Taulukot kuuluvat ohjelmoijan yleissivistykseen. Taulukko on yleinen kokoelmatyyppi, perusrakenne, jota käytetään muiden kokoelmatyyppien toteutuksessa apuna. Se esiintyy monissa ohjelmointikielissä; useassa kielessä se on yleisimmin käytetty kokoelmatyyppi.

  • Tehokkuus: Taulukon käytöllä saavutetaan joissakin tilanteissa tehokkuusparannuksia. Tämä ei ole Scala-taulukoiden käyttösyistä vähäisimpiä, mutta jätämme jälleen tehokkuusnäkökulman jatkokurssien asiaksi. Tiivis vertailu Scalan kokoelmatyyppien tehokkuudesta löytyy Scalan kotisivuilta.

  • Luontevat käyttötilanteet: Taulukko on luonnollinen valinta, jos halutaan kuvata kokoelmaa, jonka sisältö voi muuttua mutta jonka koko on muuttumaton tai jonka koolla on jokin ennalta määrätty yläraja. Esimerkiksi ruudukkoa kuvaava Grid-luokka (luku 8.1) on toteuttu "kaksiulotteista" taulukkoa apuna käyttäen.

  • Valmiit ohjelmakirjastot: Array-tyyppiä on käytetty monissa ohjelmakirjastoissa, muun muassa Scalan perus-APIssa. Yksi esimerkki on yllä mainittu split-metodi.

Taulukot puskurien toteutuskeinona

Puskurin yhteydessä näkynyt ilmaisu ArrayBuffer eli "taulukkopuskuri" johtuu tavasta, jolla Scalassa käytetty puskuriluokka on toteutettu: kullakin puskurioliolla on sisäisesti apunaan jonkin kokoinen taulukko-olio, johon puskurin alkiot tallennetaan. Kun aputaulukosta loppuu lisättäessä tila kesken, vaihtaa puskuriolio sen suurempaan taulukkoon, johon se kopioi vanhat alkiot ja lisää uudet. Nimi ArrayBuffer viittaa tässä siis taulukoilla toteutettuun puskuriluokkaan.

Tällaisen puskurinkin käyttö on siis eräänlaista taulukon käyttöä, joskin epäsuorasti ja puskurin käyttäjän kannalta lähes huomaamattomasti.

Myös Scalan luokan Vector toteutuksessa on käytetty apuna taulukkoa.

Lisää taulukkopohjaisia kokoelmia: IArray, ArrayDeque jne.

Scala APIn kokoelmatyyppi IArray muistuttaa taulukkoa ja on sisäisesti toteutettu taulukkoa käyttäen. IArray ei kuitenkaan tarjoa vaikutuksellisia metodeita; nimen I tulee sanasta immutable. IArray-olio on siis vektorin kaltaisesti muuttumaton mutta tehokkuusominaisuuksiltaan erilainen (ja monesti hyvä) kokoelma.

val muuttumatonTaulukko = IArray(10, 4, 5)muuttumatonTaulukko: IArray[Int] = Array(10, 4, 5)
muuttumatonTaulukko(1)res5: Int = 4
muuttumatonTaulukko(1) = 5-- Error:
muuttumatonTaulukko(1) = 5
^^^^^^^^^^^^^^^^^^^
value update is not a member of IArray[Int]

ArrayDeque puolestaan on puskuria muistuttava muuttuvatilainen kokoelma, jonka kumpaankin päähän — alkuun ja loppuun — voi tehokkaasti lisätä alkioita; myös poistot päädyistä ovat tehokkaita. ("Deque" on yleinen lyhennös sanoista double-ended queue ja lausutaan "deck".)

Lisätietoja näistä ja muista kokoelmista löydät Scala API:sta, jossa on eri pakkaukset muuttumattomille ja muuttuvatilaisille kokoelmille.

Junan debuggausta

Seuraavassa harjoituksessa on monelle ammattiohjelmoijalle tuttu tilanne: pitää setviä jonkun toisen tekemä koodisotku.

Tehtävänanto

Moduuli Train sisältää luokkia, joilla kuvataan junavaunuja ja niissä olevia matkustajapaikkoja kuvitteellisessa ja yksinkertaistetussa paikanvarausjärjestelmässä. Koodi on kirjoitettu vahvasti imperatiivisella tyylillä ja rakentuu taulukoiden, tilanmuutosten ja while-silmukoiden varaan.

Annettu koodi ei ole esimerkillistä. Pahin puute on, että se ei toimi oikein.

Tehtävänäsi on laatia käynnistysfunktio, jolla testaat annettuja luokkia ja paikannat virheet. Löydetyt virheet pitää myös korjata, mutta se on tämän tehtävän selvästi helpompi osio.

Luokkien toivottu toiminta on kuvattu moduulin Scaladoc-dokumentaatiossa.

Ohjeita ja vinkkejä

  • Älä jumita tähän tehtävään tuntikausiksi. Pyydä neuvoa ajoissa.

  • Testaa koodi aluksi kunnolla. Siis kutsu metodeita kattavasti erilaisilla parametriarvoilla ja erilaisissa järjestyksissä. Tämä auttaa selvittämään, mikä toimii ja mikä ei. Tämän jälkeen debuggaa koodi eli paikanna virheiden syyt.

  • Luo käynnistysfunktio, jolla testaat ohjelmaa. Tee siitä sellainen kuin parhaaksi katsot. Voit kirjoittaa funktion tiedostoon test.scala.

  • Voit sitten hyödyntää myös IntelliJ’n debuggerityökalua annettuun koodiin (ja käynnistysfunktioosi). Debuggeritta tehtävä voi olla huomattavastikin vaikeampi.

  • Virheitä on kaikkiaan kahdeksan kappaletta. Kukin niistä on selvästi eri osassa koodia.

  • Useat annetun koodin metodeista saavat luokat toimimaan oudosti tai ohjelman kaatumaan, jos niille antaa "selvästi hölmöjä" parametriarvoja (esim. asetetaan negatiivinen hyttien määrä makuuvaunuun tai lisätään junaan null-arvo vaunuolion sijaan). Tällaisia tapauksia ei tässä tehtävässä lueta virheiksi. Keskity löytämään sellaisia virheitä, jotka selvästi saavat koodin toimimaan toisin kuin spesifikaatio määrää, vaikka parametriarvot ovatkin järkeviä.

  • SittingCar on luokista monimutkaisin. Tutki muut luokat ensin. Ota SittingCar käsittelyyn, kun ymmärrys tästä ohjelmakokonaisuudesta ja oma testausprosessi ovat vähän hioutuneet.

  • Eräillä luokista on myös kumppanioliot määriteltyinä samaan tiedostoon. Nämä yksinkertaiset oliot ovat olemassa vain toimiakseen vakioiden sijoituspaikkoina.

  • Koodiin ei tarvitse tehdä tehokkuus- tai tyyliparannuksia, vaikka aihetta sellaisille löytäisitkin.

  • Myös ns. pöytätestaus (desk checking) voi auttaa, kuten tässä aiempivuotisessa palautteessa:

    Tämä oli kyllä omalla tavallaan vaikea tehtävä, mutta aika helposti se ratkesi, kun tulostin ohjelman paperilla, otin lyijykynän käteen ja otin nivaskan saunalukemiseksi saunaan. :)

    Saunonta ei ole menetelmän kannalta välttämätön.

Lisätehtävä: yksikkötestaus ja testausstrategiat

Tämä bonustehtävä on tarkoitettu lähinnä sellaisille opiskelijoille, joilla on aiempaa ohjelmointikokemusta. Motivoituneet aloittelijat voivat kyllä myös tehdä tämän. Testauksesta varsinaisemmin mm. kurssilla Ohjelmointistudio 2.

Tutustu internetin avustuksella järjestelmällisempiin tapoihin testata luokkia. Tee yksi tai useampia seuraavista.

  1. Etsi tietoa yksikkötestauksesta (unit testing). Yksikkötestaamalla pyritään varmistamaan, että kukin yksittäinen ohjelman osa toimii niin kuin sen pitää.

  2. Tee hieman kirjallisuustutkimusta testausstrategioista. Selvitä, millaisiin strategioihin voi tukeutua, jotta testeistä saa kattavat mutta välttyy tekemästä järjettömän monta erillistä testiä. Valitse jokin tunnettu testausstrategia (esim. all-pairs testing) ja sovella sitä o1.train-pakkauksen luokkiin.

  3. Etsi tietoa ScalaTest-ohjelmakirjastosta, jolla voi laatia yksikkötestejä Scala-ohjelmille. Selvitä, miten IntelliJ tukee ScalaTest-kirjaston käyttöä. Voit myös itse kokeilla käyttää tätä kirjastoa, jos haluat ja sinulla on aikaa perehtyä aiheeseen.

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

Valinnaista asiaa: tehokkuus

Tehokkuudesta, optimoinnista ja profiloinnista

Ohjelmoijat puhuvat usein "tehokkuusparannuksista". Voiko asian ymmärtää siten, että näiden tehokkuusparannusten avulla voi saada ohjelman toimimaan kuluttamatta yhtä paljon koneen tehoa, muistia, yms.?

Kyllä, kun ohjelmoinnin yhteydessä puhutaan tehokkuudesta, on kyse yleensä ohjelman (tai algoritmin) vaatimasta suoritusajasta ja/tai tarvittavan muistin määrästä. Myös muita arviointikriteerejä on, kuten energiankulutus.

Kurssimateriaalissa on siellä täällä mainittu, että ohjelman suoritusajan ja muistinkäytön optimointi ja tehokkuusasiat yleensäkin ovat ajankohtaisia lähinnä jatkokursseilla (kuten tässä Ohjelmointi 2 -kurssin luvussa). Tämä kiinnostava teema toki on silti mietityttänyt useaa opiskelijaa jo tämän kurssin aikana.

Joskus tehokkuudesta ollaan jopa liian innostuneita.

Voit painaa mieleesi seuraavat usein toistetut tehokkuusoptimoinnin kolme kultaista sääntöä:

Rule #1: Don’t.

Rule #2: Don’t. Yet.

Rule #3: Profile first.

Profilointi (profiling) on toimenpide, jossa ohjelma-ajoja mittaamalla arvioidaan ohjelman osien suoritustehokkuutta. Profiloimalla (ja muunkinlaisilla analyyseillä) voidaan tarvittaessa selvittää, minkä osien vaikutus kokonaisuuden suoritustehokkuuteen on käytännössä merkityksellinen. On yleistä, että pieni osa ohjelmakoodista ratkaisee kokonaisuuden tehokkuuden:

There is a famous rule in performance optimization called the 90/10 rule: 90% of a program’s execution time is spent in only 10% of its code.


The standard inference from this rule is that programmers should find that 10% of the code and optimize it, because that’s the only code where improvements make a difference in the overall system performance.


But a second inference is just as important: programmers can deoptimize the other 90% of the code (in order to make it easier to use, maintain, etc.), because deterioration (of performance) of that code won’t make much of a diffence in the overall system performance.


—Richard E. Pattis

Sovellusohjelmoinnissa tehokkuudella on ensisijaista merkitystä suuria datamääriä käsitellessä ja/tai kun käytetyn laitteen laskentaresurssit ovat niukat — molemmat nämä ovat toki yleisiä tapauksia. Muut laatukriteerit kuten selkeys ja muokattavuus ovat monesti oleellisempia. Aina tilanteen mukaan!

../_images/donald_knuth.png

Donald Knuth, tehokkuusanalyysin uranuurtaja ja yksi kaikkien aikojen legendaarisimmista ohjelmoijista

Tässä vielä tunnettu lausahdus herralta, joka tietää aiheesta asian jos toisenkin:

Premature optimization is the root of all evil.

—Donald Knuth

Uusien olioiden luominen ja tehokkuus funktionaalisesssa ohjelmoinnissa

Luvun 11.2 vapaaehtoisessa tehtävässä toteutettiin otteluluokka Match funktionaalisella tyylillä.

Kun kerran funktionaalisessa ohjelmoinnissa luodaan aina uusia olioita sen sijaan, että muokattaisiin vanhoja, niin mitä tapahtuu vanhentuneille olioille? Eli osaako kone automaattisesti poistaa muistista Match-olion, joka sisältää 2 maalia, kun tehdään 3. maali ja luodaan uusi Match-olio?

Kyllä. Noihin vanhoihin Match-olioihin ei jää viittausta, ja virtuaalikoneeseen kuuluva roskankerääjä siivoaa ne kyllä reippaasti muistista.

Eikö funktionaalisessa ohjelmoinnissa synny paljon ns. "vanhentunutta" dataa?

On totta, että jos käsiteltävät tietorakenteet ovat suuria, niin kokonaisten kopioiden muodostaminen muistiin ei ole tehokasta. Tämä ei kuitenkaan ole funktionaaliselle ohjelmoinnille niin iso ongelma kuin voisi ensin kuvitella.

On olemassa ratkaisuja, joilla turhaa muuttumattomien tietorakenteiden kopioimista voidaan näppärästi välttää. Yksi esimerkki tästä ovat "säilyvät tietorakenteet" (persistent data structure; ks. Wikipedia), jollaisen muuttaminen ei itse asiassa luokaan kokonaan uutta rakennetta muistiin vaan osin hyödyntää aiempaa versiota rakenteesta. Esimerkiksi useat Scalan valmiit kokoelmatyypit toimivat tällä tavoin, mutta se on jatkokurssien asia.

Muistin varaaminen on muuten seikka, johon voi kiinnittää huomiota myös muuttuvatilaisten kokoelmien kuten puskurien osalta:

Pohdintoja: Kuinka monelle viittaukselle luotava uusi tyhjä puskuri varaa tilaa? Ja mitä tapahtuu, kun viittaukset ylittävät tämän määrän? Varataanko tällöin muistia automaattisesti esim. toinen mokoma? Ei liene järkevää varata lisää muistia joka kerta, kun puskuriin lisätään viittaus.

Kun tyhjä Buffer (tarkemmin sanoen taulukoilla toteutettu puskuri eli ArrayBuffer) luodaan, se varaa itselleen oletusarvoisesti 16 alkion kokoisen taulukon. Ja kuten opiskelija tuossa veikkaa, puskuri varaa itselleen lisää tilaa tarpeen mukaan: kun vanha taulukko ei riitä, se varaa uuden tuplakokoisen ja kopioi kaikki alkiot sinne.

(Joka haluaa, voi käydä bongaamassa ArrayBuffer-luokan lähdekoodista tuon oletusarvon 16 ja tuplakokoisen taulukon luomisen.)

Kirjoitus Benchmarking Scala Collections vertailee Scala-kokoelmatyyppien tehokkuusominaisuuksia. (Kirjoitus on pääosin hyvä mutta jo joitakin vuosia vanha, minkä lisäksi vertailumenetelmä on osin harhaanjohtava.) Se sopinee parhaiten kurssilaisille, joilla on aiempaakin ohjelmointikokemusta. Tuo vertailukin osoittaa yleisesti pätevän lopputuloksen: se, mikä kokoelma on tehokkain, riippuu ratkaisevasti käyttötapauksesta.

Yhteenvetoa

  • Taulukko on muuttumattoman kokoinen alkiokokoelma, jonka sisältö voi muuttua.

    • Taulukot ovat yleisiä monissa ohjelmointikielissä ja ohjelmissa.

    • Taulukoita käytetään, koska siitä voi tilanteesta riippuen olla esimerkiksi tehokkuusetua.

  • Lukuun liittyviä termejä sanastosivulla: taulukko; testaus.

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, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, 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, Juha Sorva ja Jaakko Nakaza. 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; sitä 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...