Uusia ohjelmointikieliä tulee nykyään tasaiseen tahtiin. Valtaosa niistä ei tuo mitään mullistavaa uutta pöydälle. Hiljattain Mozillan suojista pinnalle noussut Rust on kuitenkin kerännyt mielenkiintoa varsin paljon. Vaikka Rust ei tuo mukanaan mitään mullistavaa uutuutta ominaisuuksien puolesta, kerää se monia nykyaikaisia ominaisuuksia varsin toimivaksi kokonaisuudeksi. Mikä erityisesti tekee tästä kielestä mielenkiintoisen on sen käyttökohde. Nykysuuntauksesta poiketen, Rust on virtuaalikoneeton tehokas järjestelmätason kieli, joka pyrkii haastamaan C:n ja C++:n valta-asemaa. Se tukee C-tyylisen proseduraalisen ohjelmoinnin lisäksi myös olio- ja funktionaalista ohjelmointimallia.

Olen seurannut Rustin kehitystä jo jonkin aikaa. Kun Rustista julkaistiin 1.0-versio, otin kielen kokeiluun. Rust-kielen kehittäjät ovat sen jälkeen pysyneet aikataulussaan, missä uusi versio julkaistaan kuuden viikon välein. Kirjoitushetkellä viimeisin versio on 1.6. Päivitykset ovat toistaiseksi keskittyneet optimointeihin ja API:en stabilointiin.

Käydään tässä artikkelissa muutamia Rustin perusominaisuuksia läpi.

Syntaksi pähkinänkuoressa

Monella tavalla Rust on hyvin samantyyppinen kuin C. Syntaksi on hieman erilainen, mutta muuten kielen rakenne on C-tyylisiin kieliin tottuneelle tuttu. Ohjelma koostuu pääsääntöisesti funktioista proseduraalisten kielten tapaan. Luokkia ei tarvitse tehdä ja luokka on käsitteenäkin Rustissa varsin häilyvä, vaikka olio-ohjelmointia tukeekin. Rustin oliot ovat rakenteeltaan C:n ja C++:n välimaastossa: strukti ja sen kanssa assosioidut funktiot. Rustin oliomalli perii joitain ominaisuuksia uudemmista kielistä. C-tyylisesti struktit eivät tue perintää, vaan tarkoitus on koostaa oliot, mutta samanaikaisesti rajapinnoille löytyy tuki piirteiden (trait) myötä. C:stä poiketen myös oliolle assosioidut funktiot ovat oliokielten tavoin tiukasti sidottu kyseiseen olioon.

struct A
{
    muuttuja: i32
}

struct B : A;   // Virhe, ei perintää

struct C
{
    other: A    // kooste
}

impl A                          // A:n assosioitujen funktioiden toteutus.
{
    fn new() -> A               // 'static'-funktio Luokka-struktille.
    {                           // Ei erillistä rakentaja-syntaksia, new-funktio standarditapa.
        A { muuttuja: 123 }     // Palauttaa uuden Luokka-struktin. Funktion paluuarvo on 
    }                           // viimeisen lauseen arvo, return-avainsanaa voi käyttää,
                                // mutta ei vaadittu funktion lopussa.
                                // Struktin voi luoda myös ilman new-funktiota, mutta sen tekeminen
                                // on suositetavaa erityisesti oletusarvojen takia. Struktia luotaessa
                                // täytyy kaikki sen arvot alustaa.
    
    fn func( self ) -> i32      // Instanssi-funktio, ensimmäinen parametri on aina struct itse.
    {
        return a;
    }
}

trait D         // piirre
{
    fn trait_func( &mut self, x : i8 );
}

impl D for A    // Piirteen toteutus luokalle A.
{
    fn trait_func( &mut self, x : i8 )
    { ... }
}

fn main()
{
    let mut x = A::new();
    let y = x.func( 42 );
    let z = x.trait_func( 1 );
}

Rust perii myös monia ominaisuuksia funktionaalisista ohjelmointikielistä ja onkin monella tapaa enemmän funktionaalinen kieli kuin oliokieli. Näistä ensimmäisenä vastaan tulee arvojen käyttö muuttujien sijaan, eli oletuksena kaikki arvot ovat muuttumattomia. Mikäli arvoa haluaa muuttaa, pitää se eksplisiittisesti määritellä muuttujaksi. Funktionaalisten kielten mukaisesti myös Rustissa funktiot sekä silmukka, konditionaali ja muut rakenteet ovat ensimmäisen luokan kansalaisia. Täten niitä voi käyttää kuten mitä tahansa muutakin arvoa. Samoin löytyy tuki sulkeumille (closure) sekä hahmon tunnistukselle (pattern matching).

