maanantai 2. huhtikuuta 2012

Nörttitytön perehdytys funktionaaliseen ohjelmointiin


Mainitsin viimeksi ohjelmointikielistä kirjoittaessani, että Scala on tätä nykyä ehdoton suosikkini kaikista koskaan oppimistani ohjelmointikielistä. Tämä johtuu luultavasti siitä, että opetellessani Scalaa sain samalla myös oivalluksen funktionaalisesta ohjelmoinnista. Wikipedia määrittelee lyhyesti sen seuraavasti:
Funktionaalinen ohjelmointi eli funktio-ohjelmointi on ohjelmointiparadigma, joka perustuu matemaattisten funktioiden käyttöön ja tarkemmin lambdakalkyyliin.
Image via XKCD.
 Jep. Sama suomeksi kiitos. Neil deGrasse Tyson sanoo omasta alastaan (astrofysiikka) jotakuinkin seuraavaa; tämä on tarpeeksi vaikeaa ilman, että haastetta tarvitsee lisätä turhan vaikeilla sanoilla. Olen täysin valmis allekirjoittamaan saman ohjelmoinnista.


Joten, funktionaalinen ohjelmointi on tapa ohjelmoida, jossa dataa syötetään matematiikan tunneilta tuttuun tapaan funktioihin, jotka palauttavat samalla datalla aina saman tuloksen. Tämä saattaa vaikuttaa itsestäänselvältä, mutta pitkään ohjelmointi on käytännön elämässä pyörinyt lähinnä vuokaaviomaisten tilamuutosten ympärillä. Näin ollen metodi ei aina palauta samaa tulosta samalla datalla. Tai tarkemmin sanottuna, samalla oliolla. Funktiolla ei myöskään ole sivuvaikutuksia ympäristöönsä, koska se ei muuta käyttämänsä datan tilaa.

Funktionaalinen ohjelmointi eli pitkään ainoastaan akateemisessa maailmassa huolimatta siitä, että Grace Hopper kirjoitti ensimmäisen kääntäjän nimenomaan funktionaalisen ohjelmoinnin mahdollistamiseen. Nainen kun oli matemaatikko ja halusi vain laskea. Ohjelmien monimutkaistuessa samanaikainen suorittaminen (concurrency näin tutummin) on muodostunut ongelmaksi, johon funktionaalinen ohjelmointi on osoittautunut loistavaksi parannuskeinoksi.

Samanaikaisuus on todellinen ongelma, kun yhtäaikaisten käyttäjien määrä kasvaa kasvamistaan ja kaikilla on omat tietonsa, joihin muilla ei ole asiaa. Viimeisin tunnettu, erityisen vakava esimerkki lienee erään verkkopankin uudistuksen yhteydessä tapahtuneet käyttäjä-istuntojen sotkeutumiset. Useissa tapauksissa asiakkaat saivat aivan tuntemattomien asiakkaiden tietoja näkyviin omaan käyttöliittymäänsä, joka on selkein samanaikaisuusongelmien ilmentymä. Vastaavasti esimerkiksi verkkokauppa, jossa ostoskoriin ilmestyy tuotteita, joita sinne ei ole lisännyt tai poistuu tuotteita, joita sieltä ei ole poistanut. Nykyään useat sovelluskehikot hoitavat suuren osan näistä ongelmista automaattisesti (tai käytännössä pienellä konfiguraatiolla).

Funktionaalinen ohjelmointi tarjoaa vaihtoehdon raskaille sovelluskehikoiden tarjoamille samanaikaisuusratkaisuille muuttamalla arvot muuttumattomiksi. Kun tämä yhdistetään funktioiden läpinäkyvyyteen, poistuvat samanaikaisuusongelmat ikään kuin automaattisesti. Suurin ongelma monisäikeisten ohjelmien tekemisessä kun on nimenomaan muuttuvan, jaetun datan synkronointi. Kun data ei muutu, ei tätä ongelmaa enää ole.

