Moi!
Koska suomenkielistä kirjallisuutta Haskellista tai ylipäänsä funktionaalisesta ohjelmoinnista ei ole ollut tarjolla, kirjoitimme Selma-koiran kanssa kirjasen funktionaalisen ohjelmoinnin alkeista. Se löytyy saatesanoin osoitteesta: http://www.ideallearning.fi/index.php/blogi/68-kanaherkun-tuoksuinen-johdatus-funktionaaliseen-ohjelmointiin
Kirjan lukemisen lomassa voi katsella vaikka youtube videoita samasta aiheesta: https://www.youtube.com/watch?v=FtVCfTYid3s&list=PLaPZ0rDCxLreHHJAIqkUQbdcidaksxqMO
Toivottavasti näistä on aiheeseen tutustuessa iloa!
Selma-koira ja Juuso
Hienoa saada suomenkielistä materiaalia funktionaalisesta ohjelmoinnista. Tässä pari asiaa, mitä jäin miettimään lukiessa:
"Jos sen sijaan meidän pitää kirjoittaa ohjelma mahdollisimman lyhyesti ja ilmaisuvoimaisesti niin, että ohjelmakoodin merkitys on mahdollisimman helposti tulkittavissa, valintamme on funktionaalinen ohjelmointityyli."
Mihin tämä perustuu? Olen huomannut ohjelmointia opettaessa, että rekursiivisen koodin toiminta on erityisen vaikea ymmärtää.
"Tämä johtuu siitä, että funktionaalinen ohjelmointityyli on yleisesti tunnetuista ohjelmointityyleistä luonteeltaan lähinnä matematiikkaa – kaiken perustana on funktion käsite."
Onko todella näin? Imperatiivisessa ohjelmoinnissa kaiken perustana on muuttujan käsite – eikö se ole myös matematiikkaa?
Matematiikan "muuttuja" nimestään huolimatta tosin yleensä tarkoittaa suuretta, jolla on jokin tietty muuttumaton arvo/joukko, mutta arvo voi olla tuntematon tai määrittelemätön ennen kuin lisätään riittävästi sääntöjä (joiden esitysjärjestyksellä ei ole väliä). Tässä suhteessa matematiikka muistuttaa eniten constraint logic -ohjelmointia ja myös puhdasta funktionaalista ohjelmointia, muttei tavallisia imperatiivisten kielten muuttujia.
Kieltämättä on hieman vaikeaa motivoida funktionaalisuuden parhautta. Ehkä sopiva tulokulma olisi ajattelutapojen kehitys, eikä niinkään hyödyllisten ohjelmien tekeminen? Rekursion voi ymmärtää vain ymmärtämällä rekursion!
Antti Laaksonen kirjoitti:
"Tämä johtuu siitä, että funktionaalinen ohjelmointityyli on yleisesti tunnetuista ohjelmointityyleistä luonteeltaan lähinnä matematiikkaa – kaiken perustana on funktion käsite."
Onko todella näin? Imperatiivisessa ohjelmoinnissa kaiken perustana on muuttujan käsite – eikö se ole myös matematiikkaa?
Ohjelmointi on kai tosi yksinkertaista, koska kaiken perustana on latinalaiset aakkoset, ja aapinenhan on ensimmäinen asia, mitä ykkösluokkalaisille opetetaan... Eihän nyt muuttujaa, joka on vain yksittäinen symboli, voi mitenkään verrata funktioon.
En minä kyllä ole yhtä mieltä alkuperäisenkään väitteen kanssa, sillä funktiot ovat oleellinen osa mitä tahansa ohjelmointikieltä. Eri paradigmojen väliset erot ovat jossain muualla. Myös javascriptin perustana on funktio mutta eri mielessä.
Antti Laaksonen kirjoitti:
Mihin tämä perustuu? Olen huomannut ohjelmointia opettaessa, että rekursiivisen koodin toiminta on erityisen vaikea ymmärtää.
Onko todella näin? Imperatiivisessa ohjelmoinnissa kaiken perustana on muuttujan käsite – eikö se ole myös matematiikkaa?
Kiitos kommentista! Yritän vastata parhaani mukaan.
Tämä perustuu siihen, että painotus opetuksessa onkin korkeamman kertaluvun funktioissa rekursion sijaan. Korkeamman kertaluvun funktiot tarjoavat hyvän ja kohtuullisen vähän vuotavan abstraktion rekursion ympärille. Muutamalla rekursiomallilla (recursion patterns) ratkeaa usein kohtuullisella vaivalla suhteellisen luettavasti kaikki se, mitä usein ollaan ratkaisemassa.
Aika usein näkee ja kuulee esimerkiksi yliopistojen kursseilla painotettavan rekursiota funktionaalisen ohjelmoinnin avainkäsitteenä. Pohditaan induktioaskelia ja pähkitään rekursiivista ratkaisua. Mielenkiintoa käsitteeseen näyttää lisäävän se, että yleensä ensin on opittu imperatiivinen malli muuttujineen ja opittu alku- tai loppuehtoinen toisto tai vastaava. Mielenkiinto ohjelmointiin voi lakata rekursion vaikeuteen. Ja heti seuraavaan hengenvetoon haluan todeta, että monien hankalien asioiden ymmärtäminen voi olla hyvin hyödyllistä, mikä pätee myös rekursioon.
Yleensä rekursion painotukseen oppimisessa selityksenä on, että "se on kaiken ytimessä" tai "rekursio on funktionaalisen ohjelmoinnin tärkeimpiä käsitteitä" ja "algoritmeja ei voi optimoida, ellei ymmärrä rekursiota myös suoritusmaiseman näkökulmasta". Suoritusmaisemalla viitaan tässä siihen, miten koodi jäsentyy kohti fyysisempiä kerroksia - laiterajapintaa - jolloin keskustelu siirtyy pinomuistiin, aktivaatiotietueisiin, häntärekursioon, kääntäjiin, rekistereihin ja lopulta vaikkapa siihen kuinka heiveröinen työkalu funktionaalinen ohjelmointi on, kun haetaan optimaalista ratkaisua laskennallisen monimutkaisuuden alati kasvaessa.
Puhtaasti rekursiivisen koodin toiminta on vaikea ymmärtää, mutta korkeamman kertaluvun funktioilla, jotka voidaan käsittää myös rekursiomallien abstraktioiksi, voidaan mielestäni saada aikaiseksi sekä kohtuullisen luettavaa että myös kohtuullisen turvallista koodia. Jos rekursiivisen ajattelun oppii hyvin, on tietysti helppo ohittaa korkeamman kertaluvun funktiot ja kirjoittaa rekursiivinen ratkaisu "sellaisenaan", mutta kuten totesit, luettavuus kärsii. Voisiko rekursion jättää kirjastofunktioiden kirjoittajien huoleksi sen jälkeen, kun rekursion idean on oppinut ja ymmärtää esim. ne rajoitteet, joita rekursioon työkaluna liittyvät?
Rekursiivisten algoritmien opettaminen on vaikeaa, ja vaikeaa on rekursiivisen koodin tulkitseminenkin. Korkeamman kertaluvun funktiot, kuten map, filter, foldl/r(reduce), zip, zipWith jne. tarjoavat tietyt "vakiotavat" ja ratkaisun useimpiin ongelmiin, joita käytännön ohjelmointitöissä tulee vastaan.
Koodin luettavuus paranee, koska korkeamman kertaluvun funktio antaa vihjeen siitä, mitä tiedon kanssa ollaan tekemässä: map viittaa tiedon muuntamiseen (vrt. muuttujat imperatiivisessa mallissa), filter viittaa usein joukon rajaamiseen ja reduce voi sisältää molemmat edellisistä. Silmukoihin liittyvä luettavuusongelma helpottaa, vaikka on myös totta, että vuosia silmukoita katsellut voi aika hyvin "nähdä", mitä silmukka merkitsee - tämä koskee etenkin yksinkertaisia asioita. Jos ennen silmukkaa nähdään esim. taulukon esittely, ei liene harvinaista, että ko. taulukkoon lisätään alkioita silmukan suorituksen aikana jne. Kun koodissa nähdään map-funktio, tiedetään, että ollaan tekemässä muunnosta - nähdään yhdellä silmäyksellä operaation muoto - mapin evaluoinnin tuloksena saadaan aina parametrinsa kanssa yhdenmukainen rakenne jne.
Matematiikasta tuttu muuttujan käsite poikkeaa suuresti siitä, mitä tietotekniikan muuttujalla yleensä tarkoitetaan. Jos ajattelemme puhtaasti algoritmeja ja imperatiivista tyyliä laatia algoritmeja, aika usein keskustelu johtaa in-place -algoritmeihin, joissa edellytetään muuttujia ja enemmän tai vähemmän suoraan muistinosoitukseen. Näissä tarkasteluissa, vaikka niissä joudutaankin esittämään ohjelma mekaanisen tietokoneen hengessä (lue: muuttujat mukana), ero muuttujien ja muuttujien välillä ei ole järin kummoinen. Ammattilainen osaa kyllä rakentaa imperatiivisen esityksen matemaattisesta esityksestä tarpeen mukaan. Jotta algoritmeista saadaan tarvittaessa kaikki tehot irti, on ilman muuta mentävä muuttuvat muuttujat edellä ja on siis tarve vakavaan pohdintaan siitä, miten ohjelmakoodissa oleva muuttuja ja matemaattisessa esityksessä oleva "muuttuja" vastaavat toisiaan ja miten ja onko vastaavuustarkastelu edes mielekäs vai onko parempi laatia algoritmi "suoraan" funktionaalisista ohjelmoinnista tutuin käsittein. Tässä voi tulla kyseeseen algoritmin validiuden määrittely, mikä voi tietyin varauksin olla helpompaa funktionaalisen ohjelmoinnin käsittein.
Imperatiivisen ohjelmoinnin muuttuja ei ole määritelmällisesti matematiikkaa. Tämä ei kuitenkaan tarkoita sitä, etteikö muuttujan voisi ajatella tietyissä tilanteissa edustavan jotain, mikä matematiikassa ymmärretään muuttujaksi ja näin ratkaista käytännön ongelmia esimerkiksi algoritmeja laadittaessa. Tällä ajan takaa sitä, että on turha lähteä purismiin, jos käsiterajojen harmaalla alueella on paljon hyödyllistä, niin otetaan siitä kaikki irti. Jos joku näkee "yhtäläisyyksiä" matematiikan ja imperatiivisen ohjelmoinnin muuttuja-käsitteen välillä, otettakoon siitäkin kaikki irti. Jos kuitenkn haluaa ymmärtää syvällisemmin Churchin ja Turingin mallien erot, on ilman muuta viisasta tulkita matematiikan muuttujat ja tietotekniikan muuttujat vahvasti toisistaan poikkeavina käsitteinä ja kenties vertailla miten ne eroavat ja vielä pohtia mikä on se näkökulma, jonka valossa eroja on mielekästä vertaillla.
Tämä analogia aiheuttaa kuitenkin hankalia käytännön ongelmia, siinä kohtaa kun ajan käsite otetaan mukaan tarkasteluun. Arvon sijoittaminen muuttujaan tietyllä hetkellä "katkaisee"(lausekielisen ohjelmointikielen lause) katkaisee "näennäisesti ajattoman" (laiskan) evaluointiketjun ja altistaa ohjelman sille, että kaksi suoritusyksikköä muuttujaa käyttäessään voisivat nyt johtaa synkronointitarpeisiin. Tässä kohtaa algoritmin deklaratiivinen esittäminen ei enää ole mahdollista ja olemme usein tilanteessa, joka edellyttää erikoistoimia rinnakkaisuuden hallitsemiseksi. Tronic viittasi siihen, että suoritusjärjestyksellä ei ole väliä - näin se on. Viittaan tässä kautta rantain myös funktionaalisen ohjelmointimallin soveltuvuudesta rinnakkaisuuden hallitsemiseksi.
Uskon, että yhdelle jos toisellekin ajan käsitteen sisältyminen muuttujaan voi olla joissain tilanteissa omiaan haittaamaan matematiikan funktio-käsitteen oppimista. Tämä liittyy siihen, että funktioon liitetään helposti aika, joka ei käsitteenä funktioon liity. Ajan käsite on saatu tietysti mukaan, kun on debugattu omaa koodia, asetettu keskeytyskohtia ja "nähty", kuinka muuttuja muuttuu. Näin minäkin olen 1980-luvun alussa funktioni oppinut, mutta en suosittele tapaa muille. Ainakin on hyvä pitää jossain takataskussa pieni ajatus siitä, että ne ovat eri asioita. Sanon tämän unohtamatta sitä tosiseikkaa, kuinka hyödyllinen työkalu sellainen ajonaikainen virheidenjäljityssysteemi kuten hyvä debuggeri onkaan oli sitten kyse HW-debuggerista tai SW-debuggerista. Se on edelleen yksi parhaimpia työkaluja tutkimalla oppia, miten tila muuttuu. Oli kyse sitten Visual Basicista tai mistä tahansa. Käytän itse ja suosittelen muille.
Tässä keskustelussa nousevat nyt esiin kaksi asiaa: yhtäältä miten asiat määritellään ja toisaalta, miten käsitteet kannattaa tulkita niissä maalmoissa, joissa kukin ohjelmistosuunnittelija/opiskelija/kuka tahansa elää.
Näin opettajan näkökulmasta kysyn oppilailta ensin, mitä he haluavat oppia(esim. teoreettinen/käytännöllinen orientaatio), sitten pohdimme millainen tulokulma käsitteisiin kannattaa ottaa, jotta omat tavoitteet toteutuisivat mahdollisimman hyvin.
Painottamalla ensin funktionaalisen ohjelmoinnin rajoitteita, olen tämän jälkeen usein motivoinut oppilaita funktionaalisen ohjelmoinnin hyveisiin esittämällä React.js kirjaston esimerkkinä täysin funktionaalisesta "designista", vaikka sitä usein imperatiivisesta kielestä käytetäänkin (JS). Kun ko. kirjastoa tarkastelee, huomaa, että vaikka sen alkupään versioissa kaiken "perustana" on luokat(peritään Component kantaluokasta - klassiseen tyyliin) ja luodaan "instansseja", niin olioiden kasaamismekanismi on täysin funktionaalinen - props välitetään hierarkiassa funktiokutsuissa ja lopulta koko käyttöliittymän pystytys on yksi funktio "lapsifunktioineen". Nyt kun vuodenvaihteen (2018-2019) versiossa tiputettiin kokonaan oliot pois, on huomattu, että tilanhallinta kannattaa toteuttaa "erikseen" ja muuttujat "sellaisinaan" näyttäytyvät enää harvoin ohjelmakoodissa.
Parhautta ei kenties tarvitsekaan "motivoida", koska kysymys on ehkä aina enemmän tai vähemmän tilannesidonnainen. Motivoitakoon sitten tilannesidonnaista parhautta - siitä ohjelmoinnin soveltamisessa tuntuu kuitenkin aika usein olevan kyse.
Miten olette täällä ohjelmoinitputkassa ko. asiat oppineet tai millaisia hankaluuksia tai kynnystaitoja on pitänyt saada haltuun, että tietyt asiat ovat muuttuneet sellaiseksi "päällekäyväksi" kätevyydeksi, että on pystynyt kirjoittamaan ohjelmakoodia ja ratkomaan ongelmia? Mitä aattelette noista korkeamman kertaluvun funktioista ja niiden suhteesta rekursiivisiin "primitiivitoteutuksiin"?
Lisäys:
The Alchemist kirjoitti:
(28.09.2019 06:40:37): ”– –” Ohjelmointi on kai tosi yksinkertaista, koska...
Kiitos kommentistasi!
Funktionaalinen-imperatiivinen jako voi kuulostaa hölmöltä, kunnes määrittelee funktion käsitteen, kuten se määritellään esim. Haskell-kielessä.
Jakoja voidaan tehdä eri perusteilla, mutta funktionaalinen-imperatiivinen jaosta on ehkä eniten iloa, jos haluaa jäsentää ohjelmoinnin maailmaa yhtäältä laitteen ja toisaalta matematiikan näkökulmista samaan aikaan, vertaillen.
JavaScriptin funktiot ovat todellakin funktioita eri mielessä kuin vaikkapa Haskellissa. Se, että asioita sanotaan funktioiksi ei kuitenkaan tee niistä samoja - tämä on yleensäkin tietojenkäsittelyn opiskelussa eräs hankalimmista jutuista - raikas käsitesekamelska, jonka keskellä elämme. JavaScriptin funktioissa on kuitenkin yksi piirre, mikä tekee niistä "lievästi funktionaalisen". Se on se, että funktioita voi kutsua osissa ja funktion voi "sijoittaa" muuttujaan. Tämä toteutuu teknisesti sulkeumien/kurituksen avulla.
Näiden kahden paradigman - funktionaalisen ja imperatiivisen silmiinpistävin ero on siinä, että ne perustuvat kahdelle eri tavalle kuvata "tietokoneohjelmaa": yhtäältä "ohjelma" esitetään koneen toimintana (tietotekniikka) ja toisaalta sievennettävänä lausekkeena (matematiikka). Ensimmäisessa tavassa vastaan tulee "fyysinen" muuttuja, sillä koneen "tila" muuttuu. Jälkimmäisessä tavassa vastaan tulee "jatkuva evaluaatio", jossa "arvot" liikkuvat likuhihnalla kohtia seuraavaa käsittelyvaihetta (redusointi) - muuttujia ei ole.
Ajattelen niin, että imperatiivisten kielten funktio on erityisesti keino jäsentää koodia sopivan kokoisiin paloihin. Olio-ohjelmoinnissa luokan omat funktiot saavat vielä erityismerkityksen suhteessa olion tilan hallintaan. Mutta yhtä kaikki, funktio on erityisesti tapa "moduloida". Ohjelmoinnin kannalta funktio on varmasti yhtä lailla tapa moduloida myös funktionaalisessa kielessä (esim. Haskell), mutta iso ero tulee siinä, mitä funktiot palauttavat ja saavat parametreinaan ja aiheuttavatko ne sivuvaikutuksia. Puhtaasti funktionaalisen kielen (esim. Haskell) funktiot eivät aiheuta sivuvaikutuksia ja ne palauttavat aina arvon.
Jos kirjoitat assembly koodia ja sijoitat rekisteriin 1 arvon x ja rekisteriin 2 arvon 2. Sen jälkeen siirrät ohjelman suorituksen osoitteeseen z. Luet rekistereistä arvot x ja y ja käytät arvoja johonkin. Kun arvoilla on jotain vaikkapa laskettu, ohjelma palauttaa ohjelmalaskurin siihen tilaan, että ohjelman suoritusta jatketaan siitä osoitteesta, jossa oltiin kun hyppykäsky tehtiin. Toisin sanoen, välitit kaksi parametria rekisterien avulla(tai vaikka pinossa) ja olisit voinut "palauttaa paluuarvonkin", vaikka rekisterien avulla, muistiosoitteessa tms. Oliko kyseessä funktio? Ohjelmakohdalle voi antaa "symbolisen nimen", vaikkapa "aliohjelma" ja jos kommentoit asemblyn lomaan, että "anna rekistereihin 1 ja 2 tietyt arvot ennen kuin kutsut funktiota...". Niin olisiko sitten kyseessä funktio?
Kun lähtee siitä, että paradigmojen erot kumpuavat siitä, miten määritellään yhtäältä Turingin kone ja toisaalta lambda-kalkyyli, niin kuva selkenee ja eri ohjelmointikieliä voi sen jälkeen tarkastella paradigmoja vasten myös käytännössä.
Jako imperatiiviseen ja puhtaan funktionaaliseen on hyvä ja sillä on myös enenevissä määrin käytännön merkitystä ohjelmointitöissä. Yritin sanoa, että on funktioita ja funktioita. Toivottavasti tämä ei entisestään sekoittanut ajatuksia. Ja jos sekoitti, niin selvitetään asiaa esimerkein tms.
Tässä näyttää olevan tarkoitushakuista vastakkainasettelua ja väärinymmärrystä funktionaalisen ohjelmoinnin hyväksi. Imperatiivinen ohjelmointi kuvaillaan BASICin tasoisena rivi kerrallaan -ajatteluna, vaikka tämä kuvaus koskee lähinnä huonoa ohjelmointia. Kirjasessa imperatiiviseen ohjelmointiin viittaava lause ”kuinka monta muuttujaa ongelmaa ratkaistaessa tarvitaan” kuvaa pikemminkin esoteerista ohjelmointia kuin mitään todellista nykyaikaista ohjelmointikieltä tai -paradigmaa.
Kirjoitit, että ”iso ero tulee siinä, mitä funktiot palauttavat ja saavat parametreinaan”. Myös tämä kuulostaa BASICin aikaiselta ajattelulta, jossa aliohjelma on vain nimetty koodinpala, joka surutta muokkaa globaalia tilaa. Nykyään ei toimita näin, vaan myös imperatiivisissa kielissä funktiot saavat parametreja ja palauttavat arvoja (ja eivät muokkaa globaalia tilaa, ellei se ole funktion erityinen tarkoitus), eli ajattelutavassa ei ole paljonkaan eroa funktionaaliseen ohjelmointiin. Näitä juttuja lukiessa herää jopa kysymys, onko kirjoittaja itse ohjelmoinut imperatiivisella kielellä mitään kunnollista ohjelmaa viime vuosikymmeninä.
Kehutut ”korkeamman kertaluvun funktiot, kuten map, filter” löytyvät myös monista imperatiivisista kielistä. Ylipäänsä funktioiden käyttö on erittäin suositeltavaa myös imperatiivisessa ohjelmoinnissa. Tiedon käsittelyssä ei tarvitse olla funktionaalisiin kieliin nähden muuta eroa kuin se, että ohjelma etenee yleensä järjestyksessä työvaiheittain (eli ei ole laiskaa evaluaatiota) ja välituloksille on usein annettu nimi (joka parhaimmillaan vähentää kommenttien tarvetta). Algoritmisen ongelman ratkaisu voi olla ihan yhtä lailla imperatiivisessa kuin funktionaalisessakin ohjelmoinnissa jonkinlainen funktioiden ketju.
Nähdäkseni tuo kirjanen ei esitellyt vielä mitään asiaa, jossa funktionaalinen lähestymistapa olisi hyödyksi. Näennäinen koodin ”hyvyys” tuli parista sellaisesta Haskellin ominaisuudesta, joilla ei ole mitään tekemistä paradigman kanssa vaan jotka voisivat aivan hyvin toteutua myös imperatiivisessa kielessä. Esimerkit voisi tehdä vastaavina parin rivin koodeina vaikkapa Pythonissa; lähinnä siististä tulostamisesta tulisi pientä lisävaivaa.
Jos funktionaalista ohjelmointia pitää kehua, näkisin mielelläni konkreettisia esimerkkejä, missä se on kirjoittajan mielestä niin hirmu kätevä, ja esimerkit saisivat olla jotain muuta kuin yksittäisiä erikoistapauksia.
Olisi hyvä muistaa mainita myös funktionaalisen ohjelmoinnin suhteellinen vaikeus ohjelmissa, joihin liittyy tila, aika ja käyttäjän syöte. Imperatiivinen olio-ohjelmointi sopii hyvin vaikkapa graafisen käyttöliittymän ohjelmointiin, mutta miltä näyttää funktionaalinen ratkaisu?
Higher order functions eli funktiopointterit ovat jo C-kielessä jostain 1970-luvulta, ja likipitäen kaikissa nykyisissä kielissä paradigmasta viis.
Lisäys:
Vaikka lambdat olivat imperatiivisissa kova juttu 2010-luvun alussa, on viime vuosina on jopa alettu siirtyä pois higher order funktioista (callbackeista), toisaalta generaattorilausekkeiden myötä ja nyt viimeisimpänä asynkronisen ohjelmoinnin saralla (async/await on oikeastaan täysin sama konsepti kuin generaattori/yield). Otetaas vielä pieni funktionaalishenkinen koodiesimerkki Pythonilla:
# generaattori def fibonacci(): a, b = 0, 1 while True: yield b a, b = b, a + b # "filter" ja "map" generaattorilauseella fibo_div3 = (n / 3 for n in fibonacci() if n % 3 == 0) # lista 10 ensimmäisestä kolmella jaollisesta Fibonaccin luvusta, kolmella jaettuina [next(fibo_div3) for _ in range(10)]
Kuten funktionaalisissa, suoritus on laiskaa, eikä kaadu päättymättömään sarjaan, vaan Fibonaccin lukuja lasketaan vasta viimeisellä rivillä ja vain sen verran, että on saatu 10 ehdot täyttävää lukua. Generaattorilause on niin paljon kätevämpi, että se on korvannut Pythonista edelleen löytyvät filter- ja map-funktiot.
Tronic kirjoitti:
Kuten funktionaalisissa, suoritus on laiskaa, eikä kaadu päättymättömään sarjaan, vaan Fibonaccin lukuja lasketaan vasta viimeisellä rivillä ja vain sen verran, että on saatu 10 ehdot täyttävää lukua. Generaattorilause on niin paljon kätevämpi, että se on korvannut Pythonista edelleen löytyvät filter- ja map-funktiot.
Tämän voi tietysti aina korvata perinteisessä imperatiivisessä ohjelmointikielessä yksinkertaisella iteratiivisella toteutuksella. Alla versio 8th ohjelmointikielellä, mihin lisäsin mahdollisuuden määrittää mistä luvusta lähdetään liikkeelle. Isoilla luvuilla koodin nopeuskin on kohtuullisen hyvä.
: bits? \ n -- nbits-1 n:ln 2 n:ln n:/ n:int ; : fibo-loop >r \ Put loop counter on r-stack \ a b 2dup 2 n:* \ a b a 2b over n:- \ a b a (2b-a) n:* -rot \ d=(2b-a)*a a b n:sqr swap n:sqr n:+ \ d=(2b-a)*a e=b*b+a*a r> r@ swap n:shr 1 n:band if dup \ a b b rot \ b b a n:+ \ b c=b+a then ; : fibo \ n -- fibo(n) >r \ Put n on r-stack 0 1 \ a b ' fibo-loop 1 r@ bits? loop- \ a b r> 1 n:band if n:sqr swap n:sqr n:+ \ b*b+a*a else 2 n:* over n:- n:* \ (2b-a)*a then ; : generate-fibo \ start num -- a >r \ Put end limit to r-stack a:new \ start a swap \ a start repeat dup \ a start start fibo \ a start result dup \ a start result result 3 n:mod \ a start result n 0 n:= if \ a start result rot \ start result a swap \ start a result 3 n:/ a:push \ start a a:len \ start a n r@ n:= if nip \ a break else swap \ a start n:1+ \ a start+1 then else \ a start result drop \ a start n:1+ \ a start+1 then again rdrop ; \ a : app:main 1 10 generate-fibo ( . space ) a:each! drop bye ;
Metabolix kirjoitti:
(29.09.2019 14:30:50): Tässä näyttää olevan tarkoitushakuista...
Kiitos kommentistasi!
Kirjasen alku johdattelee "tiedon käsittelyyn" liittyviin seikkoihin ja alkuosan lopussa todetaan, että väline (ohjelmointikieli jne.) tulee valita aina kulloisenkin tarpeen mukaan. Kirjoitin sen erityisesti siksi, ettei tulisi mielikuvaa, että kyseisellä ohjelmointityylillä ratkaistaisiin kaikki "ongelmat".
Kun viittasin siihen, mitä funktiot saavat parametreinaan ja palauttavat, viittasin siihen, että ne voivat sekä saada funktioita parametreinaan että palauttaa funktioita. En viitannut siihen, että ohjelmointikielissä ei parametreja olisi tai että jonkin ohjelmointikielen funktiot eivät palauttaisi mitään.
Kirja ei ota kantaa olio-ohjelmointiin ja sen soveltuvuuteen eri tilanteissa. Kirja ottaa kantaa funktionaalisen ohjelmoinnin soveltuvuuteen tiedon käsittelyyn liittyvissä ongelmissa. Tiedon käsittelyyn liittyvässä ongelmassa meillä on tietoa, josta luodaan uutta tietoa. Tiedon fyysinen lähde tai kohde ei ole mielenkiinnon kohteena. Kirjassa ei oteta kantaa tiedon syöttö -tai tulostusjärjestelmiin sivuvaikutuksineen, olivatpa ne mitä tahansa.
Jokainen lukija voi miettiä itse, mitä hyötyä tai haittaa funktionaalisesta ohjelmoinnista ja siihen liittyvistä käsitteistä voi omalla kohdalla olla. Ja ihan yhtä hyvä on toteamus, että "on hyötyä" kuin että "ei ole hyötyä". Samalla tavoin jokainen voi miettiä, mitä iloa on siitä, että muuttujia ei käytetä ja mitä hankaluuksia voi tulla ihan tilanteesta riippuen.
Kirjan ajatuksena on korostaa funktionaalisen ohjelmoinnin merkitystä tiedon käsittelyn näkökulmasta, ei tiedon esittämisen näkökulmasta tai esimerkiksi fyysisen laitteen ohjaamisen näkökulmasta.
Selma-koira kirjoitti:
Kirja ottaa kantaa funktionaalisen ohjelmoinnin soveltuvuuteen tiedon käsittelyyn liittyvissä ongelmissa.
On vaikea perustella tietyn ratkaisun soveltuvuutta, jos sitä ei millään tavalla vertaa muihin vaihtoehtoihin. Jos sahalla saa naulan hakattua seinään, onko silloin saha on tähän sopiva työkalu?
Kirjan esimerkit ovat niin suppeita, että niistä on vaikea nähdä, miten funktionaalisesta ohjelmoinnista olisi hyötyä tai miten se edes eroaisi imperatiivisesta. Uskon, että soveltuvuuden perusteluun ja imperatiivisen koodarin oivallusten herättämiseen tarvitaan jotain aika paljon syvällisempää kuin ”lelutAakkosjärjestyksessä = sort selmanLelut”.
Selma-koira kirjoitti:
Jokainen lukija voi miettiä itse, mitä hyötyä tai haittaa funktionaalisesta ohjelmoinnista ja siihen liittyvistä käsitteistä voi omalla kohdalla olla.
Minulle jää epäselväksi (nyt selityksesi jälkeen vielä entistä enemmän), mikä on kirjan kohderyhmä. Kirjan tyyli on kuin lastenkirjassa: Selma-koira ja Pate-koira juttelevat ja laittavat lelut ja lempiruoat listaksi. Kuitenkin sitten sanot, että lukija voi miettiä itse hyötyjä ja haittoja. Kuvittelisin, että jos lukijalla on valmiuksia imperatiivisen ja funktionaalisen ohjelmoinnin vertailuun, hän todennäköisesti kaipaa tietolähteeksi jotain vakuuttavampaa kuin kahden koiran leikkisää vuoropuhelua, jossa funktionaalisen ohjelmoinnin keskeisin käsite eli funktio on korvattu sanalla kone.
Metabolix kirjoitti:
(29.09.2019 22:25:51): ”– –” On vaikea perustella tietyn ratkaisun...
Kiitos kommentistasi Metabolix!
Koska kohderyhmänä ovat kaikki ohjelmoinnista kiinnostuneet, ei kirjassa edellytetä, että ohjelmointi olisi jo ennestään tuttua. Samasta syystä kirjassa ei vertailla eri ohjelmointityylejä - esitetään yksi erinomainen vaihtoehto sellaisiin tilanteisiin, jossa ohjelmointiongelma koskee erityisesti tiedon käsittelyä.
Kirjan esitysmuodolla ei ole tarkoitus sulkea pois kohderyhmää tai toisaalta sisällyttää kohderyhmää. Esitysmuotovalintaa pohtiessani en myöskään ole miettinyt vakuuttavuutta.
Lukija voi toki vertailla imperatiivista tai funktionaalista tyyliä ja samaan aikaan lukea joko Selman ja Paten edesottamuksista tai lukea jotain muuta ja käyttää tietonsa pohjana parhaaksi katsomiaan tietolähteitä. En näe, että esitysmuoto tai leikkisyyden määrä sinänsä tekee asiasta kuin asiasta enemmän tai vähemmän vakuuttavaa.
Mutta koska täällä ohjelmointiputkassa lienee paljonkin ohjelmoijia, jotka tuntevat useita eri ohjelmointikieliä ja -menetelmiä, niin keskustelua voinee täällä hyvän jatkaa matkaa ja vaikka lähteä vertailemaan ohjelmointityylejäkin ja käymään keskustelua aiheesta.
Yksi hyvin käsinkosketeltava hyöty funktionaalisessa ohjelmoinnissa tietojenkäsittelyongelmiin liittyvien ratkaisujen laatimisessa on se, että tilaton, funktionaalinen koodi on huomattavasti helpompaa testata kuin vastaava tilallinen.
Koska funktionaalisessa ohjelmassa ei ole tilan käsitettä, funktio ei voi koskaan muuttaa tilaa. Koska funktio ei voi muuttaa tilaa eikä se riipu tilasta, se saa aina aikaan saman lopputuloksen samoilla parametreilla.
Tässä lyhyt esimerkki:
funktio (tietokantaKahva) { tietokannastaTullutTieto = käytäTietokantaa(tietokantaKahva) järjestettyTieto = sort tietokannastaTullutTieto palauta järjestettyTieto }
Imperatiivinen ohjelmointimallia mahdollistaa edellisen kaltaisen funktion kirjoittamisen, jossa funktion tehtävänä on järjestää tietoa. Malli ei ota kantaa siihen, mitä funktioiden sisällä tapahtuu - siellä voi tapahtua mitä vaan. Jos tietokantayhteys katkeaa, testi epäonnistuu, vaikka funktion tarkoituksena on järjestää tietoa. Testi myös epäonnistuu, jos tietokantayhteys ei koskaan aukea. Testi epäonnistuu myös, jos tietokannan salasana on vaihdettu, eikä testaaja ole siitä tietoinen. Näin on tullut kirjoitettua koodia, joka on riippuvainen seikoista, jotka eivät mitenkään liity siihen, mitä koodin avulla halutaan saada aikaiseksi.
Funktion tehtävänä ei kuitenkaan ole käyttää tietokantakahvaa viitatakseen viittauslasketun säiliön takana piileskelevään rajapintaan, jonka takana on TCP-yhteys, joka toimii kommunikaatiokanavana kahden järjestelmän välillä. Tuon funktion tehtävänä on järjestää tietoa.
Funktionaalinen ohjelmointi johtaa ajatteluun, jossa kaikki sellaiset funktiot, jotka eivät liity sivuvaikutuksiin, pidetään puhtaina ja täten helposti testattavina.
Helpompi testaus on omiaan johtamaan siihen, että laaja ohjelma kannattaa tehdä niin, että sivuvaikutuksia(tietovaraston käyttö, datan vastaanottaminen verkosta ja niin edelleen) aihettava osa ja sivuvaikutukseton osa pidetään erillään. Näin tiedetään, mikä ohjelmanosa on helposti testattavissa ja mikä ohjelmanosa liittyy sivuvaikutuksiin ja tarvitsee erilaista kohtelua, mitä järjestelmän ylläpitoon ja kehitykseen tulee.
Funktionaalinen ohjelmointityyli ilman sivuvaikutuksia ei siis näy pelkästään yksittäisillä koodiriveillä, vaan se näkyy myös siinä, miten järjestelmä kokonaisuudessaan on viisasta suunnitella ja toteuttaa. Useissa järjestelmissä on paljon eri työkaluja (kieliäkin), joista jokainen palvelee kokonaisuutta parhaalla mahdollisella tavalla suhteessa tarpeisiin. Yksi merkittävä tarve nykyaikaisissa tietojärjestelmissä on tarve ylläpitää ja kehittää järjestelmää. Tässä tehtävässä testaus on avainasemassa.
Lisäys:
Tronic kirjoitti:
Higher order functions eli funktiopointterit ovat jo C-kielessä jostain 1970-luvulta, ja likipitäen kaikissa nykyisissä kielissä paradigmasta viis.
Niin. Assemblerilla onnistuu myös. Ohjelmanosasta a lienee voinut kommunikoida ohjelmanosaan b, niin että ohjelmanosa b voi siirtää ohjelmalaskuria kohtaan, joka on määritelty ohjelmanosassa a vaikkapa sijoittamalla rekisteriin y arvon z, joka viittaa siihen muistiosoitteeseen, josta koodia halutaan seuraavaksi suoritettavan.
Eli jos ajatellaan, että c-kielen funktio-osoitin käy korkeamman kertaluvun funktiosta, niin yhtä hyvin voitaneen ajatella, että vastaava assembler koodi ajaa saman asian. Teknisesti hypätään suorittamaan koodia alkaen osoitteesta y.
Kyse korkeamman kertaluvun funktioiden käyttämisessä ei kuitenkaan ole yksin siinä, onko se mahdollista, vaan myös siinä, onko se järkevää käytännössä.
Esimerkiksi Javassa korkeamman kertaluvun funktioiden käyttö on hyvin hankalaa erityisesti siksi, että jokainen kielen metodi on aina ripustettu luokkaan. Jotta funktioita voisi käyttää, tulee käsiteltävä data aina valmistella käsiteltäväksi Stream-rajapinnan kautta, sillä kielen muut rakenteet ja vaatimukset taaksepäin yhteensopivuudesta siihen pakottavat.
Paradigma ei ymmärtääkseni tarkoita, että jokin kieli voidaan luokitella mihin paradigmaan tahansa, jos sen avulla on teknisesti mahdollista tehdä kaikki ne asiat, jotka ovat mahdollisia kaikissa muissa ohjelmointikielissä. Näinhän voisimme luokitella konekielen mihin tahansa paradigmaan, koska kaikki kielethän lopulta suoritetaan jossain koneessa? Konekieli tekee sen, minkä kaikki muutkin kielet.
Noi generaattorit on käteviä. JavaScriptin kanssa on vuosien varrella tullut tehtyä yhtä ja toista ja aina jotain sellaista, jossa ollaan riippuvaisia ulkoisista tietolähteistä - joko WebSocket- tai HTTP-protokollan toisessa päässä. Ei ole tullut ikävä monipolvisia koodinpätkiä, joissa on kymmenkunta sisäkkäistä oikealle sisennettyä koodilohkoa, joissa odotellaan lupausten täyttymistä odotetusti tai virhetilannetta. Ei sitä ylläpidä erkkikään. Callback hell on ihan hyvin kuvannut tilannetta.
Aihe tuli ensimmäistä kertaa vastaan C++:ssa co-routine käsitteen nimellä kollegan kautta muutama vuosi takaperin. Se on mukava homma, että ne on nyt ES6:ssakin. Työvaiheiden onnistumisista riippuvan työjonon toteuttaminen on nyt helppo nakki ja ko. menetelmä tukee hyvin niitä tarpeita, joita nykyään on paljon - tiedon hakua ja yhdistelyä eri lähteistä - sellaisen datan pyörittelyä, jonka saatavuudesta ei aina ole takeita.
C:n vertaaminen tässä suhteessa konekieleen on melko raakaa, kun C:n funktiopointteri kuitenkin on toiminnallisesti juuri vastaava kuin se funktionaalisissa käytettävä ratkaisu. Samaa ei voi sanoa stackin ja osoitteiden nysväämisestä käsipelillä assemblyssä ihan jo tavallista funktiokutsua varten.
Nimenomaisesti Java on se kieli, josta tuo toiminnallisuus puuttui pitkään, ja ylipäätään olio-ohjelmoinnin pakottaminen on jo yleisesti todettu pahaksi virheeksi[1]. Java-johdannaiset kielet kuten C#, Scala ja Kotlin ovatkin sitten tahoillaan korjanneet näitä ongelmia. Javassa 8-versiosta eteenpäin ja kaikissa johdannaisissa on lambdat ja suhteellisen fiksut tavat välittää niitä parametreina toisille funktioille.
Javascriptissä callbackeista -- siis funktioiden välittämisestä funktioille -- ollaan luopumassa aivan erityisen suurella innolla, juurikin mainitsemiesi ongelmien vuoksi. async/await mahdollistaa imperatiivisen ohjelmointityylin, mitä pidetään huomattavasti helpompana (eikä tule sisennyksiä). Promiset ovat silti ongelmallisia ja seuraava vaihe näyttäisi olevan viime vuonna lanseerattu structured concurrency[2][3].
1) https://www.google.com/search?q=object-oriented programming harmful
2) https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/
3) https://www.youtube.com/watch?v=Mj5P47F6nJg
Metabolix kirjoitti:
Näitä juttuja lukiessa herää jopa kysymys, onko kirjoittaja itse ohjelmoinut imperatiivisella kielellä mitään kunnollista ohjelmaa viime vuosikymmeninä.
En tuohon osaa sanoa. Mikä sinuulla laukaisi sellaisen kysymyksen, että osaako joku, tässä tapauksessa minä, tehdä jotain "kunnolla"? Oliko siinä kirjassa jotain sellaista, joka herätti sinussa nämä kyssärit? Olisi mukavaa tietää, mistä kysymys kumpuaa - koska jostainhan se on kummunnut. Otan sen toki palautteena, kun mietin kirjan sisältöä ja seuraavia versioita. Kerään kaiken palautteen - myös tämän ja kokoan ne yhteen ja käyn sitten aikanaan läpi.
Koska epäilyksesi kuitenkin koski minua yksityishenkilönä, niin asia kiinnostaa myös siltä kantilta - siksikin, että minähän tuon kirjan kirjoitin. Oliko siellä kirjassa esim. paljon asiavirheitä tai mistä epäilyksesi johtuu? Onko verkkosivuillamme ehkä jotain, joka johtaa sinut ajattelemaan, että joku ei osaa jotain? Mikä olisi sellaista asiaa, joka toisaalta saa sinut uskomaan, että henkilöllä on jokin tietty taito?
Palautteestasi kiittäen!
Lisäys:
The Alchemist kirjoitti:
En minä kyllä ole yhtä mieltä alkuperäisenkään väitteen kanssa, sillä funktiot ovat oleellinen osa mitä tahansa ohjelmointikieltä. Eri paradigmojen väliset erot ovat jossain muualla. Myös javascriptin perustana on funktio mutta eri mielessä.
Haskell-kielen funktio on käsitteenä paljon lähempänä matematiikan funktio-käsitettä kuin vaikkapa JavaScript-kielen funktio. Lause tarkoittaa, että kaiken perustana on matematiikan funktio käsite - ei jonkin ohjelmointikielen funktio-käsite. Haskellin "funktio" pääsee aika lähelle matematiikan "funktiota". Siksi edellisessä lauseessa viitataan matemaattiseen luonteeseen - jotta yhteys syntyisi matematiikan funktio-käsitteeseen.
Tätä pitää ehkä kirjassa tarkentaa, mutta toisaalta, koska kohderyhmä on laaja, on oletus, että funktio käsitetään erityisesti matematiikan terminä - se lienee kuitenkin suurimmalle osalle lukijoista se tutumpi tulkinta. Ja jos ei tunne funktion käsitettä, niin aina voi ajatella asiat yksinkertaisina koneina tekemässä duunia koirakaverien puolesta - ei sekään väärin ole.
Kiitos tästä kommentista myös!
Selma-koira kirjoitti:
Mikä sinuulla laukaisi sellaisen kysymyksen, että osaako joku, tässä tapauksessa minä, tehdä jotain "kunnolla"? Oliko siinä kirjassa jotain sellaista, joka herätti sinussa nämä kyssärit? Olisi mukavaa tietää, mistä kysymys kumpuaa - koska jostainhan se on kummunnut.
Kommentti liittyi siihen, mitä muutenkin kyseisessä kohdassa viestiäni kommentoin: Aikaisemmasta kirjoituksestasi sai käsityksen, että mielestäsi imperatiivisen kielen funktioiden merkitys olisi vain koodin paloittelu ja että parametrit ja paluuarvot olisivat jotenkin spesifisti funktionaalisen ohjelmoinnin piirre. Tästä ymmärrettävästi heräsi huoli, miten vanhanaikainen käsitys sinulla on imperatiivisesta ohjelmoinnista. Onneksi täsmensit myöhemmin, että tarkoititkin aivan muuta (eli funktioiden välittämistä toisille funktioille). Sinänsä keskustelu sujuisi paremmin, jos viesteihin kirjoittaisi suoraan sen, mitä tarkoittaa, eikä jättäisi asioita lukijan mielikuvituksen varaan.
Selma-koira kirjoitti:
Kirjan esitysmuodolla ei ole tarkoitus sulkea pois kohderyhmää tai toisaalta sisällyttää kohderyhmää.
Toisaalta esitysmuoto rajaa todennäköisesti jollain tavalla kohderyhmää – tarkoituksella tai ei –, joten mielestäni esitysmuodon vaikutusta kannattaa miettiä.
Metabolix kirjoitti:
(30.09.2019 18:37:41): ”– –” Kommentti liittyi siihen, mitä muutenkin...
Ok. Tämä selvä.
Ymmärtääkseni pohdit, olisiko jotain sellaisia seikkoja, jotka saattaisivat saada ohjelmoijan ajattelemaan, josko funktionaalisesta ohjelmoinnista olisi jotain hyötyä. Mitä mieltä olet, onko tuo testausta koskeva asia sellainen ja jos on, niin mitä mieltä olet asiasta? Onko siitä "hyötyä"?
Funktioita ilman sivuvaikutuksia on mahdollista ja järkevää tehdä myös imperatiivisessa ohjelmoinnissa, joten niihin liittyviä hyötyjä (kuten testaamisen helppoutta) ei voi pitää erityisesti funktionaalisen ohjelmoinnin etuna.
Jos mielestäsi on ongelma, että tietokantahaku voi epäonnistua, niin näytä sitten parempi ohjelma, jossa se ei voi epäonnistua. Jos ratkaisusi on, että haku jätetään tekemättä, niin tämä ratkaisu onnistuu aivan hyvin myös imperatiivisessa ohjelmoinnissa eikä todista mitään funktionaalisesta ohjelmointityylistä sinänsä.
Moi Metabolix ja kiitos taas kommentistasi!
Ongelma ei ole, että tietokantahaku voi epäonnistua, vaan, että funktio on vaikeasti testattavissa. Yritin edellisessä kommentissani korostaa, että testaus helpottuu, en sitä, että yritettäisiin tehdä mahdottomasta mahdollista.
Jos alat ohjelmoijan urasi tai ohjelmointiharrastuksen ylipäänsä ohjelmointikielellä, joka johdattelee sellaiseen turvalliseen ohjelmointityyliin, missä sivuvaikutusten huomioiminen näkyy kielen rakenteissa alusta alkaen, olet aika paljon vahvemmilla mitä koodisi turvallisuuteen tulee (sivuvaikutukset hallinnassa) kuin olisit, jos lähdet liikkeelle työkalulla, jossa asiaan ei ole kiinnitetty sen kummemmin huomiota. Siksi ensimmäiselläkin ohjelmoinikielellä voi olla aika paljonkin väliä.
Mm. tästä syystä joissain moniparadigmakielissä, kuten Rustissa muuttujat ovat oletusarvoisesti muuttumattomia. Scalassa funktion parametrit ovat oletusarvoisesti muuttumattomia - et voi oletusarvoisesti muuttaa arvoa funktiosta käsin.
Jos funktio ilman sivuvaikutuksia on mielestäsi järkevää, niin miksemme ole nähneet ohjelmointiputkassa yhtään keskustelua, jossa todettais, että "hei, tää on muuten järkevää, miksei tehtäis näin" tai "hei, tota koodia vois vähän vielä pohtia, että kuinka helppoo se on testata" tms. Syy voi olla ehkä se, että koska kielen rakenteet eivät siihen johdattele, asia jää helposti pohtimatta. Silloin tulee helposti pohdittua muita juttuja, jotka ovat nekin toki tärkeitä, koodin luettavuudesta alkaen.
Se, mitä haluan korostaa on, että turvallinen, helposti testattava ja ylläpidettävä koodi ei lienee haitaksi ja funktionaalisen ohjelmointityylin keskeiset ajatukset, kuten sivuvaikutusten eristäminen, toteutettuna funktionaalista ohjelmointityyliä tukevilla työkaluilla on helpoin tie siihen.
Se voi olla myös yksi hyvistä vastauksista työhaastattelussa kysymykseen: "Millä keinoin saat koodistas turvallista ja ylläpidettävää?". Mitä tuumaat?
Selma-koira kirjoitti:
Ongelma ei ole, että tietokantahaku voi epäonnistua, vaan, että funktio on vaikeasti testattavissa. Yritin edellisessä kommentissani korostaa, että testaus helpottuu, en sitä, että yritettäisiin tehdä mahdottomasta mahdollista.
Luulen, että Metabolix tarkoittaa ettei sivuvaikutuksettomat "pure" funktiot ole vain funktionaalisen ohjelmointikielen yksinoikeus vaan enemmänkin ajattelumalli mitä voi soveltaa aivan hyvin nykypäivän imperatiivisillä ohjelmointikielilläkin.
Itse pidän funktionaalisella ohjelmointikielellä kirjoitettua ohjelmaa vaikeampi lukuisena kuin perinteisellä imperatiivisisella ohjelmointikielellä.
Yksi asia minkä olen huomannut, että funktionaalisissa ohjelmointikielissä muistin käyttö on kohtuullisen suuri ja suorituskyky jää perinteisistä imperatiivisista ohjelmointikielistä.
Sinänsä toki jos ajattelee "pure" ominaisuutta niin sehän on ikäänkuin varmistin, joka estää ampumasta itseään jalkaan vahingossa.
Keskustelun perusteella tulkitsisin että (monissa) funktionaalisissa ohjelmointikielissä se on oletuksena päällä, ja ohjelmoija voi kytkeä sen pois tarvittaessa, kun taas imperatiivisissa ohjelmointikielissä se ei (ainakaan useimmiten) ole oletuksena päällä.
Sinänsä voisi ajatella että varmistimen päällä olo oletuksena on parempi (ei unohdu laittaa päälle), mutta mikäänhän ei estäisi tekemästä imperatiivista ohjelmointikieltä, jossa tuo varmistin olisi oletuksena päällä. Näin ollen en näkisi tätä funktionaalisen ohjelmoinnin eduksi sinänsä, mutta toki se voidaan lukea useiden funktionaalisten ohjelmointikielten eduksi.
jalski kirjoitti:
Yksi asia minkä olen huomannut, että funktionaalisissa ohjelmointikielissä muistin käyttö on kohtuullisen suuri ja suorituskyky jää perinteisistä imperatiivisista ohjelmointikielistä.
Näin se on. Muistinkäyttö on erilaista, koska luodaan huonoimmilaan paljon kopioita, parhammaimmilaan käytetään olemassaolevia rakenteita ja kopioidaan vain se, mikä on pakko. Scalassa on paljon optimointia tuon suhteen, muttei se koskaan voi "siinä lajissa" pärjätä kielelle, jossa muistia osoitetaan suoremmin. Aina tehdään kompromissi. Olisko sulla jotain koodia, minkä kanssa oot tehny hommia?
Mikä sun mielestä tekee fp:stä (vaikka nyt Haskell tms.) vaikealukuisempaa? Olisko se vaikealukuisempaa jos oisit ekaks opetellu jonkin funktionaalisen kielen vai onko siinä fp:ssä jotain sellaista, että se on jotenkin hankalampaa vaan luonnostaan? Toi on se, mitä ite mietin ennen aloin Haskellia kokeilemaan. Tuntui, että omalla kohdalla syy oli, että olin niin pitkään tehnyt toisella tavalla, niin poisoppiminen vei aikaa.
Lisäys:
Grez kirjoitti:
Sinänsä toki jos ajattelee "pure" ominaisuutta niin sehän on ikäänkuin varmistin, joka estää ampumasta itseään jalkaan vahingossa.
Toi on just se idea. Sitten meillä on vielä C++:n const_cast operaattori. Eli C++:lla voidaan ensin kirjoittaa koodia, jossa kommunikoidaan, että "hei, tää funktio ei sit muuta olion tilaa". Jos yks devaaja on sitä mieltä, että funktio on const, niin joku voi jossain rikkoo ton sopimuksen vaikka niin, että const_castaa this pointterin ja muuttaa ei-const jäsenmuuttujan arvoa oliossa. Siinä mielessä tuo jättää jalkaan ampumisen mahdollisuuden usemalla tavalla. C++ lienee eräs monipuolisimpia ohjelmointikieliä, mutta tuosta näkökulmasta se voi olla ongelmallinen. Tulee vaikeaksi tulkita, mikä tässä nyt muuttuu ja mikä ei muutu.
Puhtaassa funktionaalisessa kielessä ei ole edes mahdollisuutta muuttujien päälle kytkemiseen. Siinä sivuvaikutukset hoidetaan usein monadisin rakentein (esim. Haskell ja Scala fp-kirjastoilla).
Ideana ei oo mollailla eri ohjelmointikieliä tai paradigmoja, vaan löytää sitä tällä palstalla toivottua "syvällisempää" juttua, mikä voisi saada pohtimaan onko fp:stä hyötyä omalla kohdalla vai ei, sehän riippuu siitä mitä tekee. Jos tekee matalan tason assujuttuja ja kirjoittaa suoraan CPU:lle, niin ei kai siihen fp oikein istu. Tai jos joku värkkäilee debuggailee Lauterbachin kanssa sulajuttuja niin ei siinäkään fp tuu usein oikein kyseeseen.
Korostan, että koko keskustelu alkoi johdatuksesta funktionaaliseen ohjelmointiin ja nimen omaan tiedon käsittelyn kannalta. Ja pyrin siinä pysymään, niin mielenkiintoisia kuin laitelähtöiset jutut ovatkin.
Selma-koira kirjoitti:
Toi on se, mitä ite mietin ennen aloin Haskellia kokeilemaan. Tuntui, että omalla kohdalla syy oli, että olin niin pitkään tehnyt toisella tavalla, niin poisoppiminen vei aikaa.
Eihän siinä periaatteessa pitäisi olla mitään "poisoppimista", koska molempi tyyli pitäisi osata, jos haluaa valita työkalun tehtävän mukaan.
Grez kirjoitti:
Selma-koira kirjoitti:
Toi on se, mitä ite mietin ennen aloin Haskellia kokeilemaan. Tuntui, että omalla kohdalla syy oli, että olin niin pitkään tehnyt toisella tavalla, niin poisoppiminen vei aikaa.
Eihän siinä periaatteessa pitäisi olla mitään "poisoppimista", koska molempi tyyli pitäisi osata, jos haluaa valita työkalun tehtävän mukaan.
Joo. Tarkoitin sitä, että kun on vuosia kylvänyt huolimattomasti muuttujia vähän mihin sattuu, niin kun yks kaks todetaan, että "hei, lopetas toi kokonaan", niin joutuu perustavalla tavalla ajatteleen asiaa uudelleen. Fp:n pohdiskelun etuna on ollut myös se, että on joutunut kyseenalaistamaan esim. monet Javasta ja C++:sta tutut olio-ominaisuudet, kuten perinnän ja luokkien välille syntyvät monet riippuvuudet suhteessa niistä saataviin hyötyihin. Sitä alkaa miettiin, että pitäiskö kaikki kasatakin ilman olioita. Ne on mun mielestä niitä vähän "syvällisempiä" juttuja, mitä ei voi funktionaalista ohjelmointia raapaisevaan kirjaan sisällyttää.
Paulo Villela selvittää tässä videolla asiaa kohdassa 13:00 eteenpäin. https://www.youtube.com/watch?v=-Q4iuERY28Q. Eihän tuo projekti mikään suuri suksee tainnut olla, mutta tekemällä ja kokeilemalla noitakin vain oppii niin isot kuin pienetkin firmat.
Tuskin funktionaalinen ohjelmointi(kieli) poistaa huonon koodarin dilemmaa, liekö edes tekee sitä vähemmän merkitseväksi seikaksi. Jos vanhastaan on tottunut vain "kylvämään muuttujia" ja osaa olio-ohjelmoinnin suunnittelumalleista lähinnä luokkien periyttämisen, niin totta kai koodi on huonoa.
Metabolix kirjoitti:
Kirjasessa imperatiiviseen ohjelmointiin viittaava lause ”kuinka monta muuttujaa ongelmaa ratkaistaessa tarvitaan” kuvaa pikemminkin esoteerista ohjelmointia kuin mitään todellista nykyaikaista ohjelmointikieltä tai -paradigmaa.
Mitä esoteerista lauseessa on tai toisaalta mitä termi "esoteerinen" tässä yhteydessä tarkoittaa? Muuttujien määrän ja laadun pohdinta on ainakin minulle ollut tyypillistä.
Niin minä ainakin aina olen lähtenyt siinä paradigmassa liikkeelle ohjelmoidessani:"jaahas.. tarvitaan muuttuja tohon luuppiin ja sit pari muuttujaa, johon sijoitetaan noi datat, mitä tulee tietokannasta ja sit heitetään ne muuttujat johonkin muualle. Tohon mä tarviin yhen temp muuttujan, että mä saan noi arvot swapattua ja yhden muuttujan joka kuvaa tota ikkunakontekstia, jonka kautta mä päivitän näyttöä...". Oon miettiny päässäni muuttujia, kontrollirakenteita ja sopivia tietorakenteita ajassa etenevänä prosessina. Sellaiselta se ajattelu on kuitenkin tuntunut. Mikä siinä on se esoteerinen osa ja mitä sillä tarkoitat?
Lisäys:
The Alchemist kirjoitti:
(02.10.2019 14:04:22): Tuskin funktionaalinen...
Olio-ohjelmointiin liittyvät riippuvuusongelmat(paljon riippuvuuksia asioiden välillä) eivät ymmärtääkseni liity erityisesti perintään, ne liittyvät siihen, että oliot riippuvat toisistaan ylipäänsä - suunnittelumallilla ei ole niin väliä, syntyneiden riippuvuuksien määrällä ja laadulla on. Ongelmat ilmenevät koodia päivitettäessä, tehdään esim. kantaluokkaan muutos, eikä osata ennustaa, mihin kaikkeen se vaikuttaa tms. Kyse ei tässä siis ole siitä, osataanko olio-ohjelmointi fiksusti, vaan siitä, että ohjelmointimalli on omiaan johtamaan suureen märää riippuvuuksia ohjelman osien välillä, mikä voi olla vaikea ylläpitää.
Mikä on huonon koodarin dilemma?
Selma-koira kirjoitti:
Muuttujien määrän ja laadun pohdinta on ainakin minulle ollut tyypillistä.
Niin minä ainakin aina olen lähtenyt siinä paradigmassa liikkeelle ohjelmoidessani:"jaahas.. tarvitaan muuttuja tohon luuppiin ja sit pari muuttujaa, johon sijoitetaan noi datat, mitä tulee tietokannasta ja sit heitetään ne muuttujat johonkin muualle. Tohon mä tarviin yhen temp muuttujan, että mä saan noi arvot swapattua ja yhden muuttujan joka kuvaa tota ikkunakontekstia, jonka kautta mä päivitän näyttöä...". Oon miettiny päässäni muuttujia, kontrollirakenteita ja sopivia tietorakenteita ajassa etenevänä prosessina. Sellaiselta se ajattelu on kuitenkin tuntunut.
Vastaan tähän kahdesta eri näkökulmasta.
1. kommentti:
Ohjelmoinnissa on paljon muistettavaa ja mietittävää, ja tämä ei ole sidoksissa ohjelmointitapaan. Jos ajateltavaa on liikaa kerralla, voi tehdä useamman selvästi rajatun funktion – myös imperatiivisissa kielissä.
Tavallaan olet oikeassa siinä, että funktionaalisessa ohjelmoinnissa ei tarvitse miettiä muuttujien määrää ja laatua, mutta kyllä siihen tulee tilalle ihan yhtä paljon uutta. Tiedot on laitettava johonkin – ellei muuttujiin niin yleensä parametreihin. Jos imperatiivisessa ohjelmoinnissa joutuu miettimään, montako muuttujaa tulee ja millainen silmukka pitäisi laittaa, niin yhtä lailla funktionaalisessa ohjelmoinnissa joutuu miettimään, mitkä parametrit funktiolle annetaan ja miten parametrista x saikaan laskettua arvon y.
2. kommentti:
Verrataanpa asiaa luonnolliseen kieleen. Mietitkö puhuessasi, että nyt tarvitaan subjekti ja tähän tulee verbin kolmannen persoonan preesens? Kielen alkeiskurssilla voi olla noin, mutta sujuvaan kielitaitoon vaaditaan aivan muu lähestymistapa. Sujuvassa puheessa pitäisi pystyä keskittymään siihen, mitä haluaa sanoa, ja sanojen ja rakenteiden pitäisi tulla melko automaattisesti.
Aivan vastaavasti sujuvassa ohjelmoinnissa pääasia on se, mitä koodilla halutaan tehdä tai kuvata, ja tarvittavat perusrakenteet löytyvät automaattisesti. Jos huomio kiinnittyy siihen, minkälaisia muuttujia pitää olla, selvästi ohjelmointi ei ole vielä kovin sujuvaa.
Selma-koira kirjoitti:
miksemme ole nähneet ohjelmointiputkassa yhtään keskustelua, jossa todettais, että "hei, tää on muuten järkevää, miksei tehtäis näin"
Nyt juksaat.
Kyllähän monissa keskusteluissa on kannustettu purkamaan koodia järkeviin osiin alkaen esimerkiksi siitä, että tiedon hakeminen, tiedon käsitteleminen ja tiedon tulostaminen olisivat erilliset toiminnot.
Olet tervetullut itsekin osallistumaan keskusteluihin ja kommentoimaan koodeja, jos mielessäsi on asioita, joita pitäisi huomioida.
Selma-koira kirjoitti:
Tässä lyhyt esimerkki:
funktio (tietokantaKahva) { tietokannastaTullutTieto = käytäTietokantaa(tietokantaKahva) järjestettyTieto = sort tietokannastaTullutTieto palauta järjestettyTieto }Imperatiivinen ohjelmointimallia mahdollistaa edellisen kaltaisen funktion kirjoittamisen, jossa funktion tehtävänä on järjestää tietoa. Malli ei ota kantaa siihen, mitä funktioiden sisällä tapahtuu - siellä voi tapahtua mitä vaan. Jos tietokantayhteys katkeaa, testi epäonnistuu, vaikka funktion tarkoituksena on järjestää tietoa. Testi myös epäonnistuu, jos tietokantayhteys ei koskaan aukea. Testi epäonnistuu myös, jos tietokannan salasana on vaihdettu, eikä testaaja ole siitä tietoinen. Näin on tullut kirjoitettua koodia, joka on riippuvainen seikoista, jotka eivät mitenkään liity siihen, mitä koodin avulla halutaan saada aikaiseksi.
Kyllä kai imperatiivisellakin kielellä voidaan eristää tietokantahaku ja tuloksien manipulointi omiin osiinsa. Tällöin haku tehdään omana osanaan ja haun tuloksena joko suoritetaan manipulointi tai ilmaistaan käyttäjälle tietokantahaun epäonnistuminen. Olet minustakin käsittänyt imperatiivisen vs. funktionaalisen ohjelmoinnin eron jotenkin kovin mustavalkoisesti.
Selma-koira kirjoitti:
Jos funktio ilman sivuvaikutuksia on mielestäsi järkevää, niin miksemme ole nähneet ohjelmointiputkassa yhtään keskustelua, jossa todettais, että "hei, tää on muuten järkevää, miksei tehtäis näin" tai "hei, tota koodia vois vähän vielä pohtia, että kuinka helppoo se on testata" tms. Syy voi olla ehkä se, että koska kielen rakenteet eivät siihen johdattele, asia jää helposti pohtimatta. Silloin tulee helposti pohdittua muita juttuja, jotka ovat nekin toki tärkeitä, koodin luettavuudesta alkaen.
Voisin kuvitella maininneeni yksinkertaisten funktioiden konseptin palstalla ja että moni muukin olisi tuonut sellaisen asian esille. Se on sivuseikka, onko funktiolla "sivuvaikutuksia" vai ei, kunhan se on yksinkertainen, jolloin se tekee sen asian, mitä sen pitäisikin.
Olion metodeista puhuttaessa näiden sivuvaikutuksien nostaminen esille ikään kuin huonona asiana on kyseenalaista. Se on mielestäni vähän makuasia, haluaako hyödyntää olion tarjoamaa "kontekstia" vai kirjoittaako siitä huolimatta tilattomia metodeja.
Olenpa tuota itsekin aika uppiniskaisesti välillä kirjoitellut tilattomia funktioita, jotka ottavat olion sisäistä dataa parametrina this-viittausten sijaan, mutten koe että se olisi ollut erityisesti parempi tapa kuin nojata this:n olemassaoloon.
Muutama viite:
Viesti #220325
Viesti #232509
Viesti #232531
Metabolix kirjoitti:
Vastaan tähän kahdesta eri näkökulmasta.
1. kommentti:
Ohjelmoinnissa on paljon muistettavaa ja mietittävää, ja tämä ei ole sidoksissa ohjelmointitapaan. Jos ajateltavaa on liikaa kerralla, voi tehdä useamman selvästi rajatun funktion – myös imperatiivisissa kielissä.
Tavallaan olet oikeassa siinä, että funktionaalisessa ohjelmoinnissa ei tarvitse miettiä muuttujien määrää ja laatua, mutta kyllä siihen tulee tilalle ihan yhtä paljon uutta. Tiedot on laitettava johonkin – ellei muuttujiin niin yleensä parametreihin. Jos imperatiivisessa ohjelmoinnissa joutuu miettimään, montako muuttujaa tulee ja millainen silmukka pitäisi laittaa, niin yhtä lailla funktionaalisessa ohjelmoinnissa joutuu miettimään, mitkä parametrit funktiolle annetaan ja miten parametrista x saikaan laskettua arvon y.
Tässä se suuri ero just onkin, se muuttujien pohdinta johtaa myös synkronoinnin pohdintaan - tää on taas sitä "syvällistä" osastoa. Tietoa ei tarvitse "laittaa" mihinkään. Riittää, että kuvaa, mitä sille tiedolle tehdään. Kun sä huomaat, että sulla on tarve esim. lisätä viiteen taulukon arvoon jokaiseen ykkönen, niin sä mietit, että joo, täytyy luupata ne läpi ja muuttaa arvot siinä taulukossa, niin että ensin luetaan sieltä arvo, lisätään siihen 1 ja sijoitetaan arvo takaisin taulukkoon. Ja jos toi on sellanen muuttuja, toi taulukko, että on riski, että useampi säie(tai muu vastaava suoritysyksikkö) voi käsitellä sitä samaan aikaan ja on vaatimus koskien tiedon integriteettiä, on käytettävä ehkä säieturvallista säiliötä(löytyy monista kielistä ja kirjastoista, kuten ehkä tiedät) ja varmistettava, että taulukkoa käsitellessä, sitä ei voi käsitellä toinen säie(muuttamismielessä) ja jos haluat varmistua myös siitä, että taulukkoa ei voi toinen säie lukea (koska sulla on siinä päivitykset menossa), ettei toi toinen säie tuu lukeneeks tietoa, joka on kenties vain osin muutettu, muttei kokonaan (ja haluat, että integriteetin osalta ajatus on, että koko taulukko on joko muutettu tai taulukko on ennallaan), niin sä oot just miettinyt että mitä muuttujaa käytät (se thread-safe säiliö) joko luku-tai kirjoituslukoilla tai molemmilla riippuen tarpeesta. Pitää aika tarkkaan miettiä millanen säiliö valitaan, varsinkin jos on toi thread safe vaatimus ja aika usein on, kun multicore jauhaa.
Jos sä aattelet, kuten fp-ohjelmoija sä mietitkin, että "hei, tässä on kyseessä tiedon muunnos".. ookoo sit sä määrittelet, miten se tieto muuttuu. Mietit, mitä siellä fp-kurssilla tai siinä koirakirjassa, sanottiin tieto muutetaan mapillä. No koska oot pelannu numeroilla, niin tiedät, että jos johonkin pitää lisätä ykkönen, niin sen voi sanoo vaikka +1. Se on sun muunnos. Sit sulla pitää olla jotain, mitä muunnetaan, se on se sun toinen parametri sille korkeamman kertaluvun funktiolle. Eli tarviit tiedoksi jotain dataa ja tiedon siitä, miten data muuttuu. Yhtään muuttujaa ei tarvittu. Se menis näin:
map (+1) sunDataMitäOnkaan
, siinä se sit oli. Toi on thread safe by design, koska kaikki data mitä se käyttää on funktion skoupissa. Haittapuolena verrattuna tuohon edelliseen esimerkkiin on tietysti, että toi algoritmi ei ole in-place, muistia tarvitaan sille uudelle datalle, joka prosessissa syntyy (tosin kääntäjät on näissä nykyään aika hyviä säästään muistia, esim. Scala). Muitakin haittoja on, mutta toi on ehkä oleellisin mietittävä - uudelle datalle tilaa ja hemmetin pitkä rekursio, jos dataa on paljon - optimoitavissa on, mutta jos haluaa, että toi on mukavan helppoo, niin lähtee siitä, että dataa on riittävän vähän käsissä.
Jos sulla on menossa joku perus webbisofta tai JavaScriptillä pyörivä käyttöiittymä tms. niin, voi miettiä kuinka sen haluaa tehdä. Jos sulla on Haskellin kaltainen vasara tai hyvä moniparadigmatyökalu tms. kädessä ja mäppäät, saat thread safetyn kaupan päälle. Kattoo vähän tilannetta - aika usein fp:llä menee ihan ok.
Siis uutta tuli päälle se, että hommia hoidetaan korkeamman kertaluvun funktiolla, kuten map ja se, että pitää tietää, mitä datalle tehdään. Uusi juttu taisi olla tässä toi korkeamman kertaluvun funktio vai oliko jotain muutakin? Ei niillä koiravideoilla ainakaan ollut juuri mitään ton kummempaa - mutta ne olikin kai lapsille.. vai oliko sittenkään?
Metabolix kirjoitti:
2. kommentti:
Verrataanpa asiaa luonnolliseen kieleen. Mietitkö puhuessasi, että nyt tarvitaan subjekti ja tähän tulee verbin kolmannen persoonan preesens? Kielen alkeiskurssilla voi olla noin, mutta sujuvaan kielitaitoon vaaditaan aivan muu lähestymistapa. Sujuvassa puheessa pitäisi pystyä keskittymään siihen, mitä haluaa sanoa, ja sanojen ja rakenteiden pitäisi tulla melko automaattisesti.
Aivan vastaavasti sujuvassa ohjelmoinnissa pääasia on se, mitä koodilla halutaan tehdä tai kuvata, ja tarvittavat perusrakenteet löytyvät automaattisesti. Jos huomio kiinnittyy siihen, minkälaisia muuttujia pitää olla, selvästi ohjelmointi ei ole vielä kovin sujuvaa.
Tuon Antti Laaksosen viittaus siihen, että muuttuja on imperatiivisen ohjelmoinnin keskeisimpiä käsitteitä on aivan totta. Niitä joutuu miettimään kieliopista riippumatta - varsinkin, kun koodia alkaa olla reilummin ja järjestelmän kuormitus vaihtelee, kuten asia usein on. Taas me kysytään, onko fp:stä mitään hyötyä? Halusin korostaa, että muuttujat "lähtökohtana" voi olla ongelma. "fp:n alkeissa" tota äskeistä perustelua ei kannata esittää, siksi esitän sen täällä, missä pohditaan asiaa syvällisemmin. Pointti on, että kun otat toisen säikeen kehiin, oot huonolla tuurilla hemmetinmoisessa liemessä sen muuttujan kanssa ja ohjelmoinnin pääasiaksi voi tullakin sen saaminen toimimaan, sen muuttujan kanssa.
Selma-koira kirjoitti:
miksemme ole nähneet ohjelmointiputkassa yhtään keskustelua, jossa todettais, että "hei, tää on muuten järkevää, miksei tehtäis näin"
Metabolix kirjoitti:
Nyt juksaat.
Kyllähän monissa keskusteluissa on kannustettu purkamaan koodia järkeviin osiin alkaen esimerkiksi siitä, että tiedon hakeminen, tiedon käsitteleminen ja tiedon tulostaminen olisivat erilliset toiminnot.
Olet tervetullut itsekin osallistumaan keskusteluihin ja kommentoimaan koodeja, jos mielessäsi on asioita, joita pitäisi huomioida.
Joo, mutta perustelu ei ole ollut koskaan sivuvaikutusten ottaminen huomioon. Se on se pointti. Näähän on ihan loistavat sivut ja toi vuosien takainen Mureakuha-boikotti sai kyllä aikaan aika railakkaat naurut. Kyllä täällä ovat monet saaneet paljon hyviä apuja monenlaisissa haasteissa, ei sillä.
Lisäys:
Teuro kirjoitti:
(03.10.2019 07:14:00): ”– –” Kyllä kai imperatiivisellakin kielellä...
Mutta jostain syystä näin ei kuitenkaan toimita. Minkä luulet olevan siihen syynä? Jos sulla on ensin 100 funktioo ja kohta onkin 200 funktioo, niin onko se ihan viisasta? Olisko jotain konstia "puhdistaa" funktioo ilman, että tekee siitä kahta erillistä? Tätä mä haen tässä takaa... Se on sitä "syvällisempää" osastoa noiden koirajuttujen päälle.
Jos meillä on sopiva työkalu, niin saisko ton hanskattua jollain muulla tavalla kuin tuplaamalla funktiomäärän? Tässä tulee se kohta, jossa voitaneen kai todeta, että se C-kielen funktiopointteri ja sen käyttö ei edusta toiminnallisuudeltaan fp:n funktion(kuten Haskell) toiminnallisuutta - mutta parempi myöhään kuin ei milloinkaan. Siinä on yks perustavanlaatuinen ero. Mikähän se vois olla.
Selma-koira kirjoitti:
Tässä se suuri ero just onkin, se muuttujien pohdinta johtaa myös synkronoinnin pohdintaan - tää on taas sitä "syvällistä" osastoa. Tietoa ei tarvitse "laittaa" mihinkään. Riittää, että kuvaa, mitä sille tiedolle tehdään. Kun sä huomaat, että sulla on tarve esim. lisätä viiteen taulukon arvoon jokaiseen ykkönen, niin sä mietit, että joo, täytyy luupata ne läpi ja muuttaa arvot siinä taulukossa, niin että ensin luetaan sieltä arvo, lisätään siihen 1 ja sijoitetaan arvo takaisin taulukkoon.
No jos mä otan imperatiivisesta kielestä esimerkiksi vaikka C# ja miten lähtisin tekemään jos mun pitäis saada käyttöön taulukko, jonka kaikki arvot olis 1 suurempi, niin se vois olla
var isompi = taulukko.Select(alkio => alkio+1);
Tuossakaan ei muutettaisi alkuperäisen taulukon arvoja ja minusta tuo on jopa hieman kivempi kirjoittaa kuin versio jossa alkuperäisen taulukon arvoja muutetaan. Minulla siis olisi ko. ohjelmalohkossa käytettävissä tuo "isompi" -muuttuja, joka katoaa näkyvyydestä kun lohko päättyy, jos en erikseen palauta sitä ulos.
Lähtökohtaisesti jos nykyaikaisessa imperatiivisessa kielessä lähden muuttamaan annetun taulukon arvoja, niin silloin tyypillisesti kyse on siitä haluan että ne alkuperäiset arvot muuttuu. Esimerkiksi jos mulla olis lista järjestelmän käyttäjistä ja haluaisin päivittää ylös montako kertaa käyttäjä on kirjautunut. Ei mulle ole mitään hyötyä jos se muutos tapahtuu vain funktion sisällä olevassa kopiossa. Ja tällöin tietty täytyy ottaa säieturvallisuus yms huomioon.
Eli oikeasti vertailukelpoinen tilanne olisi, että funktionaalisella kielellä toteutetussa järjestelmässäkin haluaisin päivittää muistiin esim. montako kertaa käyttäjä on kirjautunut. Ehkä osaat kertoa miten se funktionaalisella puolella tapahtuu ja miten se on automaattisesti säieturvallinen jne.
Selma-koira kirjoitti:
Toi on thread safe by design, koska kaikki data mitä se käyttää on funktion skoupissa.
Edelleen päästään samaan vastaukseen kuin ennenkin: Myös imperatiivisessa koodissa voi tehdä tuon asian aivan samalla tavalla turvallisesti. Mikä tässä on epäselvää?
Imperatiivisessa ohjelmoinnissa ei tarvitse käyttää globaaleja muuttujia, ei tarvitse käyttää osoittimia, ei tarvitse säheltää säikeillä. Kaikki on mahdollista mutta ei pakollista.
Huonon koodin bugit johtuvat huonosta koodista, eivät ohjelmointiparadigmasta.
Kaatumisen voi välttää muutenkin kuin makaamalla lattialla päivät pitkät; sitä paitsi lattialla makaaminen rajoittaa elämää ja pystyssä saa tehtyä paljon enemmän.
Grez kirjoitti:
var isompi = taulukko.Select(alkio => alkio+1);Tuossakaan ei muutettaisi alkuperäisen taulukon arvoja ja minusta tuo on jopa hieman kivempi kirjoittaa kuin versio jossa alkuperäisen taulukon arvoja muutetaan. Minulla siis olisi ko. ohjelmalohkossa käytettävissä tuo "isompi" -muuttuja, joka katoaa näkyvyydestä kun lohko päättyy, jos en erikseen palauta sitä ulos.
Lähtökohtaisesti jos nykyaikaisessa imperatiivisessa kielessä lähden muuttamaan annetun taulukon arvoja, niin silloin tyypillisesti kyse on siitä haluan että ne alkuperäiset arvot muuttuu. Esimerkiksi jos mulla olis lista järjestelmän käyttäjistä ja haluaisin päivittää ylös montako kertaa käyttäjä on kirjautunut. Ei mulle ole mitään hyötyä jos se muutos tapahtuu vain funktion sisällä olevassa kopiossa. Ja tällöin tietty täytyy ottaa säieturvallisuus yms huomioon.
Eli oikeasti vertailukelpoinen tilanne olisi, että funktionaalisella kielellä toteutetussa järjestelmässäkin haluaisin päivittää muistiin esim. montako kertaa käyttäjä on kirjautunut. Ehkä osaat kertoa miten se funktionaalisella puolella tapahtuu ja miten se on automaattisesti säieturvallinen jne.
Selkeän näköinen pätkä, sinne menee ilmeisesti nimetön funktio parametrina(joka sisältää tuon "logiikan" eli +1) ja se tekee "uudet" alkiot uusilla arvoilla?
Funktionaalisissa ratkaisuissa, jos muuttujia pitää käsitellä turvallisesti, niin ratkaisu on STM (Software Transactional Memory). Esim. Clojurelle, Scalalle, Haskellille löytyy toteutukset. Esim. Scala: https://nbronson.github.io/scala-stm/. Idea on sama kuin tietokannassa, joka sisältää transaktiot - samoine ongelmineen.
Jos pystyy jättämään ko. ongelman tietovaraston huoleksi, millainen se sitten onkin, niin STM:ää ei tarvita.
Tuossa näkyy myös hyvin funktionaalisen ohjelmoinnin ilmaisuvoima. Tosta lambdasta näkee heti, mitä datalle tehdään. Vaikken C#:sta ymmärrä juuri mitään, niin mäppäyksen kaltainen kuvio siinä on kyseessä, eikä tarvii miettiä, mitä noille alkioille tehdään, kun se näkyy heti.
Lisäys:
Metabolix kirjoitti:
(03.10.2019 17:31:39): ”– –” Edelleen päästään samaan vastaukseen kuin...
No jos tekee työkaluilla, joissa on otettu lähtökohtaisesti huomioon sivuvaikutukset, pääsee vähä helpommalla. Ei tarvii ihan niin paljon huolehtia kaikkee, kun oletukset on kohdillaan. Näin ehkä se vähän kokemattomampikin kaveri saa suhteellisen turvallista koodia aikaan.
Toisaalta sitten on ilmeisesti kokemattoman kaverin turha haaveilla vaikkapa tietokannasta tai graafisesta käyttöliittymästä, vai voitko antaa jotain esimerkkejä näistä?
Luultavasti parasta olisi kokemattoman koodarin ohjelmoida vaikka Brainfuck-kielellä, koska siinä ohjelman toiminta on täysin ennustettavaa ja selvää eikä koodilla varmasti ole mitään sivuvaikutuksia. Se on myös erittäin helppo oppia, koska rakenteet ovat erittäin selvät ja ei tarvitse muistaa yhtään sanaa eikä pohtia muuttujien tyyppejä tai määrittelyitä.
Selma-koira kirjoitti:
Tuossa näkyy myös hyvin funktionaalisen ohjelmoinnin ilmaisuvoima. Tosta lambdasta näkee heti, mitä datalle tehdään. Vaikken C#:sta ymmärrä juuri mitään, niin mäppäyksen kaltainen kuvio siinä on kyseessä, eikä tarvii miettiä, mitä noille alkioille tehdään, kun se näkyy heti.
En ole asiaa sen kummemmin miettinyt ja pitänyt erityisesti funktionaaliseen ohjelmointi tapaan kuuluvana, mutta esimerkiksi käyttämäni 8th ohjelmointikieli tarjoaa map ja filter yms. toiminnallisuuden. Alla esimerkki map toteutuksesta:
[2,3,5,1,3] var, annoslista annoslista @ ( 2 n:+ ) a:map var, uudet-annokset
Useampaa säiettä käytettäessä 8th tarjoaa tuen task-local muuttujille. Tosin pinopohjaisella ohjelmointikielellä muuttujista yleensä pyritään eroon ja käytetään mahdollisimman paljon sitä pinoa. Perinteiset keinot ovat muuten tietysti käytössä, eli välitetään säikeelle kopio tai käytetään lukkoja suojaamaan. Tehokkainta tietysti on kun järjestelee ongelman siten, että lukoista pääsee kokonaan eroon.
Alla esimerkki, mikä laskee kahdeksan säikeen avulla 8000 ensimmäistä fibonaccin lukua ja tulostaa tulokset. Omalla koneellani kun tulostuksen ohjaa tiedostoon aikaa kuluu yhteensä n. 21.7 sekuntia.
8 constant TASKS 1000 constant WORKSIZE var tasks a:new ( a:new a:push ) TASKS times var, results : bits? \ n -- nbits-1 n:ln 2 n:ln n:/ n:int ; : fibo-loop >r \ Put loop counter on r-stack \ a b 2dup 2 n:* \ a b a 2b over n:- \ a b a (2b-a) n:* -rot \ d=(2b-a)*a a b n:sqr swap n:sqr n:+ \ d=(2b-a)*a e=b*b+a*a r> r@ swap n:shr 1 n:band if dup \ a b b rot \ b b a n:+ \ b c=b+a then ; : fibo \ n -- fibo(n) >r \ Put n on r-stack 0 1 \ a b ' fibo-loop 1 r@ bits? loop- \ a b r> 1 n:band if n:sqr swap n:sqr n:+ \ b*b+a*a else 2 n:* over n:- n:* \ (2b-a)*a then ; : generate-fibo \ a start num -- ( fibo a:push ) rot rot loop drop ; : task \ start end index -- results @ swap a:@ nip \ start end a -rot \ a start end generate-fibo ; : start-tasks a:new ( dup WORKSIZE n:* dup WORKSIZE n:1- n:+ rot 3 ' task t:task-n a:push ) 0 TASKS n:1- loop tasks ! ; : wait-tasks tasks @ t:wait ; : display-results results @ ( ( . cr ) a:each! drop ) a:each! drop ; : app:main start-tasks wait-tasks display-results bye ;
jalski kirjoitti:
(03.10.2019 20:39:59): ”– –” En ole asiaa sen kummemmin miettinyt ja pitänyt...
Niin. Onko tuo map tuossa funktio vai onko se tietorakenne? Funktionaalisen ohjelmoinnin ideoita hyödynnetään myös matalan tason tekemisessä, jos se katsotaan tarpeelliseksi. Koodin toiminnan oikeaksi todistaminen voi olla funktionaalisessa mallissa helpompaa ja kuten koirakirjastakin muistetaan, valitaan työkalu tilanteen mukaan.
Kyse ei ole siitä, ettei asiaa voitaisi tehdä ilman funktionaalisen ohjelmoinnin ideoita, vaan kyse on siitä, millä se koodi saadaan mahdollisimman varmasti tekemään se, mitä halutaan. http://home.iae.nl/users/mhx/Forth_functional.
Lisäys:
Metabolix kirjoitti:
(03.10.2019 18:26:34): Toisaalta sitten on ilmeisesti kokemattoman...
Niin tarkoitatko sä siis, että kokemattoman koodarin on turha haaveilla tekevänsä tietokantamoottoria, joka toteuttaa esim. ACID-speksin vai tarkoitatko, että kokematon koodari ei voisi käyttää jo olemassaolevaa pysyvyysratkaisua, mikä se sitten onkaan?
21.7 sekuntia 8 säikeellä kuulostaa paljolta, kun jopa Python tekee yhdellä säikeellä saman asian kymmenesosasekunnissa.
open("fibo.txt", "w").writelines( f"{val}\n" for val in islice(fibo(), 8000) )
Tronic kirjoitti:
21.7 sekuntia 8 säikeellä kuulostaa paljolta, kun jopa Python tekee yhdellä säikeellä saman asian kymmenesosasekunnissa.
open("fibo.txt", "w").writelines( f"{val}\n" for val in islice(fibo(), 8000) )
Mikä on tuo fibo() toteutus? Oma kokemukseni on, että Pythonin bignum toteutus on älyttömän hidas. Kokeile laskea fibo(4784969). Oma nykyinen toteutukseni laskee tuloksen noin 175 ms omalla koneellani.
https://www.ohjelmointiputka.net/keskustelu/
Metabolix kirjoitti:
Selma-koira kirjoitti:
Kirja ottaa kantaa funktionaalisen ohjelmoinnin soveltuvuuteen tiedon käsittelyyn liittyvissä ongelmissa.
Kirjan esimerkit ovat niin suppeita, että niistä on vaikea nähdä, miten funktionaalisesta ohjelmoinnista olisi hyötyä tai miten se edes eroaisi imperatiivisesta...
Kyllä se eroaa:
funktionaalinen tyyli:
lista = [1,2,3] map (+1) lista
imperatiivinen tyyli:
taulukko1 [1,2,3] taulukko2 [] for(x=0;x<taulukko1.pituus;x++) { taulukko2[x]=taulukko1[x]+1 }
Onhan näissä aika iso ero. Jos map-funktio suhteessa for-luuppiin ei osoita sitä eroa, niin mikä osoittaisi sen paremmin. Miten sä itse lähestyisit asiaa? Miten laajentaisit suppeutta lukijalle, joka opiskelee "johdatusta aiheeseen". Olen miettinyt, että asiaa voisi valaista komposition avulla eli ottaa lisää "eroja" tyylien välillä esiin - esim. järjestelmätason suunnittelueroja.
Vaikka lukija olisikin jo konkari imperatiivisessa ohjelmoinnissa, mikä auttaisi häntä näkemään, että ne ovat lähtökohtaisesti kaksi aika erilaista tapaa ajatella mitä on "computing"? Mitä tuumaat? TF on ihan hyvä funktionaalisen paradigman idean sovellus - kompositiota hyödynnetään: https://ai.google/research/pubs/pub45842/. Mitä se "syvällisempi" voisi olla. Tuollaisia esimerkkitoteutuksia tai jotain muuta ja niiden "avaamista" kirjassa?
Selma-koira kirjoitti:
taulukko1 [1,2,3] taulukko2 [] for(x=0;x<taulukko1.pituus;x++) { taulukko2[x]=taulukko1[x]+1 }
Nojaa toista taulukkoa ei tarvita mihinkään, eikä arvon lisäystä ole tarvetta tehdä suinkaan hankalasti. Koodilistaus on vain hiukan pidempi ja edelleen aivan helposti ymmärrettävissä. Selkeyden vuoksi lisäsin muutaman välilyönnin.
taulukko [1,2,3]; for(x = 0; x < taulukko1.pituus; ++x) { ++taulukko[x]; }
Selma-koira kirjoitti:
funktionaalinen tyyli:
lista = [1,2,3] map (+1) listaimperatiivinen tyyli:
taulukko1 [1,2,3] taulukko2 [] for(x=0;x<taulukko1.pituus;x++) { taulukko2[x]=taulukko1[x]+1 }
Vertailusi on rikkinäinen. Tuossa eivät ole rinnakkain imperatiivinen ja funktionaalinen tyyli. Oikeasti vertaat huonosti kirjoitettua silmukkaa ja mahdollisimman lyhyttä funktiokutsua. Tietenkin funktiokutsu on lyhyempi.
Ei ole mikään yllätys, että oksennus ja gourmet eivät maistu samalta. Jos haluat vertailulla todistaa jotain, valitse molemmille puolille yhtä edustavat vaihtoehdot.
Edellä jo Grez demonstroi, että imperatiivisessa ohjelmoinnissa voi ihan yhtä lailla piilottaa silmukan funktioon ja tehdä vastaavan yhden rivin ratkaisun:
lista2 = lista.Select(x => x + 1);
Tämän ero map-kutsuusi jää kosmeettiseksi.
Jos mielestäsi oma vertailusi for-silmukan ja map-funktion välillä on asiallinen ja reilu, niin sitten on varmaan tämäkin vastaesimerkki:
Imperatiivinen tyyli (C++):
std::valarray<int> luvut = {1,2,3}; std::valarray<int> uudet = luvut + 1;
Funktionaalinen tyyli (Haskell):
muunnos :: (Int -> Int) -> [Int] -> [Int] muunnos _ [] = [] muunnos funktio lista = funktio (head lista) : muunnos funktio (tail lista) lista = [1,2,3] lista2 = muunnos (+1) lista
Nyt yllättäen imperatiivinen ohjelma onkin lyhyempi. Miten siinä näin kävi?!
C++:lla reilumpi esimerkki (verrattavaksi yllä olevaan itse tehtyyn Haskell-versioon, ei suinkaan pelkkään map-riviin) olisi vaikka tällainen:
std::vector<int> luvut = {1,2,3}; auto uudet = luvut; for (auto &x: uudet) x += 1;
C++ on suhteellisen hankala kieli kaikkine tyyppeineen, joten vielä reilumpaa olisi verrata vaikka Pythoniin:
luvut = [1,2,3] uudet = [x+1 for x in luvut]
Muistin virkistämiseksi vielä se lyhin vastaava Haskell-versio (vaikka siinä käytetäänkin vähän epäreilusti valmista funktiota):
lista = [1,2,3] uudet = map (+1) lista
Näyttääkö ero vielä valtavan suurelta?
Minusta tässä ainoa kohta, jossa ero oikeasti näkyy, on tuo map-funktion (yllä muunnos-funktion) rekursiivinen toteutus verrattuna for-silmukkaan. Jos taas ihaillaan ”yksinkertaista” koodia, jossa rekursio on piilotettu funktioon, niin sitten täytyy myös toiselta puolelta katsoa samanlaista koodia, jossa for-silmukka on piilotettu funktioon.
Tuo malliluokilla toteutettu C++ esimerkki koskee lukujen käsittelyä ja vain niitä. Koirakirjan ekassa osassa, missä jutellaan yleisesti tiedon käsittelystä. Erikoiskirjastoja laskentaan löytyy varmasti.
Esimerkissä jätetään kaikki tyyppipäättelyn varaan, missään ei kiinnitetä tyyppeihin huomiota, ja syystä. Pointti on, että jos käsitellään dataa sellaisenaan, ei oteta vielä tässä kohtaa kantaa tyyppiin tai edes tyyppijärjestelmiin ja niiden eroihin, vaan datan käsittelyyn yleensä.
Oleellista on, että määritellään data ja sen käsittely toisistaan erillään, kuvaten, mitä datalle tehdään - for in, ei tee sitä, se viittaa for-luuppiin - fp:ssä mielummin kuvataan mitä tehdään - for in, viittaa luuppiin.
Ohjelmoinnin voi aloittaa abstraktiotasolta, jossa toi map-funktiokin on. Jotta saaddaan koodia aikaiseksi, ei tarvitse pohtia toteutusta. Ihan samalla tavalla, kun ei tarvitse pohtia sitä piilotettua silmukkaakaan. Joukkoon sovellettavan funktion ideana ja toteutuksen lähtökohtana puhtaassa fp-kielessä on rekursio ja että noiden imperatiivisten työkalujen taustalla lienevät silmukat, myös tossa Python-esimerkissä.
Funktionaalisessa ohjelmoinnissa voidaan lähteä ajatuksesta, että korkeamman kertaluvun funktio on perustyökalu. Fp-kirjastoissa on ihan samaa idea kuin imperatiivisissakin. Piilotetaan se, mitä ei tarvitse nähdä.
Viestin loppupäässä tulee mun mielestä tärkeä asia. Sanot, että Haskell-esimerkissä "käytetään valmista funktiota" ja sanot, että se on vähän epäreilun. Kyse ei ole minun mielestä reiluudesta tai epäreiluudesta, vaan siitä, millaisilla abstraktiolla lähdetään liikkeelle. Noi kaksi paradigmaa olettavat erilaiset abstraktiot ja minun mielestäni niin voi ollakin - siksihän niitä paradigmoiksi kutsutaankin - fundamentit on eri.
Funktionaalisen ohjelmoinnin näkökulmasta käytännön ohjelmointitöissä korkeamman kertaluvun funktio on suunnittelun lähtökohta, se perustyökalu. Rekursio on ongelmallinen, erityisesti mitä tulee muistin käyttöön.
Ideaa voinee löyhästi verrata vaikka erilaisiin sovelluskehyksiin, jotka pakottavat tiettyjä asioita ja mahdollistavat toisia. Ne tarjoavat yhtä lailla lähtökohdan ja työkalut. Sitähän ne ohjelmointikieletkin ovat - työkaluja.
Se, että imperatiivisiin työkaluihin on tullut erilaisia fp-mallisia läpikäyntejä ja erilaisia versiota esim. mapista on seuraus siitä, että tarve on ollut datan käsittelyn määritteleminen mahdollisimman turvallisesti ja luettavasti ja on haluttu eroon mm. yli-indeksoinnista jne. koska me ihmiset teemme virheitä. Kieli on elänyt, kehittynyt. Ei ole ollut kyse huonosta koodista tai kehnoista ohjelmoijista, vaan siitä, miten jokin homma parhaiten hoituu tietyssä tilanteessa.
Todella raskasta muuten lukea imperialistinen teksti, jossa jatkuvasti käytetään mm. sä-passiivia.
Grez kirjoitti:
Todella raskasta muuten lukea imperialistinen teksti, jossa jatkuvasti käytetään mm. sä-passiivia.
Mitä tarkoittaa imperialistinen tässä yhteydessä? Liittykö se jotenkin funktionaalisen ja imperatiivisen ohjelmoinnin erojen pohdintaan?
Ei vaan se kuvaa puhetapaa. Lisää aiheesta https://www.kielikello.fi/-/imperialistinen-sa-puhe-kun-sa-ajat-formulaa
Grez kirjoitti:
Ei vaan se kuvaa puhetapaa. Lisää aiheesta https://www.kielikello.fi/-/imperialistinen-sa-puhe-kun-sa-ajat-formulaa
Ok. Kiitos vinkistä! Kävin korjaamassa.
Selma-koira kirjoitti:
Kyse ei ole reiluudesta tai epäreiluudesta, vaan siitä, millaisilla abstraktiolla lähdetään liikkeelle. Noi kaksi paradigmaa olettavat erilaiset abstraktiot ja se on ihan ok - siksihän niitä paradigmoiksi kutsutaankin - fundamentit on eri.
Minusta tuo on erittäin suuri ajatusvirhe. Kuten on aika monta kertaa yritetty selittää, imperatiivinen ohjelmointi ei edellytä mitään tiettyä abstraktion tasoa. Abstraktion määrä riippuu välineistä (kielestä, kirjastoista ym.). Tämä näkyy vaikkapa tuosta Python-koodista, jossa ”abstraktion taso” on samanlainen kuin esittämässäsi Haskell-koodissa.
Selma-koira kirjoitti:
for in, ei tee sitä [ei kuvaa tarkoitusta], se viittaa for-luuppiin
Jos sinä saat väittää noin, niin minä saan väittää, että vastaavasti tavalla map (+1) lista
viittaa funktiokutsuun eikä tiedon käsittelyyn.
Vaikka sinä tulkitset, että for tarkoittaa aina pelkästään silmukkaa, niin minusta ohjelmoinnissa for tarkoittaa (ja myös kuvaa) usein sitä, että käsitellään jonkinlaista joukkoa.
Jos suoraan luetaan Pythonin lauseke x+1 for x in t
, siinähän lukee, että tuotetaan x+1 kaikista arvoista x joukossa t. Tähän ei tarvitse väkisin ajatuksena liittää mitään silmukkaa. Matematiikan kielellä voidaan kirjoittaa B = {A₁ + 1, A₂ + 1, ..., Aₙ + 1}. Tyypillisesti Pythonissa for-rakenteella ilmaistaan lausekkeita, jotka matematiikassa sopivat esimerkiksi symbolien ∑ (summa), ∀ (kaikilla) ja ∈ (kuuluu joukkoon) perään – ovatko nämäkin mielestäsi silmukoita?
Selma-koira kirjoitti:
Pointti on, että jos käsitellään dataa sellaisenaan, ei oteta vielä tässä kohtaa kantaa tyyppiin tai edes tyyppijärjestelmiin ja niiden eroihin
Funktionaalinen ohjelmointi ei tee maagisesti tyypeistä epärelevantteja. Esimerkissäsi map-funktiolle täytyy antaa parametreina funktio ja lista, ja listan alkioiden pitää vielä olla yhteensopivia annetun funktion kanssa, tai muuten ohjelma ei toimi. Jos listan sisältönä olisi vaikka "omena" ja "päärynä", luonnollisesti esimerkkikoodi ”map (+1) lista” ei toimisi. Tämä pitää edelleen tietää ja ymmärtää, vaikka tyyppejä ei tarvitse sanallisesti koodiin kirjoittaa.
Selma-koira kirjoitti:
Funktionaalisen ohjelmoinnin näkökulmasta käytännön ohjelmointitöissä korkeamman kertaluvun funktio on suunnittelun lähtökohta.
En ehkä ymmärrä aivan, mitä tarkoitat tällä ts. miten tämä nyt mielestäsi olisi imperatiivisessa ohjelmoinnissa eri tavalla. Jos laajan ohjelman suunnittelu alkaa muuttujien laadun ja määrän pohtimisesta, kyllä silloin vika on henkilössä eikä paradigmassa.
Minusta tämä keskustelu ei etene mihinkään, kun heität ympäripyöreitä väitteitä ja ei ole mitenkään selvää, mitä edes tarkoitat.
Voitko antaa esimerkiksi jonkin konkreettisen ongelman ja siihen sopivan (noin 30 riviä pitkän) toimivan koodin, jossa tämä funktionaalisen ohjelmoinnin etu tulee mielestäsi selvästi ilmi? Katsotaan sitten, miltä asia näyttää järkevillä välineillä imperatiivisesta näkökulmasta.
Selma-koira kirjoitti:
Oleellista on, että määritellään data ja sen käsittely toisistaan erillään, kuvaten, mitä datalle tehdään - for in, ei tee sitä, se viittaa for-luuppiin - fp:ssä mielummin kuvataan mitä tehdään - for in, viittaa luuppiin.
"for x in luvut" ei kylläkään viittaa mihinkään looppiin, vaan siinä yksinkertaisesti sanotaan englanniksi "joukossa luvut oleville x:ille" eli kerrotaan että joukon alkioita halutaan kutsua nimellä x kerrottaessa mitä niille tehdään.
Käytännössä toki tyypillisillä prosessoreilla map toteutuu jonkinlaisena looppina riippumatta käytetystä ohjelmointikielestä tai paradigmasta. Vektorilaskentaprosessorilla tai vastaavalla tuollainen komento toki voitaisiin antaa suoraan prosessorillekin.
Metabolix kirjoitti:
Selma-koira kirjoitti:
Pointti on, että jos käsitellään dataa sellaisenaan, ei oteta vielä tässä kohtaa kantaa tyyppiin tai edes tyyppijärjestelmiin ja niiden eroihin
Funktionaalinen ohjelmointi ei tee maagisesti tyypeistä epärelevantteja. Esimerkissäsi map-funktiolle täytyy antaa parametreina funktio ja lista, ja listan alkioiden pitää vielä olla yhteensopivia annetun funktion kanssa, tai muuten ohjelma ei toimi. Jos listan sisältönä olisi vaikka "omena" ja "päärynä", luonnollisesti esimerkkikoodi ”map (+1) lista” ei toimisi. Tämä pitää edelleen tietää ja ymmärtää, vaikka tyyppejä ei tarvitse sanallisesti koodiin kirjoittaa.
Ei tee, mutta jos halutaan hahmottaa miten tietoa käsitellään funktiolla, pärjätään alkuun hyvin ilman tyyppejäkin. Tiedon käsittelyn idea näkyy hyvin ilman niitäkin. Haskellin hyvänä puolena juuri oppimistyökaluna on tyyppipäättely. Jätin tyypit ja rekursion pois, koska haluan korostaa nimen omaan tiedonkäsittelyä sellaisenaan. Se on osa kirjan rajauksia. Haskell mahdollistaa asian hienosti.
Tyyppijärjestelmä on eräs Haskellin parhaita puolia - se ei kuitenkaan ole oleellista kirjan kannalta, joka johdattelee funktionaaliseen ohjelmointiin pienin esimerkein. Kirja palvelee myös niitä, jotka lähtevät aikanaan tekemään vaikkapa Clojurella tai muulla ei niin tyyppitarkalla työkalulla. Kirjassa otetaan myös kantaa sisään menevien argumenttien ja ulos tulevien tuotosten tyyppeihin, mutta se ei ole keskeisintä asiaa. Tyypin idean oppii hyvin, kun huomaa, että jos odotetaan merkkiä '1' ei kelpaa 1, ilman, että näkee noita funktioiden allekirjoituksia.
(Aiheen vierestä... Mielestäni allekirjoitus on tässä yhteydessä huono käännös sanalle signature, mutta eipä se ole ensimmäinen eikä varmasti viimeinen huono käännös joka vakiintuu käyttöön.)
Grez kirjoitti:
Mielestäni allekirjoitus on tässä yhteydessä huono käännös sanalle signature, mutta eipä se ole ensimmäinen eikä varmasti viimeinen huono käännös joka vakiintuu käyttöön.
Joo. Oon yrittäny löytää järkevämpää, muttei ole oikein tullut vielä hyvää vaihtoehtoa vastaan. Toi koostumus, mikä tuosssa vilahti on ongelmallinen, kun se viittaa aineeseen. Se type signaturen kääntäminen tyyppiallekirjoitukseksi on myös vähintään jännä tapa sanoa se asia. Se on ihan eri asia kuin imperatiivisen ohjelmoinnin funktion allekirjoitus, mutta paremman puutteessa näillä on menty.
(Aiheesta "function type signature":
Tuolla käytetään näköjään "funktion tyypitys"
http://www.mit.jyu.fi/opiskelu/seminaarit/
Grez kirjoitti:
(Aiheesta "function type signature":
Tuolla käytetään näköjään "funktion tyypitys"
http://www.mit.jyu.fi/opiskelu/seminaarit/ohjelmistotekniikka/funktion/ )
Oisko vaan funktion tyyppi?
Lisäys:
Grez kirjoitti:
(05.10.2019 18:29:14): ”– –” "for x in luvut" ei kylläkään viittaa mihinkään...
Se antaa sitten syntaksiltaankin ymmärtää, että joukkoa käsitellään. Kiitos vinkistä, tuijotin tuota for-sanaa vaan. Samaltahan ne näyttää siinä, mutta merkityseroja on paljon. Toi Haskellin mapin perässä oleva lista on funktori ja noi yhtäsuuruusmerkin vasemmalla puolella olevat on nimiä.
Lisäys:
Metabolix kirjoitti:
Selma-koira kirjoitti:
Kyse ei ole reiluudesta tai epäreiluudesta, vaan siitä, millaisilla abstraktiolla lähdetään liikkeelle. Noi kaksi paradigmaa olettavat erilaiset abstraktiot ja se on ihan ok - siksihän niitä paradigmoiksi kutsutaankin - fundamentit on eri.
Minusta tuo on erittäin suuri ajatusvirhe. Kuten on aika monta kertaa yritetty selittää, imperatiivinen ohjelmointi ei edellytä mitään tiettyä abstraktion tasoa. Abstraktion määrä riippuu välineistä (kielestä, kirjastoista ym.). Tämä näkyy vaikkapa tuosta Python-koodista, jossa ”abstraktion taso” on samanlainen kuin esittämässäsi Haskell-koodissa.
Mun mielestä ne perustuu eri abstraktiolle, yksi Turingin koneelle ja toinen lambda-laskennalle. Onko nyt kyse siitä voidaanko nämä kaksi asiaa määritellä samoiksi vai ei?
Lisäksi, koska lista on funktori ja Pythonin vastaavassa koodin kohdassa oleva käsite ei, niin vaikuttaako se mielestäsi jotenkin asiaan. Vai ovatko abstraktiot edelleen "niin kuin ne siinä koodissa seisovat". Saako "lista" ja koko koodi mielestäsi silloin eri merkityksen vai ovatko ne edelleen "samoja". Fp-edellyttää ton funktorin käsitteen, muuten siitä ei oikein tule mitään.
Sä voit toki kommentoida, että "kyllähän Pythonissakin voi funktoreita tehdä, ihan vaan koodaamalla". No totta kai voi. Ja voi sen tehdä muillakin kielillä, muttei se kieli ole niiden varassa.
Sitten on se kysymys, että millä perusteella kieliä voi vertailla. Minä tein kirjassa niin, osoittaakseni eron, jossa näkyy, että toisessa on muuttujia, toisessa ei, koska pidin sitä oleellisena erona ko. paradigmojen välillä.
Imperatiivisella ohjelmoinnilla ei kyllä ole mitään tekoa Turingin koneen kanssa. Muistin käyttö on muuttujiin perustuvaa ja GOTO on poistunut käytöstä, joten nauhaa ei voi kelata tai tilojen välillä siirtyä vapaasti, ja abstraktio hajoaakin sitten jo siihen.
Ohjelmointitapojen eroja selvitellään myös tässä:
It may seem like this is obvious, but if you have a language with goto – a language where functions and everything else are built on top of goto, and goto can jump anywhere, at any time – then these control structures aren't black boxes at all! If you have a function, and inside the function there's a loop, and inside the loop there's an if/else, and inside the if/else there's a goto... then that goto could send the control anywhere it wants. Maybe control will suddenly return from another function entirely, one you haven't even called yet. You don't know!
Tronic kirjoitti:
If you have a function, and inside the function there's a loop, and inside the loop there's an if/else, and inside the if/else there's a goto... then that goto could send the control anywhere it wants.
Joissakin kielissä tosin on tarjolla vähävoimainen goto, eli sillä voi hyppiä, mutta ei mihin tahansa (esim. toiseen funktioon, for loopin sisään tms). Tällöin tuon tekstin "goto could send the control anywhere" ei pidä paikkaansa.
Näköjään muuten netistä löytyy jopa mielipiteitä, että goton hienosteltuja versioitakaan, kuten break ei pitäisi käyttää.
Lukaise se koko artikkeli, jos kiinnostaa. Molemmat mainitsemasi seikat ovat aiheina siinä.
No sehän se ongelma tietenkin on kun lainaa artikkelista pätkän näkyville. Eli lainatussa pätkässä käsiteltiin tilannetta aikana ennen nykyisiä kieliä, joissa goton toimintavapautta on rajoitettu.
Teuro kirjoitti:
(04.10.2019 18:36:48): ”– –” Nojaa toista taulukkoa ei tarvita mihinkään...
Toi idea on, että imperatiivisessa toi toinen taulukko tarvitaan, koska pitää luoda uudet numerot. Jos sen tekee noin kun tuossa sulla nyt on, se merkitsee eri asiaa kuin toi Haskell pätkä.
Selma-koira kirjoitti:
Mun mielestä ne perustuu eri abstraktiolle, yksi Turingin koneelle ja toinen lambda-laskennalle. Onko nyt kyse siitä voidaanko nämä kaksi asiaa määritellä samoiksi vai ei?
Ahaa, en todellakaan arvannut, että tarkoitit abstraktiolla tuollaista perimmäistä taustateoriaa. Tämä ei ole termin normaali käyttötapa ohjelmoinnissa. Ohjelmoinnissa abstraktiolla tarkoitetaan toteutuksen piilottamista: map-funktio on korkeamman tason abstraktio kuin sen toteutus, ja vielä korkeammalta abstraktion tasolta voi löytyä esimerkiksi funktio ratkaiseLabyrintti – riippumatta kielestä tai paradigmasta.
Tässä keskustelussa ei ole tietääkseni millään tavalla kyse Turingin koneesta ja lambda-laskennasta ja niiden välisestä yhteydestä. Tuskin kukaan edes miettii niitä tavallisessa ohjelmoinnissa.
Selma-koira kirjoitti:
jos halutaan hahmottaa miten tietoa käsitellään funktiolla, pärjätään alkuun hyvin ilman tyyppejäkin
Esimerkkisi ”map (+1) lista” edellyttää lukujen ja listan hahmottamista, vaikka sitä ei nimeltä sanottaisi. Toisaalta, jos mielestäsi tässä pärjätään ilman tyyppejä, sama tilanne esiintyy imperatiivisessa ohjelmoinnissa Pythonilla, eli mitään funktionaalisen ohjelmoinnin etua ei ole vieläkään tullut osoitetuksi.
Selma-koira kirjoitti:
Fp-edellyttää ton funktorin käsitteen, muuten siitä ei oikein tule mitään.
On totta, että opitut käsitteet usein ohjaavat ihmisten ajattelua. Toisaalta tästä käsitteiden ylivallasta voi pyrkiä eroon. Funktiot ja funktorit ja ties mitkä ovat koodin toteutusta (aivan samoin kuin muuttujat ja silmukat), eivät koodin ajatusta.
Varsinaisen ratkaisun kannalta olennaista on se, mitä ohjelmoija ajattelee. Ohjelman tai algoritmin suunnittelu voi tapahtua abstraktisti aivan irrallaan tietyn kielen käsitteistä ja määritelmistä.
Yleensä tiedonkäsittelyssä algoritmin suunnittelu alkaa siitä, että hahmotetaan ongelma ja sen osat. Käsiteltävän ongelman korkeimmasta abstraktiosta (ratkaiseLabyrintti) siirrytään matalampiin kerroksiin. Valittu ohjelmointikieli tai paradigma tulee vastaan viimeisenä; sen ei tarvitse vaikuttaa suunnitelmaan vaan toteutukseen. On täysin epäolennaista, tehdäänkö jokin toimenpide taulukolle lopulta map-funktiolla vai jollain muulla tavalla, koska tuossa vaiheessa ollaan jo erittäin matalalla tasolla toteutuksessa.
Asiaa voi verrata rakentamiseen: Ei kannata ostaa nauloja ja lautaa, jos ei ole edes piirustuksia. Toisaalta puisen rungon voi koota nauloilla tai ruuveilla, eikä tällä ole kovin suurta merkitystä piirustusten kannalta.
Loppukäyttäjälle tehdyissä ohjelmissa kuten vaikka graafisissa tietokantaohjelmissa on usein vähemmän ideaa ja enemmän toteutusta, joten niissä tilanne on erilainen kuin algoritmiohjelmoinnissa.
Pyysin aiemmin esittämään jonkin konkreettisen esimerkin, mutta eipä ole kuulunut. Esitän siis itse: Miten ratkaiset Datatähti 2020 -tehtävät? (Toki ei saa julkaista vielä vaan vasta kisan päätyttyä.) Vaikka käytännössä silmukat ja ehdot täyttävät suuren osan C++-koodista tehtävissä A–D, varsinainen ratkaisu ei ole näissä paradigmaan sidoksissa. E-tehtävä onkin kiinnostavampi sikäli, että siinä ei voi suoraan laskea oikeaa vastausta.
metabolix kirjoitti:
Ahaa, en todellakaan arvannut, että tarkoitit abstraktiolla tuollaista perimmäistä taustateoriaa. Tämä ei ole termin normaali käyttötapa ohjelmoinnissa. Ohjelmoinnissa abstraktiolla tarkoitetaan toteutuksen piilottamista: map-funktio on korkeamman tason abstraktio kuin sen toteutus, ja vielä korkeammalta abstraktion tasolta voi löytyä esimerkiksi funktio ratkaiseLabyrintti – riippumatta kielestä tai paradigmasta.
En suinkaan tarkoita abstraktiolla "perimmäistä teoriaa", vaan abstraktiota. Missä vaiheessa tekstiä päädyit päättelemään, että tässä keskustellaan toteutuksen piilottamisesta rajapinnan taakse? Ajattelin, että keskustelu oli jo ihan lähtökohtaisestikin abstraktiotasolla, jossa ei keskusteltu rajapinnoista. Teen paljon opetustöitä, laadin paljon oppimateriaalia ja otan mielelläni vastaan palautetta siitä, missä kohtaa aloit ajattelemaan, että kyse on itse määrittelemästäsi "normaalista"? Tämä olisi tärkeää tietoa monen opiskelijankin oppimisen hyväksi. Se on tärkeää myös siksi, että tämän ryhmän tarkoituksena on nimen omaan oppiminen. :)
Ajattelua voi laajentaa. Turingin koneeseen ja lambda-laskentaan liittyvät abstraktiot eivät taida edellyttää esittämäsi kaltaista hierarkiaa - maailmassa käsitteet voivat asettua myös vierekkäin.
Tässä ei keskustella kaiketi pelkästään asioista ohjelmoinnin "sisällä", vaan nimen omaan ulkopuolella - mitä ovat ne peruskäsitteet, joiden varassa erityyppisten ohjelmointikielten voidaan katsoa elävän ja olevan. Saitko kiinni ajatuksesta, vai onko ajatteluni edelleen sinulle epäselvää, koska kommenttiemme tyylistä voitaneen haistella, että emme ole ihan samalla sivulla :)
metabolix kirjoitti:
On täysin epäolennaista, tehdäänkö jokin toimenpide taulukolle lopulta map-funktiolla vai jollain muulla tavalla, koska tuossa vaiheessa ollaan jo erittäin matalalla tasolla toteutuksessa.
Tästä olemme kovasti eri mieltä. Sillä on hyvinkin paljon merkitystä, miltä lopputulos näyttää, juuri niistä syistä, jotka tämän jutustelun alkupäässä mainittiin: luettavuus, ilmaisuvoimaisuus ja sen sellaista. Pointti on, että se Haskell-kielen map-funktio ei ole matalalla tasolla toteutuksessa - se on vasta matkalla kääntäjään. :) Ja map-funktiolla ei tehdä "toimenpidettä taulukolle", vaan luodaan uutta. Jos siis tarkoitat "toimenpiteellä" manipulointia? En tee oletusta siitä mitä tarkoitat, siksi kysyn.
Mutta mukavaa, että näistä on erilaisia ja hyvin perusteltuja mielipiteitä. Ja vielä mukavampaa, että tällainen vanha pieru haastettiin ohjelmointiskabaan :). Olemme otettuja Selma-koiran kanssa!
Hauskaa kevättä 2021
jk. Kiva, että HY otti Haskell-MOOCin ohjelmistoonsa, way to go! Tämä on myös hyvää lukemista http://www.cs.ox.ac.uk/publications/books/adwh/
Selma-koira kirjoitti:
Toi idea on, että imperatiivisessa toi toinen taulukko tarvitaan, koska pitää luoda uudet numerot. Jos sen tekee noin kun tuossa sulla nyt on, se merkitsee eri asiaa kuin toi Haskell pätkä.
Myönnän etten ole funktionaalista ohjelmointia juurikaan kokeillut, mutta luulin tuossa listaan tulevan uusi arvo, joka yhden suurempi kuin siinä aiemmin ollut arvo. Mitä tuossa sinun versiossa siis tapahtuu?
Teuro kirjoitti:
(06.05.2021 12:59:11): ”– –” Myönnän etten ole funktionaalista...
Laittaisitko koodinpätkän tähän?
Viittaan tähän koodin, johon aikanaan kirjoitin oman version imperatiivisella koodilla. Luulen siis edelleen, että muuttujaan ´lista´ tulee uudet arvot, jotka ovat yhtä suuremmat kuin aiemmin. Jos näin on, niin minusta tuo oma versioni tekee saman asian. jos eroa on, niin kuulisin mieluusti mitä eroa näillä on keskenään.
Selma-koira kirjoitti:
lista = [1,2,3] map (+1) lista
lista = [1,2,3] map (+1) lista
map (+1) lista luo arvon [2,3,4], muuttujia ei ole. lista nimi tarkoittaa edelleen arvoa [1,2,3]. Jos funktion map (+1) tulos halutaan nimetä, voidaan kirjoittaa:
uuslista = map (+1) lista, jolloin uuslista tarkoittaa samaa kuin [2,3,4]
Optimoiko muuten kääntäjä / tulkki automaattisesti pois tuollaisen kutsun funktioon, jonka tulosta ei käytetä mihinkään? Varsinkin jos funktiolla ei ole sivuvaikutuksia, niin tuntuisi melko järkevältä.
Funktionaalinen koodi:
lista = [1,2,3] map (+1) lista
Vastaava koodi C:
int lista[3] = {1, 2, 3};
Tosin voihan tällaisen ajaa vaikka Javascriptillä
[1,2,3].map(a=>a+1)
Tulos [2,3,4]
Tästä näkee että ei tässä imperatiivisessakaan toteutuksessakaan tarvitse muuttujia erikseen tehdä.
Vai pitäisikö jopa todeta että useat nykykielet sisältävät piirteitä sekä funktionaalisesta että imperatiivisesta paradigmasta.
Tässä kun ei ole puhtaasti funktionaalisesta kielestä kyse, ei ole taattua ettei funktiolla olisi sivuvaikutuksia. Näin ollen kääntäjäkään ei voine optimoida tuota pois, vaikka toki funktion a+1 tapauksesa silmällä näkee helposti että sivuvaikutuksia ei ole.
Huomautan ensiksi, että tämä keskustelu on 1,5 vuotta vanha eikä siis välttämättä jatku nyt ihan samasta pisteestä.
Selma-koira kirjoitti:
En suinkaan tarkoita abstraktiolla "perimmäistä teoriaa", vaan abstraktiota.
Hieno homma, mutta tautologisella määritelmällä et ainakaan selvennä keskustelua yhtään.
En ole ennen kuullut, että Turingin kone ja lambda-laskenta olisivat ”abstraktioita”, vaan ne ovat matemaattisia malleja.
Selma-koira kirjoitti:
Missä vaiheessa tekstiä päädyit päättelemään, että tässä keskustellaan toteutuksen piilottamisesta rajapinnan taakse? – – missä kohtaa aloit ajattelemaan, että kyse on itse määrittelemästäsi "normaalista"?
Ensinnäkin tämä ei ole mikään ”itse määrittelemäni”. Keskustelemme ohjelmoinnista, jossa abstraktio on yleisesti käytetty termi monessa merkityksessä, ja olettamani merkitys on käytännön ohjelmoinnissa selvästi yleisin.
Sinä et ole määritellyt vieläkään, mitä tarkoitat. Tuorein määritelmäsi oli täysin hyödytön tautologia: tarkoitat abstraktiolla abstraktiota.
Olettamani merkitys myös on (yhä edelleenkin) keksimistäni ja tuntemistani vaihtoehdoista parhaiten sopiva siihen, missä yhteydessä itse ensin käytit termiä:
Selma-koira kirjoitti:
Korkeamman kertaluvun funktiot tarjoavat hyvän ja kohtuullisen vähän vuotavan abstraktion rekursion ympärille.
Lukutaitoni mukaan tuossa kirjoitat nimenomaan, että korkeamman kertaluvun funktion sisään voidaan piilottaa rekursiota käyttävä toteutus. Mielestäni tässä virkkeessä ei ole mitään mahdollisuuksia tulkita, että abstraktio tarkoittaisi vaikka Turingin konetta tai lambda-laskentaa.
Samoin:
Selma-koira kirjoitti:
Ohjelmoinnin voi aloittaa abstraktiotasolta, jossa toi map-funktiokin on.
En ihan ymmärrä tuota virkettä, ehkä siinä on jokin kirjoitusvirhe. Missä map-funktio on? Turingin koneessa tai lambda-laskennassako? Joka tapauksessa map-funktion sisällä on piilossa jokin toteutus (kuten rekursio tai for-silmukka), eli edelleen olettamani abstraktion määritelmä sopii tähän virkkeeseen.
Selma-koira kirjoitti:
Saitko kiinni ajatuksesta
En saanut.
Selma-koira kirjoitti:
Metabolix kirjoitti:
On täysin epäolennaista, tehdäänkö jokin toimenpide taulukolle lopulta map-funktiolla vai jollain muulla tavalla, koska tuossa vaiheessa ollaan jo erittäin matalalla tasolla toteutuksessa.
Tästä olemme kovasti eri mieltä. Sillä on hyvinkin paljon merkitystä, miltä lopputulos näyttää, juuri niistä syistä, jotka tämän jutustelun alkupäässä mainittiin: luettavuus, ilmaisuvoimaisuus ja sen sellaista.
Tästä edelleen herää kysymys, oletko ohjelmoinut mitään vaikkapa tuhansien rivien mittaista, oletko lukenut tämän keskustelun viestejä tai yritätkö vain ymmärtää tahallasi väärin.
Olen koko keskustelun koettanut selittää, että funktionaalinen ja imperatiivinen koodi eivät käytännössä eroa luettavuuden ja ilmaisuvoiman puolesta toisistaan, jos ne tekee oikein. Sinä kuitenkin olet tehnyt imperatiivisesta koodista nähdäkseni tahallisen huonoja esimerkkejä huonosti valitulla kielellä ja käytät niitä sitten oman agendasi perusteluna.
Mutta ei: SDP:n hallituskaudella koko yhteiskunta suljettiin ja satoja ihmisiä kuoli kulkutautiin. Kyseessä on selvästi totalitaarinen ja keskiaikainen puolue, taatusti huono valinta. Kommunistisen puolueen manifesti on yksinkertainen ja selvä, kannattaa äänestää heitä.
Selma-koira kirjoitti:
Haskell-kielen map-funktio ei ole matalalla tasolla toteutuksessa - se on vasta matkalla kääntäjään.
En tiedä, mitä yrität tällä sanoa tai mitä edes tarkoitat tässä matalalla tasolla. Itse tarkoitan edelleenkin sitä ohjelmoinnin abstraktion tasoa, jossa korkealla tasolla on vaikka ”lähetä esitäytetyt veroilmoitukset kaikille” ja matalalla tasolla on ”laske tähän henkilön ansiotulojen summa”.
Map-funktio on ehdottomasti matalalla tasolla toteutuksessa, koska se on vain työkalu mekaaniseen toimenpiteeseen. Tietysti jos ohjelma on niin alkeellinen, että sen ainoa tarkoitus on lisätä yksi jokaiseen taulukon lukuun, map-funktio voi olla samalla myös korkein asia toteutuksessa. Jos taas ohjelman tarkoitus on muodostaa esitäytetty veroilmoitus, map-funktion käyttö on merkityksetön yksityiskohta ja varmasti pienin murhe ohjelmassa.
Hehkutat map-funktiota esimerkkinä funktionaalisen ohjelmoinnin selvyydestä. Vertaat sitä virheellisesti vaikkapa for-silmukkaan. Mutta miten map-funktio on toteutettu? Yllättäen ei sekään ole enää niin helppolukuinen. Vertaa sitä toteutusta for-silmukkaan, ja vertaa funktionaalista map-funktiota imperatiiviseen map-funktioon, niin vertailussa on jotain järkeä.
Selma-koira kirjoitti:
Ja map-funktiolla ei tehdä "toimenpidettä taulukolle", vaan luodaan uutta.
Myös tämä on viime kädessä täysin merkityksetön yksityiskohta, joka on eri kielissä triviaalia korjata kumpaan tahansa suuntaan.
On lähinnä koomista, että kirjoitat ilmaisuvoimasta ja abstraktioista mutta esimerkkisi ovat enintään parin rivin mittaisia sormiharjoituksia etkä ole pyynnöstäkään saanut aikaan mitään isompaa esimerkkiä, joka oikeasti havainnollistaisi jotain merkittävää eroa (eikä vain sinun aiheuttamaasi sotkua). Huolestuttavaa, jos ohjelmointia jossain opetetaan näin kapeakatseisesta lähtökohdasta.
Minusta ohjelmoinnin yleisopetuksen pitäisi lähteä isommista konkreettisista kokonaisuuksista, joita voi jäsentää sanallisesti. Tämä jäsennys tapahtuu ihan samalla tavalla riippumatta ohjelmointikielestä, ja ohjelmointikieli on vain työkalu lopulliseen toteutukseen. Koodin sekavuus ja vaikealukuisuus ei yleensä johdu välineestä vaan ajattelutavasta: lähdetään tekemään koodia vähän sinne päin, vaikka ongelmaa ei ole vielä kunnolla jäsennetty.
metabolix kirjoitti:
Myös tämä on viime kädessä täysin merkityksetön yksityiskohta, joka on eri kielissä triviaalia korjata kumpaan tahansa suuntaan.
Miksi kuitenkin esim. Grezin esimerkissä map-funktio luo uuden listan, jos se on merkityksetön yksityiskohta ja täysin "triviaalia"? map-funktiot esiintyvät lukuisissa kielissä ja ne toimivat niin, että luodaan uutta dataa, ei manipuloida vanhaa. Miksi nuo kaikki map-funktiot luovat uutta dataa, jos se on täysin triviaalia? Miksi ne ylipäänsä ovat olemassa, jos ne edustaisivat jonkinlaista trivialiteettia? Miksi ne on tehty noin, vaikka mielestäsi ne olisi ihan "triviaalia" fiksata?
Se ilmaisuvoima ilmenee juuri niillä parilla rivillä - siinä se pointti just on.
Eikä tämä olet sotkua, vaan ihan täyttä asiaa.
Missä tilanteessa lähtisit JavaScriptin map-toteutusta "korjaamaan"?
Selma-koira kirjoitti:
Miksi kuitenkin esim. Grezin esimerkissä map-funktio luo uuden listan, jos se on merkityksetön yksityiskohta ja täysin "triviaalia"?
Niin yleisesti ottaen kielessä kuin kielessä ohjelmoija voi itse päättää haluaako luoda uuden vai muokata vanhaa ja siten kyseessä on triviaali eli merkityksetön yksityiskohta.
Selma-koira kirjoitti:
Missä tilanteessa lähtisit JavaScriptin map-toteutusta "korjaamaan"?
Eli jos haluaa Javascriptillä korjata toteutusta siihen suuntaan että ei luoda uutta vaan muokataan vanhaa, niin se onnistuu esim. seuraavasti:
lista = [1,2,3]; lista.forEach((o,i,a)=>{a[i]=o+1;});
Grez kirjoitti:
Niin yleisesti ottaen kielessä kuin kielessä ohjelmoija voi itse päättää haluaako luoda uuden vai muokata vanhaa ja siten kyseessä on triviaali yksityiskohta.
Millä perusteella voit tehdä tuon päätöksen, kumpaa tapaa on hyvä missäkin tilanteessa käyttää? Miten voit varmistua, että listan manipulointi on ok kaikissa tilanteissa? Onko siihen jotain hyvää keinoa, varsinkin, jos olet riippuvainen muiden tekemistä ohjelmista/komponenteista? Miten tuo "triviaali" yksityiskohta ratkeaa sellaisissa hyvin yleisissä tilanteissa, joissa emme tiedä mitkä muut ohjelman osat käyttävät samaa muuttujaa ja miten niiden osien logiikka riippuu muuttujasta?
Mistä tiedät, ettei jokin kirjasto xyz oo riippuvainen siitä datasta, jota sen sun koodi muuttaa? Aattelitko käydä ne kaikki kymmenkunta OSS-kirjastoa läpi ja tutkia? Miten sä sen käytännössä duunissa klaaraisit?
Selma-koira kirjoitti:
Miten sä sen käytännössä duunissa klaaraisit?
Yleisesti ottaen auttaa jos tietää mitä tekee.
Grez kirjoitti:
Selma-koira kirjoitti:
Miten sä sen käytännössä duunissa klaaraisit?
Yleisesti ottaen auttaa jos tietää mitä tekee.
Mut siis, jos et tiedä tai et voi olla varma, koska koodia, josta softasi riippuu on esim. 100000 riviä? Mitä sit teet? Heitätkö hanskat tiskiin ja toteet, että "liian hapokasta"?
No eipä ole tullut eteen tilannetta että tuo olisi ollut ongelma ja olisi pitänyt heittää hanskat tiskiin.
Yleisellä tasolla toki kysymykseesi "kummalla tavalla pitäisi tehdä" on vaikea vastata, koska riippuu ihan tilanteesta, milloin mikäkin on järkevää.
Ehkä jos heität jonkin konkreettisen esimerkin valintatilanteesta, niin siihen voisi vastata.
Selma-koira kirjoitti:
Miksi kuitenkin esim. Grezin esimerkissä map-funktio luo uuden listan, jos se on merkityksetön yksityiskohta ja täysin "triviaalia"?
Koska Grezin käyttämän kielen kehittäjä on päättänyt niin. Ajatukseen varmaan vaikuttaa se, että map-funktio yleisesti kielissä toimii näin ja olisi hämäävää tehdä samalla nimellä erilainen funktio.
Näyttää siltä, että taas ymmärsit tahallasi väärin. En sanonut, että map-funktiossa olisi vikaa tai sitä pitäisi ”fiksata”. Sanoin, että map-funktiolla ei ole suurta merkitystä. Tunnut tekevän siitä hirmu ison asian. Jos se on oikeasti noin tärkeä, miksi meillä ei ole yleisesti käytössä jaottelu ”map-funktion sisältävät kielet” ja ”kielet, joissa ei ole map-funktiota”?
Oletetaan, että Haskellista puuttuisi valmis map-funktio. Voi voi sentään, mikä neuvoksi?! Onko rakas ohjelmointikielesi nyt peruuttamattomasti rikki, vai onko ongelmaan ehkä yksinkertainen (eli triviaali) ratkaisu?
Sanon suoraan: Jos et pysty käsittelemään (ja myös esimerkkien kautta esittelemään) näitä ”abstraktioita” yhtään map-funktiota monimutkaisemmalla tasolla, mielestäni et ole oikea henkilö opettamaan ohjelmointia kenellekään.
Selma-koira kirjoitti:
Miten tuo "triviaali" yksityiskohta ratkeaa sellaisissa hyvin yleisissä tilanteissa, joissa emme tiedä mitkä muut ohjelman osat käyttävät samaa muuttujaa ja miten niiden osien logiikka riippuu muuttujasta?
Edelleenkö tahallista väärinymmärrystä? Jos on tilanne, jossa tieto pitää kopioida, sen voi sitten kopioida.
Taas vedät näitä ongelmia hatusta ilman yhtään konkreettista esimerkkiä. Ja ongelmat tietenkin ratkeavat puhtaasti funktionaalisella ohjelmoinnilla, jossa kyseistä ongelmat aiheuttavaa ohjelmaa tai kirjastoa ei edes olisi olemassa, koska se on mahdoton tehdä.
Käytännössä tiedon kopioimatta jättäminen on nopeampaa, monissa tilanteissa jopa hyvin olennaisella tavalla nopeampaa, ja sen hinta on se, että pitää ymmärtää riippuvuussuhteita eli sitä, minne on annettu ”sama” muuttuja ja missä sitä saa muokata. Painotan vielä, että aina voi luoda kopioita myös imperatiivisessa ohjelmoinnissa, jos se on tarpeen. Eli tästä asiasta on turha vääntää, edelleenkään ongelmat eivät johdu paradigmasta vaan koodin suunnittelusta. Jos ongelman kannalta on ok luoda kopioita datasta, tämän voi aivan vapaasti tehdä, ja ollaan tasoissa funktionaalisen ratkaisun kanssa.
Grez kirjoitti:
No eipä ole tullut eteen tilannetta että tuo olisi ollut ongelma ja olisi pitänyt heittää hanskat tiskiin.
Yleisellä tasolla toki kysymykseesi "kummalla tavalla pitäisi tehdä" on vaikea vastata, koska riippuu ihan tilanteesta, milloin mikäkin on järkevää.
Ehkä jos heität jonkin konkreettisen esimerkin valintatilanteesta, niin siihen voisi vastata.
Sulle on tarjolla JS-rajapinta, joss on funktio f.
Kutsut funktiota f ja saat sieltä taulukon, joss oletat olevan lukuja. Sun pitää lisätä jokaiseen lukuun 1 ja välittää tieto eteenpäin, vaikkapa jonkin toisen funktion parametrina. Käytätkö forEach vai map-ratkaisua? Et voi olla varma, onko jokin muu ohjelmanosa riippuvainen funktion f luomasta datasta. Miten toimisit?
Selma-koira kirjoitti:
Miten toimisit?
No mitäpä luulet, ja mitä tämä kysymys mielestäsi osoittaa?
Metabolix kirjoitti:
Selma-koira kirjoitti:
Miten toimisit?
No mitäpä luulet, ja mitä tämä kysymys mielestäsi osoittaa?
No mielipidettänne tässä kyselen? :)
En pitäisi tuota esimerkkiä kovin konkreettisena. Lähinnä tulisi mieleen että "miksi tällaista ollaan tekemässä?". Tuossa tapauksessa toki uuden taulukon luominen olisi järkevä vaihtoehto. Yhtä helppo olisi kirjoittaa toinen tuulesta temmattu esimerkki jossa saman taulukon päivittäminen olisi järkevämpi vaihtoehto.
Esimerkiksi jos ajatellaan funktiota, joka tekee pari sataa toimenpidettä taulukolle, niin on järkevää kopioida syötetaulukko uuteen taulukko vain kerran (ei välttämättä edes mapilla) ja sen jälkeen käyttää koko ajan sitä samaa taulukkoa. Sen saman työskentelytaulukon voi sitten lopuksi palauttaa ulos.
Kysymyksesi osoittaa vain sen, että yleensä kuhunkin tilanteeseen on täysin triviaalia löytää järkevä vaihtoehto, eikä kuvaamaasi "liian hapokasta tilannetta" yleensä tule vastaan.
Siksi itsekin ihmettelen, että mitä oikein yrität ajaa takaa?
Selma-koira kirjoitti:
Miten tuo "triviaali" yksityiskohta ratkeaa sellaisissa hyvin yleisissä tilanteissa, joissa emme tiedä mitkä muut ohjelman osat käyttävät samaa muuttujaa ja miten niiden osien logiikka riippuu muuttujasta?
Yksi mainio tapa ehkäistä ongelmia on välttää globaaleja muuttujia. Tässä ongelman hahmottamista auttaisi konkreettinen esimerkki merkittävästi. Millaisessa tilanteessa 100 k rivin ohjelmassa globaali muuttuja olisi järkevä ratkaisu - puhumattakaan sen mielivaltaisesta kopeloinnista.
Selma-koira kirjoitti:
No mielipidettänne tässä kyselen? :)
Ajatko tien oikeaa vai vasenta kaistaa? Oikea kaista on vapaa ja vasemmalla kaistalla saattaa joku ajaa satasta vastaan. Miten toimisit? Mielipidettänne tässä kyselen. :) Funktionaalisella autolla voi ajaa vain oikeaa kaistaa, vaikka siinä olisi betoniporsas tukkeena.
On varmaan parempi osaltani lopettaa tämä keskustelu, kun nähtävästi Selma-koiran tavoitteena ei ole edes esittää perusteluja vaan lähinnä herättää lisää keskustelua. Ehkä taustalla on jokin niin vahva usko Haskelliin, että riittävällä jankkaamisella muutkin väistämättä löytävät saman totuuden. Palataan asiaan sitten, jos saadaan jokin konkreettinen esimerkki ongelmasta, jossa funktionaalinen kieli tuottaa selvän koodin ja imperatiivinen kieli ei taivu yhtä selvään koodiin. Sehän se alkuperäinen väite oli, kun oli vastakkain map-funktio ja jokin umpisurkeasti väsätty C-for-silmukka.
Grez kirjoitti:
En pitäisi tuota esimerkkiä kovin konkreettisena. Lähinnä tulisi mieleen että "miksi tällaista ollaan tekemässä?"
Eli funktion alkajaisiksi olet tekemässä kopiota taulukosta ja sitten käsittelet kopiota ja palautat sen, koska et halua muuttaa alkuperäistä dataa. Ok.
Map-funktiolla saat tuon kaiken kätevästi aikaiseksi, jos sinulle kelpaa se, että lähtötiedoissa että tuloksessa on sama määrä alkioita, voit myös määritellä, miten uusi data syntyy, jokainen alkiohan käydään kuitenkin läpi. Mikä on se muunnos, jonka haluaisit taulukolle tehdä, missä map ei toimisi?
Näin siitä map-funktiosta tulee samalla funktio, joka tekee sen uuden, muuntuneen datan.
Miksi ajattelet, että joku kopioisi saman datan useasti? Mistä tulee tuo "vain kerran"? Mitä se tarkoittaa?
Selma-koira kirjoitti:
Miksi ajattelet, että joku kopioisi saman datan useasti? Mistä tulee tuo "vain kerran"? Mitä se tarkoittaa?
Tähän ei kyllä oikeastaan voi muuta sanoa kuin että lue tuo viestini, johon vastasit, vaikka pari sataa kertaa ajatuksella läpi. Sitten ehkä ymmärrät mitä siinä sanottiin.
Grez kirjoitti:
Tähän ei kyllä oikeastaan voi muuta sanoa kuin että lue tuo kirjoittamani viesti, johon vastasit, vaikka pari sataa kertaa ajatuksella läpi. Sitten ehkä ymmärrät mitä siinä sanottiin.
Ok. Eli ensin data kopioidaan paikasta a paikkaan b. Paikassa b dataa muutetaan -ja sitten "palautetaan".
Missä kohtaa tulee tarve manipuloida koko taulukkoa niin, että jokainen alkio joudutaan muuntamaan useammin kuin kerran? Mikä on se käyttötapaus, missä map ei kelpaa? mapilla saat uutta dataa niin, että vanhaan ei kajota, vähän kuin ne 2-in-1 shampoot, joissa on hoitoaine mukana.
Selma-koira kirjoitti:
Eli funktion alkajaisiksi olet tekemässä kopiota taulukosta ja sitten käsittelet kopiota ja palautat sen, koska et halua muuttaa alkuperäistä dataa. Ok.
Tässä yksittäisessä tapauksessa kopiointi on varmaankin fiksuinta ei välttämättä aina.
Selma-koira kirjoitti:
voit myös määritellä, miten uusi data syntyy, jokainen alkiohan käydään kuitenkin läpi. Mikä on se muunnos, jonka haluaisit taulukolle tehdä, missä map ei toimisi?
On hyvin helppoa toteuttaa silmukkarakenteella (joka voidaan piilottaa omaan funktioon) ja datan käsittelyssä ohjelmoija voi vaikuttaa syntyvään dataan yhtä lailla. Funktionaalinen paradigma ei eroa tässä mielessä kummoisesti.
Selma-koira kirjoitti:
Mikä on se käyttötapaus, missä map ei kelpaa? mapilla saat uutta dataa niin, että vanhaan ei kajota, vähän kuin ne 2-in-1 shampoot, joissa on hoitoaine mukana.
Kukaan ei ole väittänyt, ettei map kelpaisi. Kyse on siitä, että vastaava toiminto voidaan kirjoittaa ilmaisuvoimaisesti myös erilaisella paradigmalla, eikä funktionaalinen erotu mitenkään.
Teuro kirjoitti:
Kukaan ei ole väittänyt, ettei map kelpaisi. Kyse on siitä, että vastaava toiminto voidaan kirjoittaa ilmaisuvoimaisesti myös erilaisella paradigmalla, eikä funktionaalinen erotu mitenkään
Mapista näet heti, että on muunnos kyseessä, koska tiedon rakenne ei voi muuttua (tarkoittaa tässä samaa määrää alkioita sisään ja ulos). Jos näet for-silmukoita, pitää tutkia tarkemmin, mitä siinä oikein tehdään.
Sitä sillä ilmaisuvoimalla tarkoitetaan ja map-funktio on ilmaisuvoimaisempi tapa ilmaista ohjelmakoodilla muunnos kuin for-luuppi. Ei se for-luuppi mitenkään ilmaisuvoimaton ole, mutta vähemmän ilmaisuvoimainen kuin map. Säästää ohjelmoijan aikaa, kun näkee heti, mitä siinä tehdään ja aika usein töissä siitä ajansäästöstä on hyötyä, miksei harrastuksissakin :)
Vertaat nyt edelleen lyhytä ja yksinkertaista funktiokutsua raakaan keskelle koodia huonosti kirjoitettuun silmukkaan. Tottakai tuosta on helppoa tehdä sekava esimerkki. Jos toisenlainen lähestymistapa koodataan fiksusti ovat kuten on osoitettu varsin kosmeettisia. Edelleen funktionaalinen tapa on varmasti fiksu ja toimmiva, mutta ei missään nimessä ainoa hyvä tai edes paras. Oletan että myös funktionaalisesta tavasta voidaan tehdä sekava ja vaikeasti ymmärrettävä versio. Huonosti kirjoittaminen ei toimi oikein hyvin.
metabolix kirjoitti:
Koodin sekavuus ja vaikealukuisuus ei yleensä johdu välineestä vaan ajattelutavasta: lähdetään tekemään koodia vähän sinne päin, vaikka ongelmaa ei ole vielä kunnolla jäsennetty.
Yksi hauska jäsennys on esim. lähteä pohtimaan olisiko johonkin hyvä ratkaisu vaikkapa map, filter, fold (reduce) tai niiden yhdistelmä. Mutta toki voi miettiä for-luuppienkin avulla, jos niin haluaa. Eli onko kyseessä tiedon muuntaminen, suodattaminen tai kokonaan uuden tiedon luominen vanhan perusteella, myös rakenteen osalta.
Sit voi järjestää vaikka hauskan skaban, että kummalla tavalla koodista on helpompi tehdä päätelmiä - silmukoista vai map, filter ja reduce funktioista. Näin on joskus kursseilla tehtykin.
Monet opiskelijat usein haluavat ymmärtää, millaisissa tilanteissa nuo kolme funktiota hyvin istuisivat työkaluiksi ongelmanratkaisutilanteissa. Enemmistö on kolmen viime vuoden aikana lähtenyt suosimaan näitä funktioita, mitä JavaScript-oppilaisiin tulee. Kun siitä tulee tapa, koodi on kaikille helpompaa tulkita.
Ehkä ohjelmointiputkalla suositaan enemmän for-luuppeja tai sitten on vaan kovin pieni otos. Ilman muuta ko. asiaa kannatta lähteä tutkimaan ja ottaa siitä ilo irti :) Ei vakavasti, mutta vakaasti :) Ihan kiva opas on tämäkin: http://learnyouahaskell.com/
Antti Laaksonen kirjoitti:
(27.09.2019 10:34:31): Hienoa saada suomenkielistä materiaalia...
Kyllä se Antti niin lienee, että se on ehkä matematiikkaa, mutta aika paljon vähemmän matematiikkaa kuin funktionaalisissa kielissä, etenkin niissä puhtaammissa?
Tämä lienee sulla jo hyllyssä, mutta vaikkei ei olisikaan, niin olisi mukava kuulla kommenttisi teoksesta.
https://www.amazon.com/Algorithm-Design-Haskell-Richard-Bird-ebook/dp/B08BKXJ1N3
Hauskaa kevättä!
Selma-koira kirjoitti:
Mikä on se käyttötapaus, missä map ei kelpaa?
Esimerkkejä käyttötapauksista löytyy vaikka kuinka, pieni otanta: lajittelu, salaus (esim. AES), tiivistäminen (esim. SHA256), pakkaus ja purku (esim. jpg, zip).
Nämä kaikki pystyisi varmaan tekemään puhtaasti funktionaalisella ohjelmoinnillakin, mutta yleensä ne toteutetaan muilla kielillä ja sitten niitä käytetään funktionaalisesta kielestä, ihan vaan koska funktionaalisesti toteutettuna monet noista olisi paitsi huomattavasti tehottomampia, myös hankalampia kirjoittaa ja ylläpitää.
Enkä tiedä tuosta Haskellin map-funktiostakaan, että onko sen toteutus Haskellilla vai jollain muulla kielellä.
Eli jos haluaa pystyä tekemään "ihan kaikkea" funktionaalisella kielellä, niin täytyy käytännössä pystyä tekemään myös imperatiivisella kielellä.
Ehkä olen väärässä, mutta ainakin mitä nyt ensimmäiseksi googletin "SHA256 Haskell implementation" niin tarjottiin vain C:llä toteutettua kirjastoa Haskelliin.
Grez kirjoitti:
Esimerkkejä käyttötapauksista löytyy vaikka kuinka, pieni otanta: lajittelu, salaus (esim. AES), tiivistäminen (esim. SHA256), pakkaus ja purku (esim. jpg, zip).
Mun kysymys liittyi tuohon koodiin ja sen ilmaisuvoimaan, että onko siinä siis ongelma lähinnä tuo suorituskyky (kopioidaan turhaan dataa, algoritmi toteutuu tehokkaammin ilman), vai onko sun tekemissä algoritmeissa muutakin, mihin map ei istu? Miltä susta itestä tuntuu, vai kirjoitteletko lähinnä kryptoalgoritmeja C:llä? Minkä tyylisä juttuja teet esim. töissä?
C:llä nuo algoritmit tuntuvat usein olevan toteutettu. Mistä sulla sellanen olo on tullut, että noi olis vaikea kirjoittaa tai ylläpitää Haskellilla, siis jos osaa? Mä en tunne noita algoritmeja ollenkaan, etten osaa sanoa mitään.
Selma-koira kirjoitti:
Mun kysymys liittyi tuohon koodiin ja sen ilmaisuvoimaan, että onko siinä siis ongelma lähinnä tuo suorituskyky (kopioidaan turhaan dataa, algoritmi toteutuu tehokkaammin ilman), vai onko sun tekemissä algoritmeissa muutakin, mihin map ei istu? Miltä susta itestä tuntuu, vai kirjoitteletko lähinnä kryptoalgoritmeja C:llä? Minkä tyylisä juttuja teet esim. töissä?
Useimmissa imperatiivisissa ohjelmointikielissä on nykyään mahdollista myös kirjoittaa koodia funktionaalisella tyylillä ja ihan kaikilla tyyleillä saa kyllä kirjoitettua vaikealukuista koodia. Taitaa isoin etu esim. Rust:illa iteraattorien käytöstä verrattuna yksinkertaisiin silmukoihin nähden olla se, että kääntäjä pystyy helpommin optimoimaan koodin.
En näe mitään ongelmaa siinä, että ohjelmointikielestä löytyy erikseen myös map-toteutus mikä muokkaa alkuperäistä taulukkoa. Esim käyttämäni ohjelmointikieli 8th tajoaa map= nimisen map-toteutuksen, mikä muokkaa alkuperäistä taulukkoa...
Näin on. Tässä on mukava vertailu. Haskell-osaamisella on helppo siirtyä Rustiin tai vaikka Scalaan. https://www.fpcomplete.com/blog/collect-rust-traverse-haskell-scala/
Mutta iteraattorien käyttö ainakin tuon Snoymanin mukaan olisi hitaampaa? Oletko itse mittaillut ja saanut eri tulokset? Vai onko tuo vanhaa tietoa?
https://www.fpcomplete.com/blog/2017/07/
Rustissa on todella paljon fiksuja monesta fp-kielestä tuttuja piirteitä ja yksi parhaista on immutability oletuksena. Tosi fiksun oloinen kieli!
Selma-koira kirjoitti:
Mutta iteraattorien käyttö ainakin tuon Snoymanin mukaan olisi hitaampaa? Oletko itse mittaillut ja saanut eri tulokset? Vai onko tuo vanhaa tietoa?
Rustissa on todella paljon fiksuja monesta fp-kielestä tuttuja piirteitä ja yksi parhaista on immutability oletuksena. Tosi fiksun oloinen kieli!
Iteraattorien kanssa kääntäjän ei tarvitse tarkastella pysytäänkö taulukon rajojen sisällä (array bounds check), joten luulisin iterattorien käytön Rustin tapauksessa olevan nopeampaa.
Omat silmäni eivät ole oppineet tykkäämään syntaksista ja laiska kun olen niin haluan saada kirjoiteltua jotain toimivaa valmiiksi nopeasti enkä tapella borrow checkerin kanssa...
Niin näyttää olevan. Aika paljon parempi kuin pyöritellä klassista for-luuppia. Hyvä, että ei pysty edes tekemään sellaista. Ei tarvitse miettiä edes asiaa. Tosin joku kohta sanoo, ettei sillä oo mitään merkitystä, koska "pitää vaan olla huolellinen" :) ehkä päästään lopullisesti c-tyyppisestä for-luupista eroon, jos Rust yleistyy. Sillä voi kuitenkin kirjoittaa hyvin suorituskykyistä koodia ja hienoa, että on muistinhallinta, eikä edes manuaalista.
Selma-koira kirjoitti:
Ehkä ohjelmointiputkalla suositaan enemmän for-luuppeja
Kertaus on perinteinen oppimistapa, joten kirjoita sata kertaa: for-silmukka ja imperatiivinen ohjelmointi ovat kaksi eri asiaa.
En tajua, mikä pakkomielle sinulla on esittää for-luuppi ajattelun välineenä, vaikka se on työkalu. Ei mikään aikuisten oikea ohjelma ala siitä ajatuksesta, että hei, montako muuttujaa ja for-luuppia tähän tarvitaan. (Näinhän esitit käsityksesi imperatiivisesta ohjelmoinnista keskustelun alussa.) Kerran jo selitin, että rakentajan ongelma ei ole, millä välineellä palikat laittaisi kiinni, vaan mihin ne pitää laittaa. Ihan sama, onko käytössä nauloja vai ruuveja, ei se ole ajattelua hallitseva asia.
Ja edelleen vertaat sitä for-luuppia korkeamman tason funktioihin, niin pääset esittämään, miten ajatusmalli ja ilmaisuvoima on erilainen. Se on väärä vertaus!
Mutta jos välttämättä haluat tehdä vertailua for-silmukkaan, niin tehdään. For-silmukalla voidaan toteuttaa map-funktio, fold-funktio ja reduce-funktio. Miten toteutat ne funktionaalisella kielellä?
Kirjoita Haskellilla vastine vaikka tälle Python-koodille. Et saa käyttää valmista map-funktiota, koska tehtävänä on toteuttaa map-funktio.
def oma_map(funktio, lista): return [funktio(x) for x in lista]
Jos tuo suora listan muodostus ahdistaa, niin ei se koodi paljon pitene, vaikka silmukan ottaisi erilleen:
def oma_map(funktio, lista): tulos = [] for x in lista: tulos.append(funktio(x)) return tulos
Myös rekursiota voi Pythonissa käyttää, jos haluaa. Funktionaalisessa kielessä ei taida olla muuta vaihtoehtoa tähän ongelmaan.
Eli nyt sinun pitää osoittaa, että tämän funktion sisäinen toteutus on mahdollista selvemmin ja ilmaisuvoimaisemmin funktionaalisella kielellä, tai sinun pitää keksiä jokin parempi argumentti.
Selma-koira kirjoitti:
Yksi hauska jäsennys on esim. lähteä pohtimaan olisiko johonkin hyvä ratkaisu vaikkapa map, filter, fold (reduce) tai niiden yhdistelmä. Mutta toki voi miettiä for-luuppienkin avulla, jos niin haluaa. Eli onko kyseessä tiedon muuntaminen, suodattaminen tai kokonaan uuden tiedon luominen vanhan perusteella, myös rakenteen osalta.
Edelleen pyörit alkeistasolla, jossa ongelma on niin pieni, että on tärkeintä miettiä, sopiiko siihen map-funktio vai for-silmukka. Olen yrittänyt kysyä vähän isompaa esimerkkiä, mutta et ole vieläkään keksinyt sellaista.
Jotta koodissa olisi edes yli 2 riviä, otetaan harjoitukseksi vaikka Datatähti 2020 -kilpailun B-tehtävä, Merkkijonot:
Merkkijonot A ja B ovat harmoniset, jos ne ovat yhtä pitkät ja seuraavat ehdot pätevät kaikissa kohdissa: Jos A:n kahdessa kohdassa on sama merkki, niin myös B:n vastaavissa kohdissa on sama merkki. Jos A:n kahdessa kohdassa on eri merkki, niin myös B:n vastaavissa kohdissa on eri merkki.
Sinulle annetaan lista, jossa on n merkkijonoa, ja tehtäväsi on laskea harmonisten merkkijonoparien määrä. Jokainen merkkijono muodostuu merkeistä A–Z ja siinä on enintään 50 merkkiä.
Miten tehtävä taittuu Haskellilla? Jotta ei tarvitse lukea tiedostoa, otetaan harjoitusdataksi vaikka tehtävän esimerkkidata: "AAB", "ABKA", "SSG", "TSGT", "ZZZZ", "KEAK".
Tehtävä havainnollistaa sitä, mitä olen koko ajan sanonut: ei varmaan sinullakaan suurin kysymys ole tuossa, millä tempulla listaa käsittelee, vaan ongelman ydin on siinä, miten tunnistetaan tehtävässä kysytyt harmoniset parit (mielellään jollain järkevällä nopeudella eikä vertaamalla jokaisen merkkijonoparin jokaista merkkiparia).
Jos Selma-koira ei osaa tehdä tätä yksinkertaista tehtävää, täytyy tulkita, että Selma-koira vain soittaa suutaan täällä. Jos taas saadaan tästä kiinnostava funktionaalinen ratkaisu, voidaan lähteä miettimään vielä vähän pidempiä koodeja.
Mainittakoon vielä, että oma Python-ratkaisuni tuohon oli noin 20 riviä ja varmasti pystyn tekemään tuon myös funktionaalisesti ja hätätapauksessa jopa Haskellilla. Kiinnostaisi vain nähdä, mikä on Selma-koiran ratkaisu, kun Selma-koira niin kovasti toitottaa tätä oman ajattelutapansa ylivertaisuutta.
Jos tämä tehtävä oli Selma-koiralle liian helppo, Selma-koira voi esitellä myös vaikka, miten sudoku ratkaistaan tms.
Tai jos nämä tehtävät ovat Selma-koiralle liian vaikeita ymmärtää, voisiko Selma-koira sitten itse kertoa, minkälaisia koodeja Selma-koira yleensä kirjoittaa. Voidaan sitten yhdessä ihailla niiden ilmaisuvoimaa. Toistaiseksi ei ole Selma-koiralta nähty kovin hienoja eikä erityisen ilmaisuvoimaisia koodeja.
Otanpa vielä lopuksi käsittelyyn Selma-koiran kirjan viimeisen ja siis oletettavasti hienoimman esimerkin.
Selma-koiran kirja kirjoitti:
luokitus=[("Pupun jäljestys","Metsästys"), ("Hirven jäljestys","Metsästys"), ("Linnun noutaminen","Metsästys"), ("Lumen pöllyytys","Pihatyöt"), ("Kukkapenkkien kaivaminen","Pihatyöt"), ("Parvekkeen vahtiminen", "Muut"), ("Piilotetun luun löytäminen","Muut"), ("Oman hännän jahtaaminen","Muut"), ("Kuun ulvonta","Muut")] topinTodistus'= [("Pupun jäljestys",9), ("Hirven jäljestys",8), ("Linnun noutaminen",8), ("Lumen pöllyytys",10), ("Kukkapenkkien kaivaminen",10), ("Parvekkeen vahtiminen",9), ("Piilotetun luun löytäminen",7), ("Oman hännän jahtaaminen",9), ("Kuun ulvonta",8)] data LuokkienSummat = LuokkienSummat {metsästys::Int, pihatyöt::Int, muut::Int} summatAiheittain' = foldr (\x acc->case (luokittele (fst x)) of "Metsästys" -> acc {metsästys=(metsästys acc)+snd x} "Pihatyöt" -> acc {pihatyöt=(pihatyöt acc)+snd x} "Muut" -> acc {muut=(muut acc)+snd x}) LuokkienSummat {metsästys=0, pihatyöt=0, muut=0} topinTodistus' luokittele taito = snd(head(filter(\z-> (fst z)==taito) luokitus))
Sivuhuomiona, kirjan koodi piti koota palasista ja siitä puuttui sisennys ja case-rakenteesta yksi sulkumerkki (”case (luokittele (fst x) of”), eli kirja ei vaikuta kovin laadukkaalta.
Lisätään koodiin vielä tulostus, niin saadaan se edes ajettua kokonaisena ohjelmana.
teksti (LuokkienSummat metsästys pihatyöt muut) = "Metsästys " ++ show metsästys ++ ", piha " ++ show pihatyöt ++ ", muut " ++ show muut main = putStrLn (teksti summatAiheittain')
Tämä koodi on nyt ilmeisesti Selma-koiran taidonnäyte funktionaalisen ohjelmoinnin ilmaisuvoimasta. Voi voi sentään.
Tässä on Pythonilla tehty imperatiivinen versio.
def luokittele(taito): return next(luokka for taito2, luokka in luokitus if taito == taito2) summat = {'Metsästys': 0, 'Pihatyöt': 0, 'Muut': 0} # tai: summat = dict((luokka, 0) for _, luokka in luokitus) for taito, numero in topinTodistus: summat[luokittele(taito)] += numero print(', '.join(f'{asia}: {summa}' for asia, summa in summat.items()))
Annoin tasoitusta ja pidin tuon luokituksen alkuperäisessä Haskell-muodossa, jolloin piti tehdä myös luokittele-funktio. Oikeasti tieto voisi olla dict-rakenteessa, josta saisi suoraan hakasulkumerkinnällä oikean tuloksen.
Mutta kas, koodissa onkin taas for-silmukka, joten sen täytyy varmaan olla automaattisesti huonompaa ja epäselvempää kuin sinun Haskell-koodisi. Hassua, en edes ajatellut asiaa, kun kirjoitin koodia. Tietysti voisin Pythonissa käyttää myös samanlaisia temppuja kuin Haskellissa, mutta en näe niillä mitään lisäarvoa tässä koodin selvyyden tai ilmaisuvoiman suhteen.
Selma-koira kirjoitti:
Aika paljon parempi kuin pyöritellä klassista for-luuppia. – – Tosin joku kohta sanoo, ettei sillä oo mitään merkitystä, koska "pitää vaan olla huolellinen" :) ehkä päästään lopullisesti c-tyyppisestä for-luupista eroon, jos Rust yleistyy.
Taas vedät jostain mukaan tuon C-tyyppisen for-luupin, vaikka sinä olet ainoa, joka siitä puhuu täällä. Ihan jo C++ sisältää toisenlaisen for-silmukan, for (auto x: lista)
.
Katso vaikka näitä Python-tyyppisiä for-luuppeja, puhutaan niistä. Vai eikö niissä ole tarpeeksi valittamisen aihetta Selma-koiralle?
Kertaus on opintojen äiti, joten kirjoita vielä sata kertaa: C-kielen for-silmukka ja imperatiivinen ohjelmointi ovat kaksi eri asiaa.
C-kielen for-silmukka muuttuvine indekseineen on imperatiivisen ohjelmoinnin ilmentymä parhaimmillaan ja pahimmillaan. Mukavaa, että siitä päästään eroon kielissä, joihin otetaan piirteitä funktionaalisen ohjelmoinnin puolelta yhä enemmän. Tulee turvallisempaa ja luettavampaa koodia. Rustissa tulee hahmonsovitukset ja kaikki, ja nopee suorittaa. Mahtava juttu :)
Otan nyt kantaa tähän imperatiivinen/fuunktionaalinen ohjelmointi, vaikka en nyt koko ketjua olekaan lukenut.
Yksi syy käsitykseni mukaan on, miksi funktionaalinen ohjelmointiparadigma alkoi joskus syrjäyttämään imperatiivista paradigmaa, on se, että tajuttiin, että imperatiivinen ohjelmointi sisältää paljon toistoa, ja menee mielenterveys, jollei se ole jo ohjelmoijalla mennyt, jos ei löytyisi keinoa vähentää toistoa.
Olio-paradigma on vallitseva ohjelmointiparadigma tällä hetkellä 2021, mutta siinäkin päädytään piirteisiin funktionaalisesta ohjelmoinnista, ja miksei myös imperatiivisestakin ohjelmoinnista, tietotyypit, joista oliomuistipaikka-viittaukset ja ilmentymät luodaan, sisältää funktioita attribuuttien ja konstruktoreiden lisäksi. Käsittääkseni funktio palauttaa aina arvon? Voin olla väärässä, joku voi korjata, jos tämä ei pidä paikkansa, että ei tule virheellistä tietoa nyt aivan 100-varmana faktana esitettyä.
Mutta taitaa olla niin, että C-osaamisella ei työpaikan löytymisen kannalta enää ole juuri merkitystä, trendaavia kieliä taitaa olla Python, JavaScript, R, ja sitten vielä käytetään C++:aakin oliokielistä, mutta Javakin on hyödyllisempi taito hallita, mitä C jo vuonna 2021 ehkä?
Unix Hater's Handbook -kirjassa teoksen alkuperäis-englannin kielen versiossa kirjan kirjoittaja pitää C++:aa 90-luvun COBOLina otsikkotasolla, mutta onhan siinäkin se moniperinnöllisyys sekava, ja vaikein asia ymmärtää on ne pointterit, osoittimet suomeksi.
Naulan kantaan Jere. Oli se aika turhauttavaa aikoinaan laatia mm. erillinen funktio johonkin tapahtumankäsittelyyn - nyt mennään lambdoilla ja "aina soi". Moni juttu on mennyt niin luettavampaan kuin turvallisempaakin suuntaan. Option/Maybe alkaa löytymään jo kielestä jos toisestakin.
Metabolix, jos tykkäät vertailla kieliä, noi Snoymanin esimerkit on hyviä, niissä näkyy hyvin rakenne-erot.
Työpaikan näkökulmasta keskimäärin paras veikkaus on ollut JavaScript pari viime vuotta, varmaan vielä ensi vuonnakin.
Mutkikkaat perintähierarkiat oli hankala juttu, näin on. Traitit (scala, rust) näyttävät saavan koko ajan lisää potkua. Tunnetaan Haskellissa tyyppiluokkina. Ei ole olioita ikävä :)
Selma-koira kirjoitti:
Metabolix, jos tykkäät vertailla kieliä, noi Snoymanin esimerkit on hyviä, niissä näkyy hyvin rakenne-erot.
Harmi vain, että nuo esimerkit eivät liity tähän alkuperäiseen aiheeseen oikeastaan mitenkään, kun tuolla tehdään suuntaan ja toiseen Rustilla ja Haskellilla asioita mahdollisimman samanlaisella koodilla.
Loppuivatko argumentit kesken, kun et vastannut mitään edellisiin viesteihini ja lähdit viemään keskustelua tuollaiseen suuntaan?
Sinä henkilökohtaisesti olet väittänyt, että funktionaalisen ohjelmoinnin etuja ovat ”luettavuus, ilmaisuvoimaisuus ja sen sellaista”. Siksi haluaisin nähdä sinulta henkilökohtaisesti myös ratkaisuja esitettyihin ongelmiin tai jotain pidempiä esimerkkejä, jotta toisit keskusteluun muutakin kuin mielipiteen huutelua faktana. Tähän mennessä ainoa täällä antamasi esimerkki on yhden map-kutsun ja huonosti kirjoitun for-silmukan vertailu. Kirjasi koodit ovat vain muutaman rivin mittaisia, yllä esittelin yhden niistä, siinäkin Python-versiosta tuli lyhyempi (eli kai vastaavasti ilmaisuvoimaisempi?) ja mielestäni myös helppolukuisempi.
Vai onko sittenkin parempi ilmaisuvoima on saavutettu ainoastaan vanhaan C-kieleen verrattuna ja yrität nyt selittää asiaa parhain päin sen kautta, että kaikki muiden kielten parannukset ovatkin vain ”piirteitä funktionaalisen ohjelmoinnin puolelta”?
Selma-koira kirjoitti:
Työpaikan näkökulmasta keskimäärin paras veikkaus on ollut JavaScript pari viime vuotta, varmaan vielä ensi vuonnakin.
Mitä olen aktiivisesti seurannut AMK:sta valmistumiseni jälkeen TE-keskuksen avoimet tyopaikat -palvelua ja Linkediniä käytän myos tyonhaussa kotikaupunkini Turun alueella IT/Ohjelmistokehitys-tyopaikkoja, niin noista kehityskehyksistä JavaScriptin osalta AngluarJS, React, Node.JS ja pythonin osalta Ruby on Rails on sellaisia, joissa aika moni tyopaikka hakee jonkun vuoden tyokokemuksella tyontekijoita.
Itsekin käytin php:n opiskeluun opinnäytetyovaiheessa aikaa jonkin verran, joka oli yksi syy, miksi tutkielman laatiminen kesti niin pitkään, niin ei php enää ole oikein suosiossa tyonantajien tyopaikkailmoituksissa.
Turun alueella muutoinkin melko vähän ohjelmistokehitys-tyopaikkoja, jos laajentaisi koko Suomen alueelle, pitäisi olla valmis muuttamaan rakkailta kotikulmilta vieraaseen kaupunkiin, ja jos olisi auto käytossa, niin sitten Saloon saakka voisi laajentaa rekrytointi-toiveita, kun sielläkin on rakenteilla se IoT-kampus, vaikka siellä enää Nokian matkapuhelintehdasta siinä määrin olekaan enää, mitä 90-luvun lopussa/2000 luvun vaihteessa. Mitä olen katsonut Googlenkin ohjelmistokehitys-avoimia paikkoja, niin lähimmät taitaa olla Tukholmassa Junior Java kehittäjän paikat, ja JavaScript, niin ei juuri ole tuota alkupääomaa kertynyt, että olisin valmis tällä kokemuksella lähtemään ihan vielä ulkomaille toihin, vaikka Ruotsi nyt juuri ulkomaita olekaan. Ja voi olla samat ongelmat ilman mitään validia tyokokemusta päästä EU-alueelle Suomen ulkopuolellekaan toihin, kun ei Suomessakaan pääse.
Jos katsoo Googlesta noita trendaavia kieliä 2021, niin aika paljon artikkeleita ja keskustelua loytyy, mitkä nyt on kuumia ohjelmistofirmojen kysynnän suhteen.
Tuollainenkin artikkeli tuli, jossa kysyttiin, ongelmanasettelu oli aseteltu niin, että "Kannattaako ohjelmointia opiskella vielä 2020-luvulla", niin mielestäni kannattaa, koska no tietoverkkojen merkitys kasvaa jatkossakin entisestään, niin tietoturva ja nimenomaan verkkotietoturva on yksi mielenkiintoisimmista ja järkevämmimmistä tietokoneistus-aiheita, ja sitten ohjelmointi on järkevintä, mitä tietokoneella voi tehdä, ohjelmoinnin avullahan tietokoneen saa tekemään juuri sitä, mitä sen haluaakin saada tekemään, niin siinä mielessä on turhaa puhetta, etteiko ohjelmointia kannattaisi opiskella.
Jere Sumell kirjoitti:
Itsekin käytin php:n opiskeluun opinnäytetyovaiheessa aikaa jonkin verran, joka oli yksi syy, miksi tutkielman laatiminen kesti niin pitkään, niin ei php enää ole oikein suosiossa tyonantajien tyopaikkailmoituksissa.
Meidän tänä vuonna päättyvässä isommassa koulutuksessa 2 opiskelijaa päätyi PHP-hommiin, 1 tekee sulautettuja C:llä, 9 laatii JavaScriptillä joko käyttöliittymiä tai taustapalveluja. 1 tekee puhtaita UI-töitä. PHP on hyvin hyödyllinen mm. noiden julkaisujärjestelmin muuttelemiseen. Kyllä niitäkin hommia on julkaisujärjestelmien kanssa vielä pitkään - hyvä ja koeteltu työkalu.
Viime vuonna meillä oli Turusta Tampereelle muuttanut opiskelija, joka päätyi Go-hommiin. Työnantajilla on paljon toiveita sellaisten sovelluskehysten ja työkalujen suuntaan, joissa korostuu turvallisuus - aika usein vaatimuksena on, että työkalun tulisi mahdollisimman hyvin tukea ko. toimintaympäristön tarpeita, myös uusia kieliä kokeillaan.
Edelleen yritykset tekevät todella paljon verkkosovelluksia ja kommunikaatio-ohjelmoinnin perusteet ovat vuosien varrella osoittautuneet tärkeäksi. Tähän pannaan kursseilla paljon paukkuja.
Sovellukset muodostuvat aika usein keskenään verkon yli juttelevista komponenteista - tietoa käsitellään ja aika usein tehdään muunnoksia (map) ja joukkojen rajauksia (filter) tavalla tai toisella - useimmiten työkalu on Java tai JavaScript. Mitään rakettitiedettä tuo ei ole ja töitä on. Web-teknologioiden opiskelu ei ole iso ponnistus sekään, harjoittelemalla oppii.
Reactin osalta on huomattu, että se on ollut erinomainen ponnahduslauta monelle töihin. Kun siihen yhdistetään tuo kommunikaatio-ohjelmoinnin oppimäärä, työpaikat ovat auenneet hyvin. Kun Redux korvataan Context API:lla saadaan verrattain yksinkertainen ja ylläpidettävä ratkaisu. Angular on vaikeampia opittava ja sovelluksen pitää olla aika suuri, että siitä saadaan hyötyä.
Tähän on lisätty sopiva annos funktionaalista ohjelmointia, koska se on ollut erinomainen tapa lähestyä tuota ongelmaa, missä dataa tulee, dataa käsitellään ja dataa menee. Algoritmisesti ongelmat ovat suhteellisen yksinkertaisia, tuotteiden ja palvelujen lisäarvo perustuu usein muihin seikkoihin. Kursseilla pidetään myös aina perinteiset Haskell-sessiot, jossa vertaillaan imperatiivisten ja funktionaalisten työkalujen ja käsitteiden eroja.
Ohjelmointikursseja järjestävien Opiframen ja Sarasen ohjelmointikursseilla on tosi hyvät sisällöt työllistymisen kannalta. Myös oululainen Buutti tekee hyvää duunia asioiden eteen ko. teemoissa, varmaan on muitakin. Meillä on yhtenä teemana nuo funktionaalisen ohjelmoinnin alkeet, joka on tämän keskustelun aihe ja olemme katsoneet aiheen tärkeäksi myös tuommoisilla full stack -ohjelmointikursseilla. Moni opiskelija on lueskellut myös yliopistojen full stack -aiheisia MOOC:cceja, hyviä ovat nekin, niissä korostuvatkin usein teknologiat, kuten React, Redux tai GraphQL, usein esittelyluonteisesti, kuten tuollaisella lyhyellä MOOC:illä idea onkin.
Hieno juttu on, että niin Buutti https://buuttiedu.com/tyonantajille/ohjelmointikoulutukset/javascript-koulutus/ kuin Opiframekin https://opiframe.com/javascript-oulu-kesa2020/ ovat sisällyttäneet fp:n oppisisältöihin. Molemmissa on otettu funktionaalinen ohjelmointi mukaan! Uskon, että sisältö ilmestyy Sarasenkin kursseille, ellei jo ole ilmestynyt.
Java-pohjaisilla oliosovelluksilla on vielä pitkä elinkaari, koska koodia on paljon isoissa järjestelmissä, jotka työllistävät paljon käsipareja. Paradigma elää ja voi hyvin, sekin. Ohjelmointia aloitteleva tai alanvaihtaja on kuitenkin usein liian ison haasteen edessä, jos heitetään suoraan taustajärjestelmään suunnittelemaan. Usein tarvitaan vuosi tai pari, että päästään sisään järjestelmään. Käyttöliittymistä on helpompi aloittaa ja silloin JavaScript on usein se kieli, millä mennään.
Ilman muuta kannattaa opetella, jos tykkää :)
Metabolix: No ne piirteet nyt ovat esiintyneet funktionaalisissa ohjelmointikielissä. Ei se sen kummempaa ole kuin sanoa, että ilmaisu jenkki on anglismi. Sanan alkuperä on erilaisessa kulttuurissa.
Kirjan ideana on, että ongelmia voi lähteä ratkomaan täysin tietämättömänä rekursion käsitteestä, kuten kirjassa todetaan. Ihan samalla tavalla kuin ei tarvitse tietää miten nuo Rustin iteraattorit on toteutettu. Siinä on tarjottu yksi mahdollisuus jäsentää ohjelmoinnin maailmaa, ilman for-tyyppistä toistorakennetta painottaen datan muunnoksen käsitettä. :) Minun mielestäni tämä on oleellinen seikka, kun ohjelmoijia valmistetaan niihin haasteisiin, joita työpaikoilla tätä nykyä on. Ja kyllä, tämä on nimen omaan mielipide. Sellaista se on, ihmisillä on erilaisia mielipiteitä ja se on hyvä juttu :) Ja kiitos noista huomiosta, jotka liittyvät tuohon kirjaan!
Ja tällaiset keskustelupalstat, kuten tämä putka ovat erinomaisia kanavia jakaa kokemuksia ohjelmoinnista ja saada vinkkejä ja jakaakin. Hauskaa, että Antti Laaksonen jo nuorena tämän laittoi pystyy. Ja hienosti on teknisestikin toiminut ja keskustelua riittää :)
Jere Sumell kirjoitti:
Turun alueella muutoinkin melko vähän ohjelmistokehitys-tyopaikkoja, jos laajentaisi koko Suomen alueelle, pitäisi olla valmis muuttamaan rakkailta kotikulmilta vieraaseen kaupunkiin,
Ei kannata ton takia heittää kirvestä kaivoon, nykyään tehdään paljon etätöitä. Lisäksi JavaScriptin käyttäminen esim. React-sovellusten tekemisessä ei ole ihan tekemätön paikka lyhessäkään ajassa, jos ohjelmoinnin perusteet on hanskassa.
metabolix kirjoitti:
Näyttää siltä, että taas ymmärsit tahallasi väärin. En sanonut, että map-funktiossa olisi vikaa tai sitä pitäisi ”fiksata”. Sanoin, että map-funktiolla ei ole suurta merkitystä. Tunnut tekevän siitä hirmu ison asian. Jos se on oikeasti noin tärkeä, miksi meillä ei ole yleisesti käytössä jaottelu ”map-funktion sisältävät kielet” ja ”kielet, joissa ei ole map-funktiota”?
Kyllä näitä luokitteluja löytyy. https://en.wikipedia.org/wiki/Map_(higher-order_function)
Ja ihan kiva tapa opetella tuo idea on vaikka Haskell. :)
Metabolix kirjoitti:
Kirjoita Haskellilla vastine vaikka tälle Python-koodille. Et saa käyttää valmista map-funktiota, koska tehtävänä on toteuttaa map-funktio.
def oma_map(funktio, lista): return [funktio(x) for x in lista]
Tässä oma map-funktio Haskellilla:
omaMap f [] = [] omaMap f (x:xs) = f x : omaMap f xs
Olen samaa mieltä, että olisi hyvä vertailla esim. Haskellia ja Pythonia jossain todellisessa projektissa niin, että annetaan halutun sovelluksen kuvaus ja sitten Haskell- ja Python-asiantuntijat toteuttavat sen mahdollisimman hyvin kielelle ominaisella tyylillä.
Selma-koira kirjoitti:
Hauskaa, että Antti Laaksonen jo nuorena tämän laittoi pystyy. Ja hienosti on teknisestikin toiminut ja keskustelua riittää :)
Mukavaa, että Selma-koirakin on löytänyt tänne. Teknistä toimivuutta ei voi kyllä lukea minun ansiokseni.
Antti Laaksonen kirjoitti:
Mukavaa, että Selma-koirakin on löytänyt tänne. Teknistä toimivuutta ei voi kyllä lukea minun ansiokseni.
Tuo metabolix olikin näköjään jo aiempaan keskusteluun lisännyt tuon kirjoittamasi map-toteutuksen, tosin hän oli lisäillyt siihen vielä funktion allekirjoituksen, joten siitä tuli vähän pidempi. Haskellin tyyppipäättely on hyvä juttu. Mutta hyvä se on kerrata, mutta vaikka mulle kertausta kovasti muutama viesti takaperin suositeltiin, niin tuota mun ei tarvii enää (kerrata). :)
Selma on täällä jo kuin kotonaan, haukkuja tulee, mutta elämä on :)
Meillä on per opiskelija suhteellisen vähän aikaa käytettävissä. Olemme huomanneet, että rekursion oppiminen algoritmien laatimiseksi sillä tasolla, että siihen saisi edes jonkinlaista päällekäyvää kätevyyttä, on todella pitkä ja kivinen tie. Siksi olemme keskittäneet opetusvoimamme korkeamman asteen funktioihin. Ne tuntuvat toimivan hyvin aikuisilla ja miksei jo lukioikäisilläkin. Lisäksi ne ovat niin yleisesti käytössä, että käyttöä on tulevaisuudessakin.
Rekursion idea on usein haastava opittava, ihan paradigmasta riippumatta. korkeamman asteen funktioita voi oppia hyödyntämään näkemättä/pohtimatta map-funktion toteutusta - en minä sitä silti olisi alan korkeakoulututkinnoista noin vain pudottamassa - lyhyemmillä kursseilla, joissa on toiset oppimistavoitteet, se on välttämätöntä. Toki jokainen voi itse opiskella lisää.
Muistan elävästi vuonna 1983, kun olin saanut ensimmäisen tietokoneeni, että ainoat tietolähteet olivat Sinclair Spectrum ohjekirja ja Mikrobitti ja Printti -lehdet.
On ihan eri lähteä täällä kysymään asioista ja parhautta on myös ohjelmointikielten kirjo tällä palstalla! Hienoa täällä on myös tuollainen kaikenlainen kokeiluihin innostaminen ja ylipäänsä se, että kokeillaan. On matalan tason rekistereihin kirjoittamisesta aina Haskelliin asti!
En niin kovin ole nuorten kanssa tekemisissä, mutta jos on puhe tullut, mistä vinkkiä saa, niin aina on Putka tullut mainittua, koska usein into lähtee juuri kokeiluista, kokeilee sitten funktionaalista tai mitä vaan.
Just vinkkasin facebookin ohjelmointiryhmässäkin, että tulevat tänne keskusteleen, ketkä tykkäävät.
Teknisestä toimivuudesta kiitokset sille, kenelle ne kuuluvat!
Käytännön ohjelmointitöissä, varsinkin erilaisissa hajautetuissa järjestelmissä tulee tämän tästä tilanteita, että jostain tulee tietoa, jota pitää käsitellä ja sit ehkä siirtää tietoa taas toisaalle. Ajattelin, että tällainen yksinkertainen esimerkki palvelisi useimpia.
Uskoisin, että olisi pedagogisesti hedelmällisempää vertailla vaikkapa C++ kieltä ja Haskellia - Pythonissa on fiksusti jo paljon fp-ominaisuuksia, ja oppimisen kannalta uskoisin monelle täälläkin erojen näkemisen olevan oleellisempaa. Ei siksi, että Pythonissa olisi jotain vikaa, vaan siksi, että erot näkyisivät selvemmin. Samalla voidaan tutkia C++:n etuja ja keskustella vaikka pohtia Haskellin ongelmia, niitä on yllin kyllin, ellei tätä lähtökohtaa katsota "epäreiluksi"? Mutta ei sen niin väliä ole, olen käyttänyt näissä vertailuissa yleensä klassisia imperatiivisia työkaluja, koska monen oppilaan tausta on usein "klassikoissa", kuten C++ ja Java, ei Pythonissa. Mutta teillä on paras tietämys, kuka näitä juttuja lukee, niin mikä hyvältä tuntuu, mistä lukijat eniten hyötyä saavat ja oppivat uutta.
Selma-koira vetää ympäri Suomea funktionaalisen ohjelmoinnin työpajoja erilaisissa koulutustapahtumissa, joita pidetään eri kaupungeissa. Millaisin ilmauksin saataisiin selville ne eri kaupungit, joissa Selma-koira esiintyy, että tiedetään millainen Suomen kiertue on tulossa, jos työpajoihin haluaa osallistua? Käytössä on 3 funktiota:
etsiTyöpajat - tälle voi antaa koiran nimen argumentiksi, ja se evaluoituu työpajoiksi (lista), jotka koira vetää
etsiKoulutustapahtumat - tämä saa argumentikseen työpajan ja evaluoituu koulutustapahtumiksi (lista)
etsiKaupungit - tämä saa argumentikseen koulutustapahtuman ja evaluoituu kaupungeiksi (lista)
Nyt pitäisi laatia ohjelma, joka esittää ongelman ratkaisun. Haskell-kielellä se voisi mennä vaikka näin:
haeKaupungit esiintyjä = nub $ do työpajat' <- etsiTyöpajat esiintyjä koulutustapahtumat' <- etsiKoulutustapahtumat työpajat' kaupungit' <- etsiKaupungit koulutustapahtumat' return kaupungit'
Ja sit käyntiin:
> haeKaupungit "Selma" ["Tampere","Tornio","Lahti","Kemi"]
Selma-koira kirjoitti:
Rekursion idea on usein haastava opittava, ihan paradigmasta riippumatta. korkeamman asteen funktioita voi oppia hyödyntämään näkemättä/pohtimatta map-funktion toteutusta - en minä sitä silti olisi alan korkeakoulututkinnoista noin vain pudottamassa - lyhyemmillä kursseilla, joissa on toiset oppimistavoitteet, se on välttämätöntä. Toki jokainen voi itse opiskella lisää.
Itse en ole mikään erityinen rekursion ystävä. Esimerkiksi Sudokun tai miinaharava pelin tapauksessa kirjoitan mielummin iteratiivisen version mikä käyttää listaa tai jonoa tallentamaan tehtyjä siirtoja. Mielestäni rekursio tekee koodin kulusta vaikeammin seurattavan ja virheen etsimisestä vaikeampaa. Suorituskyvyn kannalta paras rekursio tilanne on häntärekursio, mutta silloinhan se oikeasti käytännössä onkin vaan tuo vihaamasi perus silmukka...
Selma-koira kirjoitti:
haeKaupungit esiintyjä = nub $ do työpajat' <- etsiTyöpajat esiintyjä koulutustapahtumat' <- etsiKoulutustapahtumat työpajat' kaupungit' <- etsiKaupungit koulutustapahtumat' return kaupungit'
Ehdotettu merkkijono-ongelma taisi olla liian vaikea, kun päädyit tällaiseen 5-riviseen taidonnäytteeseen.
nub-funktion aikavaativuus on O(n^2). Oletko edennyt Haskell-opinnoissasi listoja pidemmälle? Onneksi Selma-koiran koulutustapahtumat taitavat olla niin harvassa, ettei koodin tehokkuudella ole väliä.
std::vector<std::string> haeKaupungit(std::string esiintyja) { std::set<std::string> kaupungit; for (auto tyopaja : etsiTyopajat(esiintyja)) for (auto tapahtuma : etsiKoulutustapahtumat(tyopaja)) for (auto kaupunki : etsiKaupungit(tapahtuma)) kaupungit.insert(kaupunki); return std::vector<std::string>(kaupungit.begin(), kaupungit.end()); }
jalski kirjoitti:
Itse en ole mikään erityinen rekursion ystävä. Esimerkiksi Sudokun tai miinaharava pelin tapauksessa kirjoitan mieluummin iteratiivisen version mikä käyttää listaa tai jonoa tallentamaan tehtyjä siirtoja. Mielestäni rekursio tekee koodin kulusta vaikeammin seurattavan ja virheen etsimisestä vaikeampaa. Suorituskyvyn kannalta paras rekursio tilanne on häntärekursio, mutta silloinhan se oikeasti käytännössä onkin vaan tuo vihaamasi perus silmukka...
Minulla oli aikoinaan varsinkin lautapelialgoritmeissa ongelmana se, että pinon kokoa ei saanut tarpeeksi isoksi, kääntäjässä taisi olla joku max 10 MB tms., jota ei voinut ohittaa. Se oli 90-lukua se. Tuon takia piti pelata taulukoilla rekursion sijaan ja käyttää kekoa. Ristinolla ja Othello, nuo muistan. Molemmat VC++ kääntäjällä.
En minä mitään vihaa. Näin iloisesti pyörii minimax, https://www.youtube.com/watch?v=pWS8AkEZOME.
Ihan hymyssä suin olen koko ajan, vaikka kuvaa ei näykään ja vaikka silmukoilla mennäänkin :)
jlaire kirjoitti:
nub-funktion aikavaativuus on O(n^2). Oletko edennyt Haskell-opinnoissasi listoja pidemmälle? Onneksi Selma-koiran koulutustapahtumat taitavat olla niin harvassa, ettei koodin tehokkuudella ole väliä.
Heheh, no järjestetään kurssi, niin asia korjaantuu!
Saattaa olla pikkulistoilla nopeampi kuin muut vaihtoehdot, mutta tässä oli kai asiana tuo koodin luettavuus ja ilmaisuvoimaisuus.
jlaire kirjoitti:
Oletko edennyt Haskell-opinnoissasi listoja pidemmälle?
Vaikea sanoa, koska en tiedä, mitkä ovat ne päät? Mistä sinä aloittaisit Haskell-opiskelun?
Selma-koira kirjoitti:
Ja kyllä, tämä on nimen omaan mielipide.
Onkin ensimmäinen kerta, kun kerrot, että kyseessä on vain mielipide. Tähän asti olet esittänyt kaiken faktana, joka muidenkin vain pitäisi ymmärtää ja hyväksyä. Mutta oli sitten fakta tai mielipide, perusteluja pitäisi olla, ja niitä ei ole toistaiseksi nähty sinun puoleltasi juurikaan.
Selma-koira kirjoitti:
Tuo metabolix olikin näköjään jo aiempaan keskusteluun lisännyt tuon kirjoittamasi map-toteutuksen,
Jaa, itse en ole tällaisesta tietoinen, eli mistähän ihmeestä kyseinen koodi löytyy?
Selma-koira kirjoitti:
Uskoisin, että olisi pedagogisesti hedelmällisempää vertailla vaikkapa C++ kieltä ja Haskellia
C++ on erityisen huono valinta paradigmojen vertailuun, koska C++ on monessa asiassa poikkeuksellisen monimutkainen kieli (mm. tarkat muuttujien tyypit, datan ja osoitinten erottelu, template), jolloin erityisen suuri osa koodin eroista liittyy näihin piirteisiin eikä itse paradigmaan. Juuri edellä jlaire demonstroi hienon Haskell-esimerkkisi C++-versiota, jossa ensimmäinen näkyvä ero Haskelliin ovat nuo pitkät muuttujatyyppien määrittelyt – paradigman puolesta merkityksetön mutta C++:n kannalta välttämätön piirre.
Python on hyvä valinta (ainakin imperatiivisiin) esimerkkeihin, koska se on tunnettu ja helppolukuinen ja näennäisen yksinkertainen kieli. Usein jo C++-koodin muuttaminen Pythoniksi tuottaa selvemmän ja lyhyemmän tuloksen, vaikka pidettäisiin täysin sama imperatiivinen rakenne. (Tietysti Python on yleensä paljon hitaampi, siksihän C++ on yhä käytössä.) Python-koodissa ei ole mitenkään pakko käyttää funktionaalisia ratkaisuja, jos haluaa demonstroida toteutusta ilman niitä.
Kahden eri kielen vertailussa väistämättä näkyvät kielten erot eivätkä vain paradigmojen erot. Ohjelmointiputkan historiassa on nähty, miten esimerkiksi C-tyyliset aaltosulut ja BASIC-tyyliset if-endif-lohkot ovat olleet iso asia aloittelijoille, vaikka ero on lähinnä kosmeettinen ja paradigma on sama. Ihan jo C:ssä tarpeelliset #include-rivit ja main-funktio ovat olleet myrkkyä QBasic-koodareille. Edellä mainitut C++:n ominaisuudet peittävät alleen melkein minkä tahansa muun vertailtavan asian, jos ei ole tottunut sitä lukemaan.
Paras valinta paradigmojen vertailuun olisi siis jokin kieli, jossa molemmat paradigmat on riittävällä tavalla huomioitu, jotta voisi kirjoittaa samalla kielellä kaksi erilaista versiota. Jos tämä ei ole mahdollista, pitäisi valita kielet, jotka muistuttavat mahdollisimman paljon toisiaan, ettei huomio kiinnity epäolennaiseen.
Selma-koira kirjoitti:
Pythonissa on fiksusti jo paljon fp-ominaisuuksia
Sinulla näyttää olevan hirmu tiukka käsitys, mikä kelpaa imperatiiviseksi koodiksi ja miten kaikki hyvät jutut ovat fp-ominaisuuksia. Kuitenkin itse käytät surutta muuttujia ja do-rakennetta, joka on Haskellin tapa kirjoittaa imperatiivista koodia. Käytit sitä jopa äsken esimerkissäsi, miten hienosti Selman koulutuspaikat löytyvät. Eli sisältääkö Haskell fiksusti imperatiivisia ominaisuuksia? Eikö oikea funktionaalinen ratkaisu ole tarpeeksi selvä?
Funktionaalisen ohjelmoinnin esimerkiksi tarkoitettu koodisi on itse asiassa malliesimerkki imperatiivisesta ohjelmoinnista: joka rivillä kutsutaan funktiota ja sijoitetaan tulos muuttujaan, ja lopuksi komennolla palautetaan tämä tulos. Täysin imperatiivinen rakenne! Mielestäni olisi selvempää tässä tapauksessa laittaa myös nub do-lohkon sisään (vaikka return-riville), koska nykyisellä rakenteellasi se jää helposti huomaamatta.
Tässä on Selma-koiralle vielä pieni koiramainen harjoitusohjelma ennen seuraavaa viestiä.
riittävänPitkä perustelu = length perustelu > 300 todellaLyhyt perustelu = length perustelu < 50 arvioi perustelu | riittävänPitkä perustelu = "Jes! Ota naksu! Odota, kun ihmiset lukee!" | todellaLyhyt perustelu = "Hyi! Tuhma Selma! Huonosti perusteltu!" | otherwise = "Kuka on huono perustelija, niin, kuka on, niin, kuka kuka!" main = do putStrLn "Selma, väitä! Väitä!" väite <- getLine putStrLn "Selma, perustele! Perustele! No, perustele!" perustelu <- getLine putStrLn (arvioi perustelu)
Kuten ehkä huomaat, näin lyhyt koodi on lähinnä vitsi eikä mikään vakava argumentti paradigman puolesta tai vastaan.
metabolix kirjoitti:
Funktionaalisaen ohjelmoinnin esimerkiksi tarkoitettu koodisi on itse asiassa malliesimerkki imperatiivisesta ohjelmoinnista: joka rivillä kutsutaan funktiota ja sijoitetaan tulos muuttujaan, ja lopuksi komennolla palautetaan tämä tulos. Täysin imperatiivinen rakenne! Mielestäni olisi selvempää tässä tapauksessa laittaa myös nub do-lohkon sisään (vaikka return-riville), koska nykyisellä rakenteellasi se jää helposti huomaamatta.
Ei siinä ole muuttujia, eikä mitään palauteta, eikä mitään kutsuta. Ei ole funktion kutsumisen käsitettä, ei paluuarvon käsitettä eikä muuttujiakaan, kuten ne imperatiivisissä kielissä ymmärretään.
Ei ole myöskään do-rakennetta, on do notaatio.
Tässähän se Haskellin hauskuus juuri on - paitsi, että se on funktionaalinen, se mahdollistaa asioiden suoritusjärjestyksen esittämisen silloin, kun se on tarpeen - jokin asia voi tapahtua vasta, kun jotain muuta on tapahtunut ensin. Se on sitä ilmaisuvoimaa.:)
Selma-koira kirjoitti:
Ei siinä ole muuttujia, eikä mitään palauteta, eikä mitään kutsuta. Ei ole funktion kutsumisen käsitettä, ei paluuarvon käsitettä eikä muuttujiakaan, – – Ei ole myöskään do-rakennetta, on do notaatio.
Aprillipäivä meni jo.
Onko siis niin, että koodin ei tarvitse oikeasti olla funktionaalista, jos laittaa silmät kiinni ja kaataa naksupurkista lisää määritelmiä Selma-koiran kippoon?
Esimerkiksi muuttujan käsite löytyy hyvin yksiselitteisestä lähteestä, Haskell 2010 Language Report, kohta 3.14: ”As indicated by the translation of do, variables bound by let have fully polymorphic types while those defined by <- are lambda bound and are thus monomorphic.”
Muutkaan asiat eivät muutu toisiksi sillä, että niitä katsoo eri laseilla. Olkoon paluuarvo tai funktion tulos, olkoon funktiokutsu tai funktion käyttö tai mikä tahansa.
Vai oletko sitä mieltä, että tuo Haskell-koodisi on funktionaalinen, koska semanttisesti ajattelet niin, mutta tämä käytännössä identtinen Python-koodi onkin sitten imperatiivinen, koska tässä on takuulla muuttujia, funktiokutsuja ja paluuarvoja?
def haeKaupungit(esiintyjä): työpajat = etsiTyöpajat(esiintyjä) koulutustapahtumat = etsiKoulutustapahtumat(työpajat) kaupungit = etsiKaupungit(koulutustapahtumat) return set(kaupungit)
P.S. Turha väittää, että tässä käytettäisiin ”fp-ominaisuuksia”, kun tässä käytetään hyvin tarkkaan vain perinteisiä imperatiivisia ominaisuuksia, joihin muuttujat ja funktiot (aliohjelmat) kuuluvat.
metabolix kirjoitti:
kuten ne imperatiivisissä kielissä ymmärretään.
Tämä osa oli tärkeä! :)
Selma-koira kirjoitti:
metabolix kirjoitti:
kuten ne imperatiivisissä kielissä ymmärretään.
Tämä osa oli tärkeä! :)
Niin, ymmärsin, että se oli sinulle tärkeä. Minusta se on käytännön kannalta lähinnä semanttista saivartelua. Kysyin (jopa esimerkin kera), että onko nyt ainoa funktionaalisen ja imperatiivisen ohjelman ero se, miten ne ”ymmärretään”, kun koodi ja lopputulos voi olla identtinen.
Tässä tuli demonstroitua tavallaan myös se, mitä aiemmin sanoin map-funktion ja for-silmukan välisestä ”kisasta”: kyseessä on pieni toteutuksen yksityiskohta, jolla ei ole merkitystä. Nythän jo tehtävän muotoilussa annoit käyttöön korkeamman tason rajapinnan tapahtumien ja paikkojen käsittelyyn. Varmaan sisäisesti sielläkin täytyy olla map tai silmukka tai muu vastaava ratkaisu, mutta sillä ei ole merkitystä tällä korkeammalla abstraktion tasolla.
Selma-koira ainakin sai selkeästi todistettua, että funktionaaliset kielet eivät lähtökohtaisesti ole esim. sen ilmaisuvoimaisempia kuin muutkaan kielet vaan kielien erot ilmaisuvoimaisuudessa johtuvat kokonaan muista syistä kuin impreatiivisuus vs. funktionaalisuus.
Grez kirjoitti:
Selma-koira ainakin sai selkeästi todistettua, että funktionaaliset kielet eivät lähtökohtaisesti ole esim. sen ilmaisuvoimaisempia kuin muutkaan kielet vaan kielien erot ilmaisuvoimaisuudessa johtuvat kokonaan muista syistä kuin impreatiivisuus vs. funktionaalisuus.
Tässä Python-koodissa ei toteudu tuo annettu speksi, funktioiden etsiTyöapajat ja etsiKaupungit argumenteilla on eri tyypit.
Haskell-kielen ilmaisuvoimaisuus tulee pääasiassa kahta kautta: yhtäältä siitä, että muuttuvaa dataa ei ole, mistä johtuu, että ohjelma on kasattava funktioiden yhdistämisellä ja toisaalta kielen tyyppijärjestelmästä.
Haskellin do-notaatiota käytetään usein IO-monadin (tai jonkun tilamonadin) kanssa kirjoittamaan imperatiivistä koodia. Selma-koiran esoteerisessä esimerkissä käytetään hieman yllättävästi lista-monadia, jolloin koodi on puhtaasti funktionaalista ja rivien välissä on implisiittisiä concatMap-operaatioita. Siksi proseduraalisessa versiossa on sisäkkäisiä silmukoita.
Lista-monadissa "return x" tarkoittaa täsmälleen samaa kuin "[x]", eli muodostetaan yhden alkion kokoinen lista. Returnia ei ole syytä käyttää koodissa, joka käsittelee vain listoja.
Saman voisi kirjoittaa myös ilman do-notaatiota, joko monadin bind-operaattorilla tavallisilla funktioilla:
haeKaupungit1 esiintyja = nub $ etsiKaupungit =<< etsiKoulutustapahtumat =<< etsiTyopajat esiintyja haeKaupungit2 = nub . concatMap etsiKaupungit . concatMap etsiKoulutustapahtumat . etsiTyopajat haeKaupungit3 = nub . concat . map etsiKaupungit . concat . map etsiKoulutustapahtumat . etsiTyopajat
Tai näin:
haeKaupungit' esiintyja = nub [ kaupunki | tyopaja <- etsiTyopajat esiintyja , koulutustapahtuma <- etsiKoulutustapahtumat tyopaja , kaupunki <- etsiKaupungit koulutustapahtuma ]
Tai miksei näin?
haeKaupungit_forM esiintyja = nub . concat . concat . concat $ forM (etsiTyopajat esiintyja) $ \tyopaja -> forM (etsiKoulutustapahtumat tyopaja) $ \koulutustapahtuma -> forM (etsiKaupungit koulutustapahtuma) $ \kaupunki -> return kaupunki
Enemmän do-notaatiota, enemmän ilmaisuvoimaa?
haeKaupungit_dododo esiintyja = nub . concat . concat . concat $ do forM (etsiTyopajat esiintyja) $ \tyopaja -> do forM (etsiKoulutustapahtumat tyopaja) $ \koulutustapahtuma -> do do forM (etsiKaupungit koulutustapahtuma) $ \kaupunki -> do do do return kaupunki
Antti Laaksonen kirjoitti:
Olen samaa mieltä, että olisi hyvä vertailla esim. Haskellia ja Pythonia jossain todellisessa projektissa niin, että annetaan halutun sovelluksen kuvaus ja sitten Haskell- ja Python-asiantuntijat toteuttavat sen mahdollisimman hyvin kielelle ominaisella tyylillä.
Jlaire: Ajattelin seurailla tuota Laaksosen Antin viisasta kommenttia jonkin asian toteuttamisesta kielelle ominaisella tyylillä. Se oli minusta hyvä idea ja liittää tämän keskustelun hyvin niihin käytänteisiin, joita ohjelmistokehittäjät töissään suosivat. :)
Selma-koira kirjoitti:
Jlaire: Ajattelin seurailla tuota Laaksosen Antin viisasta kommenttia jonkin asian toteuttamisesta kielelle ominaisella tyylillä.
Hyvä idea! Oletko jo valinnut todellisen projektin? Muista jakaa koodi tänne, kun saat valmiiksi.
jlaire kirjoitti:
Selma-koira kirjoitti:
Jlaire: Ajattelin seurailla tuota Laaksosen Antin viisasta kommenttia jonkin asian toteuttamisesta kielelle ominaisella tyylillä.
Hyvä idea! Oletko jo valinnut todellisen projektin? Muista jakaa koodi tänne, kun saat valmiiksi.
Mutta Antin idean huono puoli on, että maksava asiakas, jolla on projekti, ei ehkä maksa saman speksin toteuttamisesta kahdella eri työkalulla. Lisäksi voi myös kysyä, tuoko se ilmaisuvoimaisuuskeskusteluun lisäarvoa lopulta. :)
Mistä sinä sellaisen jlaire löytäisit? :)
jlaire kirjoitti:
Selma-koiran esoteerisessä esimerkissä käytetään hieman yllättävästi lista-monadia, jolloin koodi on puhtaasti funktionaalista ja rivien välissä on implisiittisiä concatMap-operaatioita. Siksi proseduraalisessa versiossa on sisäkkäisiä silmukoita.
Mikä sinut siinä "yllätti" tai mikä teki siitä "esoteerisen", mitä sillä tarkoitatkaan?
metabolix kirjoitti:
def haeKaupungit(esiintyjä): työpajat = etsiTyöpajat(esiintyjä) koulutustapahtumat = etsiKoulutustapahtumat(työpajat) kaupungit = etsiKaupungit(koulutustapahtumat) return set(kaupungit)
Huomasitko, että tuo ratkaisusi ei noudata speksiä?
jlaire kirjoitti:
Returnia ei ole syytä käyttää koodissa, joka käsittelee vain listoja.
Liittyikö tämä nyt minun kirjoittamaani koodiin vai omaasi, vai yleisesti johonkin muuhun asiaan?
Selma-koira kirjoitti:
Tässä Python-koodissa ei toteudu tuo annettu speksi,
jlaire kirjoitti:
rivien välissä on implisiittisiä concatMap-operaatioita.
En olisi ikinä arvannut, että kun edellisellä rivillä oleva funktio tuottaa listan, seuraavalla rivillä sama nimi (joka on vielä kirjoitettu monikossa!) ei olekaan enää lista vaan vain yksi asia. Mutta näköjään Haskellissa näinkin voi tapahtua. Mielestäni tässä suhteessa koodin luettavuus on siis heikko, koska tämä erikoinen ominaisuus olisi pitänyt jostain vain tietää ja myös monikkomuotoinen nimeäminen johtaa harhaan.
-- Miksi tämä... arvot <- f(x) g(arvot) -- ... on eri kuin tämä: g(f(x))
Eli aivan totta, en huomannut, että Python-koodi ei noudata speksiä.
Toisaalta nyt speksi ja tehtävä on ilmeisesti valittu niin, että toteutus on juuri Haskellilla mahdollisimman tyylikäs. Python-koodia kohdannut ongelma ei taida kuvastaa paradigmojen eroa vaan juuri tähän käyttötapaukseen sopivaa Haskell-kielen ominaisuutta. Yhtä hyvin voidaan sopia, että taustafunktiot hyväksyvät myös listan, jolloin Python-versio olisi myös kunnossa, tai Pythonissa voidaan tehdä listoja varten apufunktiot, joihin tuo merkityksetön ero jää piiloon, tai voidaan erikseen käyttää tuollaista concatMap-tyylistä funktiota – ai mutta sehän on varmaan taas fp-ominaisuus, vaikka sen toteuttaisi for-silmukalla.
Edellisessä yritin vain saada koodin näyttämään mahdollisimman samalta, mutta jos pidetään alkuperäinen speksisi ja siirrytään kielelle ominaiseen toteutustapaan, Pythonilla tulee edelleen yhtä lyhyt ja ilmaisuvoimainen ratkaisu. Minusta tämä Pythonin for-rakenne on edullinen myös luettavuuden kannalta: siitä näkee heti, että käsitellään useita arvoja, ja ei tule yllätyksiä aikavaativuuden suhteen.
def haeKaupungit(esiintyjä): return set(kaupunki for paja in etsiTyöpajat(esiintyjä) for tapahtuma in etsiKoulutustapahtumat(paja) for kaupunki in etsiKaupungit(tapahtuma))
Ehdotin jo aiemmin tuollaisen pienen merkkijonoalgoritmin toteutusta. Siinä tehtävänanto kuvaa ongelmaa eikä funktioita ja siten ei ole riippuvainen kielestä. Mutta onko se Selma-koiralle liian kova luu? Kyseessä on kuitenkin vain koululaisten kisan alkukilpailun tehtävä, joten ei siitä pitäisi olla opettajalle mitään vaivaa. Saataisiin reilumpi vertailu, kun lähtökohta ei olisi Haskell-koodarin Haskellille suunnittelema Haskell-tehtävä.
Metabolix, tuossa Haskell-koodissa saadaan suurempi määrä logiikkaa pienempään merkkimäärään - se on objektiivisesti ilmaisuvoimaisempi.
Se on jotain, mitä voimme mitata. Mielestäni Haskell on myös subjektiivisesti ilmaisuvoimaisempi, varsinkin tiedonkäsittelytilanteissa, joihin koiramaisessa ohjelmointikirjassa viitataan.
For ja in jäävät tuohon Python-koodiin, koska imperatiivisella kielellä rakenne on siellä oltava.
Samasta syystä Koiramaisen ohjelmointikirjan alussa se for-luuppi siellä oli ja sitä vertailtiin ilmaisuvoimaisempaan tapaan saada aikaan sama juttu.
metabolix kirjoitti:
”On varmaan parempi osaltani lopettaa tämä keskustelu, kun nähtävästi Selma-koiran tavoitteena ei ole edes esittää perusteluja vaan lähinnä herättää lisää keskustelua. Ehkä taustalla on jokin niin vahva usko Haskelliin, että riittävällä jankkaamisella muutkin väistämättä löytävät saman totuuden. Palataan asiaan sitten, jos saadaan jokin konkreettinen esimerkki ongelmasta, jossa funktionaalinen kieli tuottaa selvän koodin ja imperatiivinen kieli ei taivu yhtä selvään koodiin. Sehän se alkuperäinen väite oli, kun oli vastakkain map-funktio ja jokin umpisurkeasti väsätty C-for-silmukka.”
Kirjassa C-tyyppistä silmukkaa esitellään juuri siitä syystä, että saadaan ne muuttujat näkyviksi. Vertailussa oli ideana paradigmojen vertailu ja on erinomaisen tärkeä tehdä havainto, että yhdessä paradigmassa on jokin käsite, jota toisessa ei ole.
Ne muuttujat, kuten ne imperatiivisessa paradigmassa ymmärretään, ovat siinä Pythonin for in -rakenteessakin, sama pätee tuohon C++ esimerkkiin. Kirjassani esittämäni esimerkit toimivat erinomaisen hyvin demonstroimaan eroa; Python esimerkissä olisi ollut se ongelma, että muuttuja ei näy, vaikka se siellä onkin.
Tässä Glasgown yliopiston porukka tekee samantyyppistä vertailua vähän eri kielillä tosin: https://www.futurelearn.com/info/courses/
Videoon liittyvät lähdekoodit löytyvät täältä: https://github.com/wimvanderbauwhede/
Kannustan jokaista vertailemaan koodeja ja miettimään, onko hommassa itua vai ei. Niin Graham Hutton kuin Wim Vanderbauwhedekin ovat huippuammattilaisia, ja näitä me Selma-koiran kanssa peesataan. Wimillä on tutkijanuransa lisäksi lisäksi vahva tausta teollisuudessa.
Tässä Graham Hutton Glasgown yliopistosta näyttää tekevän samantyyppisen vertailun 2021 luentonsa aluksi, kuin tehdään tuossa koirakirjassakin. Huttonin luennot ovat erinomaisia, jos englanti taipuu. Kannattaa katsoa loputkin!
Eräs viime kursimme opiskelijoista oli opiskellut tutkinnon samaisessa koulussa ja kovasti kehui Grahamia opettajana.
https://www.youtube.com/watch?v=rIprO6zoujM&list=PLF1Z-APd9zK7usPMx3LGMZEHrECUGodd3&index=3
Jos haluaa tutkia ihan alkeita ja kokeilla juttuja suomeksi, nämä Selma-koira -jutut voivat toimia: https://www.youtube.com/watch?v=c21iRXdX-Q0
Hienoa työtä teeman parissa on tehnyt opettaja Tiina Partanen, jonka juttuja löytyy täältä: https://racket.koodiaapinen.fi/racket.html
Ja Tiinan videoita on mm. täällä: https://www.youtube.com/watch?v=kTnaSLRsDBM&list=PLear-l84FmuwKe-1cOI0mxKSHwDGNLBJM
Kannustan rohkeasti lähtemään tutkimusmatkalle funktionaaliseen ohjelmointiin!
Kaikkien mielipiteitä kunnioittaen, itsenäiseen ajatteluun sekä tutkimuksiin kannustaen,
Selma-koira ja kumppanit
Selma-koira kirjoitti:
Suurempimäärä logiikkaa pienempään merkkimäärään - se on objektiivisesti ilmaisuvoimaisempi.
Jos näin on, koodi kannattaisi ajaa jonkin pakkaavan obfuskaattorin läpi, jotta ilmaisuvoima paranee. Kannattaisi myös jättää pois ylimääräiset funktiot ja laittaa kaikki samaan, jotta säästyy ihan turhia rivejä. Jos lyhyet muuttujat loppuvat kesken, voi jatkaa emojeilla. Tavukoodi on vielä voimakkaampaa.
Tai mitä jos määrittelen kielen, jossa A tarkoittaa ensimmäistä tekemääsi koodia, B toista, C kolmatta ja niin edelleen. Objektiivisen määritelmäsi mukaan tämä kieli on ylivoimaisesti ilmaisuvoimaisempi kuin mikään, mitä pystyt ikinä kirjoittamaan.
Selma-koira kirjoitti:
Kirjassa C-tyyppistä silmukkaa esitellään juuri siitä syystä, että saadaan ne muuttujat näkyviksi. Vertailussa oli ideana paradigmojen vertailu ja on erinomaisen tärkeä tehdä havainto, että yhdessä paradigmassa on jokin käsite, jota toisessa ei ole.
Vertailussa pitäisi tuoda esiin hyvät ja huonot puolet ja tutkia näiden merkitystä ja eri mahdollisuuksia. Kirjasi ja viestisi käsittelevät imperatiivisesta ohjelmoinnista pääasiassa kokemiasi huonoja puolia ja funktionaalisesta vain hyviä. Vaikka kirjan alussa hieman kuvailet imperatiivista ohjelmointia, teksti ei vaikuta minusta vertailulta vaan irralliselta kuvaukselta: ”Kissa näyttää tältä ja siinä on näitä ongelmia. Koira näyttää tältä, ja seuraava kirjanen kertoo kaikki koiran hyvät puolet.” Tässä jälkikeskustelussa tärkein todistettava asia näyttää olevan, että missään tapauksessa näillä kahdella ei ole mitään yhteistä, paitsi jos se on plagioitu sinun lempparistasi.
Erityisesti nostat imperatiivisen ohjelmoinnin ongelmaksi muuttujat ja koodirivit ja kronologisen ajattelun. Esität asian jotenkin niin, kuin ajatus väistämättä joutuisi noille raiteille. Kuten keskustelussa on tuotu ilmi, monilla ajattelu ei ole noin jumiutunut ja argumenttisi tuntuu aivan vieraalta.
Kirjoitat imperatiivisen ohjelmoinnin vertauksesta ruokareseptiin, kuin tämä olisi ainoa (ja ongelmallinen) tapa hahmottaa imperatiivista koodia – ja aivan kuin oma koodisi olisi täysin erilaista. Silti omat Haskell-koodisi toimivat tällä reseptiperiaatteella, selvänä esimerkkinä vaikka tuo edellä oleva koodi, jossa (koodin järjestyksen näkökulmasta) ensin haetaan työpajat, sitten haetaan tapahtumat ja viimeiseksi kaupungit. Ajallinen järjestys on väistämätön, koska ei voi hakea tapahtuman kaupunkeja, jos tapahtumaa ei ole haettu. Jos perustelusi onkin se, että laiskan evaluaation vuoksi Haskell-ohjelmassa ei tarvitse hakea kaikkia tietoja kerralla, niin sama tilanne kyllä saadaan aikaan myös Pythonin generaattorilausekkeilla tai generaattorifunktiolla.
Selma-koira kirjoitti:
Python esimerkissä olisi ollut se ongelma, että muuttuja ei näy, vaikka se siellä onkin.
Kun muuttujien miettiminen on mielestäsi niin iso ongelma, niin tuon kommenttisi mukaan Pythonissa asia olisi paremmin. Asenteellisuus korostuu, kun nyt tämä parannus onkin "ongelma" ja Python ei kelpaa vertailuun.
Pythonillakin voi kirjoittaa niin, että muuttujat näkyvät, ja tämä olisi selvempää kuin C++. Miksi et hyväksy tätä vertailukohtaa esimerkkeihisi?
Koskapa Selma-koira ei vieläkään tarttunut haasteeseen, tein sen kokeeksi itse. Kuulimme äsken, että lyhyempi koodi on objektiivisesti ilmaisuvoimaisempi, joten tiivistin ja samalla hidastin vanhaa Python-ratkaisuani. Tulos olikin yllättävän tiivis! Haskellilla en päässyt ihan yhtä lyhyeen, mutta se varmaan johtuu vain heikoista Haskell-taidoistani. Ehkä Selma-koira tai jlaire pystyy parantamaan koodia, koska eihän ole mahdollista, että Haskell-koodi jäisi pidemmäksi.
Python:
sanat = ["AAB", "ABKA", "SSG", "TSGT", "ZZZZ", "KEAK"] harmonisoidut = [[sana.index(m) for m in sana] for sana in sanat] print(sum(harmonisoidut.count(h) - 1 for h in harmonisoidut) // 2)
Haskell:
sanat = ["AAB", "ABKA", "SSG", "TSGT", "ZZZZ", "KEAK"] indeksi (alku:loput) m = if alku == m then 0 else 1 + indeksi loput m harmonisoi sana = map (indeksi sana) sana harmonisoidut = map (harmonisoi) sanat laske lista sana = length . filter (== sana) $ lista tulos = sum (map (\ s -> laske harmonisoidut s - 1) harmonisoidut) `div` 2 main = putStrLn (show tulos)
Haskell on menneen talven lumia, vaihdoin jo kauan sitten objektiivisesti ilmaisuvoimaisempiin APL-suvun kieliin.
Tässä on J-ratkaisu:
+/ (*<: % 2:) #/.~ i.~&.> ;: 'AAB ABKA SSG TSGT ZZZZ KEAK' 4
Selma-koira voisi viimeinkin näyttää miten asia hoidetaan Haskellilla, niin päästään laskemaan kummassa on enemmän merkkejä.
Aihe on jo aika vanha, joten et voi enää vastata siihen.