Funktioiden osittainen soveltaminen PHP:ssa
Funktionaalisessa ohjelmoinnissa on paljon käsitteitä, jotka tuntuvat hieman vieraalta imperatiiviseen ja oliopohjaiseen ohjelmointiin tottuneelle koodarille. Yksi tällaisista on funktioiden osittainen soveltaminen, eli partial application.
Osittaisella soveltamisella tarkoitetaan sitä, että funktiota voidaan kutsua vajailla argumenteilla niin, että tuloksena palautuu funktio, jolle voidaan välittää loput argumentit. Esimerkiksi Haskellissa tämä on ainoa tapa kutsua funktioita, sillä Haskell käyttää kaikissa funktiokutsuissa ns. curry-muunnosta (eng. currying), jolla funktiokutsut muunnetaan ketjuksi yhden argumentin funktiokutsuja. Tämä näkyy myös Haskellin funktioiden tyyppikuvauksissa:
max :: Ord a => a -> a -> a
Tyyppikuvaus kertoo, että max
-funktio ottaa vastaan kaksi Ord
-tyyppiluokan argumenttia ja palauttaa tulokseksi samaa tyyppiä olevan arvon. Yhden parametrin funktioilla tyyppikuvaus on vastaavasti seuraava:
id :: a -> a
Jo pelkästään näiden tyyppikuvausten perusteella on pääteltävissä, että kahden argumentin funktiokutsu on oikeasti kaksi yhden argumentin funktiokutsua ketjutettuna. Jos tällaista funktiota kutsutaan vain yhdellä argumentilla, tulee tulokseksi osittain sovellettu funktio, joka ottaa vastaan toisen argumentin:
max 2 :: (Ord a, Num a) => a -> a
On suurelta osin kehittäjäyhteisön kiinnostuksesta kiinni, mihin suuntaan kieltä kehitetään, ja funktionaalisen ohjelmointiparadigman tuen parantaminen on aivan hyvin mahdollista ilman, että imperatiivinen ja oliopohjainen ohjelmointi kielellä kärsivät.
Mitä hyötyä osittaisesta soveltamisesta on?
Osittaisesta soveltamisesta on hyötyä etenkin kahdella osa-alueella:
- Tilanhallinnassa
- Funktioiden geneerisyydessä
Funktionaalisessa ohjelmoinnissa pyritään pitämään tila muuttumattomana ja käyttämään puhtaita funktioita. Puhtailla funktioilla on kaksi vaatimusta:
- Funktio palauttaa aina saman tuloksen samoilla argumenteilla
- Funktiolla ei ole sivuvaikutuksia, eli se ei käytä eikä muuta järjestelmän tilaa suorituksen aikana
Koska puhdas funktio ei käytä ulkopuolista tilaa ollenkaan hyväkseen, on funktiolle pystyttävä välittämään relevantit tiedot argumentteina. Funktion osittainen soveltaminen auttaa tässä siten, että sillä luodaan konteksti, jossa funktion suorittaminen lopullisella kutsuhetkellä tapahtuu. Se siis tarjoaa varsin hyvän tavan välittää järjestelmän tila funktiolle ilman sivuvaikutuksia.
Kun funktion toteutus ei ole riippuvainen ulkopuolisesta tilasta, funktio on hyvin yleiskäyttöinen. Tällaiset geneeriset funktiot on helppo yhdistellä isommiksi kokonaisuuksiksi ja ne näin parantavat koodin uudelleenkäytettävyyttä. Väittäisin, että tämä on paras tapa tuottaa geneeristä koodia, sillä yleensä (etenkin PHP:n tapauksessa) pyrkimys geneerisyyteen tuottaa ylisuunniteltuja ja tarpeettoman monimutkaisia toteutuksia.
Näiden lisäksi järjestelmän tilan välittäminen funktiolle argumentteina tuo käytetyn tilan näkyväksi koodissa. Kun tiedetään, että funktio tarvitsee toimiakseen vain argumentteina saamansa tiedot, on koodin kanssa toimiminen paljon helpompaa ja nopeampaa. Tämä myös tekee funktioiden testaamisesta helppoa, koska tilaa voidaan mallintaa helposti ja vain tarvittavilta osin.
Miten osittaista soveltamista voidaan käyttää PHP:n kanssa?
PHP ei tue osittain sovellettuja funktioita – ainakaan vielä. Tämän vuoden puolella on tuotettu RFC, jossa on ehdotus osittain sovellettujen funktioiden tuen lisäämisestä PHP:hen. En kuitenkaan usko, että tuota tukea on tulossa PHP:hen aivan lähiaikoina, joten meidän on toistaiseksi tultava toimeen ilman. Varsinaisen tuen puuttuminen ei kuitenkaan tarkoita, etteikö melko pitkälti vastaavanlaista toiminnallisuutta olisi silti mahdollista käyttää.
PHP 7:lle löytyy varsin kattava osittaisen sovelluksen kirjasto, joka tukee mm. placeholder-argumentteja. Tuo on varsin hyödyllinen ominaisuus, sillä PHP:ssa funktioiden parametrien järjestys usein vaihtelee näennäisen satunnaisesti (on siinä oikeasti kai joku logiikkakin). Placeholder-argumentit sallivat parametrien osittaisen soveltamisen vapaassa järjestyksessä. Ongelmana tässä kirjastossa kuitenkin näen sen liiallisen kompleksisuuden, sillä periaatteessa tärkeimmät osat toiminnallisuudesta vaativat ohjelmointikieleltä vain tavan käsitellä lambda-funktioita ja sulkeumia (eng. closure).
Lambda-funktiot ovat anonyymejä funktioita ja niitä voidaan käsitellä kuin minkä tahansa tietotyypin arvoja. Sulkeumat puolestaan ovat lambda-funktioita, jotka kapseloivat sisäänsä ulkopuolelta tulevaa tilaa. PHP:ssa use
-avainsanan avulla voidaan välittää tietoa sulkeuman ulkopuolelta sisäpuolelle. Lambdat ja sulkeumat ovat olleet mukana PHP:ssa jo versiosta 5.3 asti ja tuki on viime vuosina parantunut entisestään.
Tässä vaiheessa voisi olla hyvä ottaa pieni esimerkki sulkeuman käyttämisestä funktion osittaista soveltamista emuloivan toteutuksen tekemiseen. Ajatellaan, että olisi tarpeen rakentaa taulukko tietynlaisia merkkijonoja yhden lukutaulukon perusteella. Toteutus voisi näyttää tältä:
$numbers = [12, 2, 34];
$strings = [];
foreach ($numbers as $number) {
$strings[] = str_pad('uid-' . $number, 14, '_');
}
Koska esimerkki on hyvin yksinkertainen, on toteutuskin varsin suoraviivainen. Toteutuksessa on kuitenkin huomattavissa seuraavat ongelmat:
-
$strings
-taulukkoa käytetään ”globaalina” tilana, joka muuttuu suorituksen edetessä. -
foreach
-silmukka ei varsinaisesti kuvaa sitä, mitä tehdään. Pintapuolisesti tarkasteltuna se kertoo vain, että$numbers
-taulukko iteroidaan läpi. - Silmukan sisällä tehtävä toiminto sisältää osan käsiteltävästä tilasta, eli merkkijonon etuliitteen, lopullisen merkkijonon pituuden sekä merkkijonon täytemerkin. Tila on siis hajautettu kahteen paikkaan.
Miltä toteutus näyttäisi sulkeuman kanssa toteutettuna ja auttaisiko se pääsemään eroon edellisen toteutuksen ongelmista? Katsotaanpa:
$numbers = [12, 2, 34];
$strings = array_map(str_builder('uid-', 14, '_'), $numbers);
function str_builder($prefix, $length, $pad) {
return function($number) use ($prefix, $length, $pad) {
return str_pad($prefix . $number, $length, $pad);
};
}
Yhh… No, eipä tuo oikeastaan näytä irrallisena ainakaan yksinkertaisemmalta alkuperäiseen nähden. Vaan onko toteutuksessa silti jotakin hyvääkin? No, onhan siinä:
- Kaikkea tilaa käsitellään yhdessä paikassa.
- Toteutus ja tila on eriytetty toisistaan.
-
array_map
kuvastaa hyvin sitä, mitä halutaan tehdä. -
str_builder
-funktio on varsin geneerinen ja sitä voidaan periaatteessa käyttää muuallakin kuin vain tässä käyttötapauksessa.
Suurimpana ongelmana sulkeumatoteutuksessa on se, että str_builder
-funktio on melko vaikealukuinen siihen nähden, miten yksinkertainen funktio se lopulta on. Ongelma juontaa juurensa PHP:n syntaksista, joten sille ei ole suoraan oikein mitään taikatemppua tehtävissä. Ongelmaa voidaan kuitenkin jossain määrin helpottaa hyvin yksinkertaisella oliopohjaisella toteutuksella.
Osittainen soveltaminen PHP:ssa hiukan helpommin
PHP:ssa on paljon outouksia. Yksi outouskategoria on olioiden maagiset metodit, jotka tuovat olioille muutamia hiukan erikoisempia ominaisuuksia. Maagisista metodeista mielestäni yksi hämmentävimmistä on __invoke()
, joka mahdollistaa olioiden kutsumisen funktioina. Jep.
Koska __invoke()
-metodi on kaikesta perverssiydestään huolimatta olemassa ja toimii kuten lupaa, sitä voi käyttää osittain sovelletun funktioluokan rakennusaineena. Jos unohdetaan placeholder-argumentit, saadaan __invoke()
-metodin avulla tehtyä erittäin yksinkertainen toteutus osittain sovelletusta funktiosta:
class PartiallyAppliedFn {
protected $fn;
protected $args;
public function __construct(callable $fn, ...$args) {
$this->fn = $fn;
$this->args = $args;
}
public function __invoke(...$args) {
return call_user_func_array($this->fn, array_merge($this->args, $args));
}
}
Luokassa ajatuksena on, että instansioinnin yhteydessä luodaan alustava taulukko osittain sovellettavalle funktiolle välitettävistä argumenteista. Kutsuvaiheessa argumenttitaulukkoon lisätään kutsun yhteydessä annetut argumentit ja kutsutaan funktiota näillä kaikilla argumenteilla. Luokan avulla esimerkkitapaus voitaisiin toteuttaa seuraavasti:
$numbers = [12, 2, 34];
$str_builder = new PartiallyAppliedFn(build_string, 'uid-', 14, '_');
$strings = array_map($str_builder, $numbers);
function build_string($prefix, $length, $pad, $number) {
return str_pad($prefix . $number, $length, $pad);
}
Hmm… Tämä näyttää nyt jo paljon paremmalta! Koodissa on saatu säilytettyä kaikki sulkeumatoteutuksen hyvät puolet, mutta koodi on nyt myös kauttaaltaan helppolukuista. Ylläpidettäviä koodirivejä on tässä toteutuksessa rutkasti enemmän kuin alkuperäisessä foreach
-silmukkatoteutuksessa, mutta koodia on huomattavasti mukavampi ylläpitää ja sen testaaminen on todella paljon helpompaa.
Kannattaako tätä oikeasti koittaa käyttää PHP:n kanssa?
Mielestäni on lähtökohtaisesti huono idea koittaa väkisin taivuttaa ohjelmointikieltä sellaiseen, mihin se ei oikeasti sovellu. PHP on kuitenkin jossain määrin erikoistapaus: siinä on syyliä ja paiseita vaikka muille jakaa ja sen kehityskaari muistuttaa lähinnä vuoristorataa. PHP on tästä kaikesta huolimatta minulle rakas kieli ja toivon sille pelkkää hyvää. PHP:n 7-versio oli myös erittäin hieno julkaisu ja oli mahtavaa nähdä, miten suuren harppauksen ohjelmointikieli otti sen myötä.
Myös PHP:n tulevaisuus vaikuttaa valoisalta: PHP 7:n myötä uudet kehittäjäryhmät ovat löytäneet kielen ja pitkästä aikaa PHP:n ympärillä käydään positiivissävytteistä keskustelua. PHP:n kuolemaa on povattu vuosikaudet, mutta mielestäni PHP on tällä hetkellä enemmän elossa kuin koskaan.
Koska PHP tarjoaa mahdollisuuden käyttää funktioita kielen kannalta epäkonventionaalisilla tavoilla, kannattaisi minusta näistä mahdollisuuksista ottaa hyöty irti. On suurelta osin kehittäjäyhteisön kiinnostuksesta kiinni, mihin suuntaan kieltä kehitetään, ja funktionaalisen ohjelmointiparadigman tuen parantaminen on aivan hyvin mahdollista ilman, että imperatiivinen ja oliopohjainen ohjelmointi kielellä kärsivät.
Tässä kirjoituksessa esitellyn esimerkin kaltaisessa toteutuksessa osittaisesta soveltamisesta ei välttämättä saada vielä kauheasti hyötyä irti. Isommissa, monimutkaisemmissa toteutuksissa hyödyt kuitenkin saattavat olla merkittäviä. Mikäli funktioiden osittainen soveltaminen PHP:ssa tekee koodista helpommin ymmärrettävää ja parantaa koodin ylläpidettävyyttä ja testattavuutta, olisi mielestäni hölmöä olla käyttämättä sitä.