Jotta tämä nyt ei menisi liian korkealentoiseksi, jatketaan esimerkeillä.
Alla on esimerkki Javalla (selkeän imperatiivinen kieli) kirjoitetusta Fibonacci-lukujen erottelusta:

// Fibonacci sarja, Java tyyliin
public void fibonacci(int iterations){
    int first = 0; 
    int second = 1;
 
    system.out.println(first + ", " + second);
    for (int i = 0; i < iterations; ++i) {
        system.out.println(", ");
        int sum = first + second;
        first = second;
        second = sum;
        system.out.println(sum);
    }

}
 
public void main(String[] args)
{
    fibonacci(10);
}

Vertailun vuoksi vastaava toiminto Scalalla (Scalalla on mahdollista ohjelmoida sekä imperatiivisesti, että funktionaalisesti, mutta alla oleva esimerkki on funktionaalisesta näkökulmasta):

lazy val fib: Stream[Int] = Stream cons(0, Stream.cons(1, fib.zip(fib.tail).map(p => p._1 + p._2)))

fib.take(10).print
 Molemmat esimerkit tulostavat kymmenen ensimmäistä lukua Fibonacci sarjassa, molemmissa määritellään ensimmäiset kaksi lukua sarjassa, mutta siihen yhtäläisyydet loppuvatkin. Java-esimerkki kierrättää for-loopissa kunkin iteraation ja tulostaa tuloksen. Scala-esimerkki puolestaan käyttää hyväkseen zip-funktiota, joka yhdistää kaksi listaa toisiinsa, tässä tapauksessa 0 ja 1 summat kymmenen sarjassa. Eli siis 0,1; 1,1; 1,2; 2,3; 3,5... jne. Map-funktio puolestaan soveltaa p => p._1 + p._2 funktion jokaiseen zipin tuottamaan lukuun. Lopuksi vielä take(10) hakee kymmenen ensimmäistä sarjan lukua.

Scalan esimerkissä tuolla funktiosarjalla voisi edelleen toteuttaa uusia funktioita tai sen voi rikkoa osasiinsa, joita on helppo muokata ja testata, pitäen kuitenkin toiminnallisuuden samana. Tuo funktiosarja, fib, on myös itsessään yksi arvo/muuttuja ja siten uniikki, kun taas Java-versio määrittelee neljä eri muuttujaa, joista kaikki aiheuttavat mahdollisen ongelmatilanteen samanaikaisessa suorittamisessa. Jos fibonacci-metodi olisi vain osa luokkaa, jota kutsuttaisiin jostain muualta, voisi kahden yhtäaikaisen kutsun tapahtuessa yksi käyttäjä saada esimerkiksi 10 ensimmäistä lukua, haluttuaan vain 5 ja päinvastoin.

Funktionaalisen ohjelmoinnin kauneus on mielestäni siinä, että funktiot ovat luotettavia. Ne tekevät aina täsmälleen samoin ja aina täysin loogisesti. Ne voi pilkkoa pienimpiin osasiinsa, jossa ne on helppo testata. Päivätyössäni kirjoitan pääasiassa imperatiivista koodia, joka toimii välillä epäloogisen tuntuisesti, koska olion tila on muuttunut jossain huomaamattomassa kohdassa. Metodit kasvavat usein mittaviksi ja niitä on vaikea pilkkoa hallittavan kokoisiksi ja testaaminen on ajoittain vaikeaa ja hiuksia repivän turhauttavaa. Jos palaan Neil deGrasse Tysonin ajatukseen, funktionaalinen ohjelmointi tekee vaikeasta kokonaisuudesta sen verran helpompaa meidän yksinkertaisille aivoillemme.