let a = 42;
a = 24;         // Virhe, a on muuttumaton arvo.
let mut b = 42; // Määritellään b muuttujaksi.
b = 24;

// Samat säännöt pätevät funktion parametreissa.
fn funktio( c: f32, mut d: i32 )
{
    let f = if d == 0 { 2 } else { 4 }; // f on if-lauseen paluuarvo.
    let m = match(c)                    // pattern matching
    {
        0 => 4,
        _ => 2
    }
}

Muistinhallinta ja elinkaari

Merkittävin ominaisuus Rustissa liittyy kuitenkin muistinhallintaan ja arvojen siirtoon. Kääntäjä on näiden sääntöjen kanssa (syystäkin) melko tiukka, minkä takia kokemattomalle tämä aiheuttaa paljon harmaita hiuksia. Toisin kuin C:ssä, kasan (heap) varattua muistia ei tarvitse vapauttaa manuaalisesti. Sen sijaan kääntäjä on käännösvaiheessa tietoinen arvojen elinkaaresta ja lisää automaattisesti muistinvapautuksen heti, kun arvon elinkaari päättyy. Tämän takia kääntäjän pitää olla koko ajan tietoinen arvon elinkaaresta ja eritoten omistajasta. Olennainen osa tätä on arvojen siirto ja lainaus funktioihin kopioinnin sijaan.

C:stä poiketen funktiokutsussa ei kopioida kaikkien parametrien arvoja. Vain perustietotyypit (i32, f32, yms.) sekä Copy-piirteen toteuttaneet kopioidaan. Muissa tapauksissa arvon omistus siirretään, mikä invalidoi paikallisen arvon estäen sen käytön. Silloin kun arvoa ei haluta siirtää, voidaan se lainata. Tämä vastaa parametrin antamista viitteenä, mutta Rustin tapauksessa kääntäjä asettaa joitain rajoitteita lainatun arvon käytölle. Tämän lainausmekanismin ymmärtäminen on äärimmäisen tärkeää Rustin kanssa. Kaikilta osin tämä ei kuitenkaan aina ole selkeää kielen puolesta. Välillä joutuu (turvallisesti) kiertämään kääntäjän tiukkoja sääntöjä mm. mem::replace-funktiota käyttäen. Toisaalta parempi näin päin, että kääntäjä pitää tiukasti kiinni rajoitteista, varsinkin kun kyseessä on Rustin muistinkäsittelyn oikeellisuudesta. Tapauksissa, joissa pitää kokonaan ohittaa nämä säännöt (mm. C-kirjastokutsut), voidaan se tehdä unsafe-lohkossa. Tällöin kääntäjä luottaa ohjelmoijan tekevän asioita oikein ja kaiken sen ulkopuolelle tulevan olevan validia tavaraa.

struct S { value: i32 }

fn A()
{
    let x = S { value: 42 };
    B( x );
    let y = x;  // Käännösvirhe, x on siirretty B-funktioon ja siten ei enää validi täällä.
}

fn B( param: S ) { }

fn C()
{
    let x = S { value: 42 };
    D( &x );
    let y = x;  // Toimii, x oli vain lainassa D-funktiossa ja on edelleen käytettävissä täällä.
}

fn D( param: &S )
{
    B( param ); // Käännösvirhe, param on lainassa, mutta B haluaa omistuksen.
}

Jälkimaku

Lyhyen kokeilun jälkeen Rustista jäi kuitenkin varsin hyvä maku suuhun. Syntaksiltaan ja ominaisuuksiltaan se on varsin miellyttävä kieli käyttää. Toistaiseksi se ei ole aivan täydellinen. Erityisesti lainausmekanismin erikoisuudet ja rajoitteet häiritsevät aloittelijaa paikoittain. Dokumentaatio on varsin hyvällä tasolla ja kieli jatkaa nopeahkoa kehitystään. Myös saatavilla olevien kirjastojen määrä on kasvanut nopeasti crates.io -palvelussa vakaan version jälkeen. Rust on ehdottomasti varteenotettava C:n dominoiman ekosysteemin haastaja, mutta aika kertonee kuinka laajalti se tullaan ottamaan käyttöön.