3 kommenttia:

  1. Tämä kyseinen artikkeli täyttää ainakin osan siitä oletuksesta, että funktionaalisen ohjelmoinnin ystävät eivät osaa Javaa eivätkä ymmärrä concurrencystä tuon taivaallista. Väitös siitä, että lokaalit muuttuja aiheuttavat ongelmia yhdenaikaisuuden kanssa ei todellakaan pidä paikkaansa. Jos ko. muuttujat olisivat luokan jäseninä ja kaikki säikeet käyttäisivät samaa instanssia, niin sitten niin voisi käydä. Lokaaleja muuttujia ei koskaan jaeta säikeiden kesken.

    Funktionaalisuudesta innostuneille suosittelen lukemaan asiaan liittyvää keskustelua täältä: http://www.heikniemi.net/hardcoded/2011/06/sanko-tapahtuma-funktionaalinen-ohjelmointi-ja-f/. Kyseisessä keskustelussa voi nähdä mm. sen, että funktionaalinen ohjelmointi ei olekaan niin hirveän helppoa kun on tietynlainen ongelma. Esimerkiksi kissat ja hiiret ongelman ratkaisua ei tullut funktionaalisena versiona lainkaan ja WebCrawler'kin oli Java-toteutuksena lyhyempi mitä vastaava F#-toteutus oli. Keskusteluthredi on pitkä kuin nälkävuosi, mutta siellä on paljon asiaa ja luonnollisesti myös epäasiallisuuksia niinkuin hyvässä keskustelussa aina ;-)

    VastaaPoista
  2. Harrastelija kun minäkin olen, niin oksensin tällaisen "funktionaalisen" Java-ohjelman joka a) näyttää miltä Java 8 näyttää funktionaalisine piirteineen ja b) todistaa ettei yhdenaikaisuusongelmaa lokaaleilla muuttujilla ole. Ajaa voi jahka kääntää sen Java 8-kääntäjällä.

    t. Harrastelija


    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    import java.math.BigInteger;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;

    public class Fibonacci {
    private static final int THREAD_COUNT = 50;

    interface Action { void execute( ); }

    private static Collection fibonacci( int n ) {
    Collection series = new ArrayList<>( );
    BigInteger a = BigInteger.ZERO, b = BigInteger.ONE;
    for( int i = 0;i < n; i++ ) {
    series.add( a );
    a = a.add( b );
    b = a.subtract( b );
    }
    return series;
    }


    public static void main( String ... args ) throws Exception {
    final int n[ ] = { 500,1000,1500,2000,2500,3000,3500,4000,4500,5000 };
    // Lasketaan ensin fibonacci sarjoja yhdellä säikeellä (main thread)
    List> sresults = new ArrayList<>( );
    for( int i = 0; i < THREAD_COUNT; i++ ) {
    sresults.add( fibonacci( n[ i % n.length ] ) );
    }
    // Oukkidoukki, sitten lisätään vähän höyryä ja pistetään kaikki 50 säiettä ajoon
    ExecutorService executor = Executors.newFixedThreadPool( THREAD_COUNT );
    Collection results = new ArrayList<>( );
    for( int i = 0; i < THREAD_COUNT; i++ ) {
    int expectedN = n[ i % n.length ];
    Collection stResult = sresults.get( i );
    Future> future = executor.submit( () -> fibonacci( expectedN ) );
    results.add( () -> {
    try {
    if( stResult.equals( future.get( ) ) )
    System.out.println( "Ne ovat kuin ovatkin samat ja sarjan pituus on " + stResult.size( ) );
    }
    catch( InterruptedException | ExecutionException e ) {
    throw new RuntimeException( e );
    }
    } );
    }
    results.forEach( ( action ) -> action.execute( ) );
    executor.shutdown( );
    }
    }

    VastaaPoista
  3. Ei voikaan ajaa sillä koodista on jäänyt julkaisussa pois vähän sitä sun tätää (mm. osa generics tyypeistä).

    VastaaPoista

Kotisivu on muuttanut osoitteeseen geekgirls.fi. Kaikki vanhat (ja uudet) artikkelit kommentteineen löydät uudesta sivusta.

Huomaa: vain tämän blogin jäsen voi lisätä kommentin.