Metodit ja funktiot, osa 1

31.01.2022

Metodit ja funktiot, osa 1

Tässä osassa käymme aihetta läpi yleisesti. Seuraavassa osassa tarkastelemme asioita enemmän kooditasolla.

Kaikessa ohjelmoinnissa on lopulta kyse käyttäytymisestä (behavior), eli toiminnallisuuksista. Ilman niitä kaikki tietorakenteet, algoritmit ja mallit ovat täysin onttoja sekä elottomia.

Käyttäytyminen on kodifioitu funktioihin tai metodeihin: jotain tulee sisään ja jotain lähtee ulos.

Olio-ohjelmoinnissa (Object-oriented Programming, OOP) käyttäytyminen on sidottu objektin tai luokan metodeihin. Funktionaalisessa (Functional Programming, FP) ohjelmoinnissa käyttäytyminen on funktioissa.

Olio-ohjelmoinnissa toiminnallisuuksiin pääsee käsiksi vain objektin kautta. Temppu ei ole yleensä kovin monimutkainen mutta vaatii oman seremoniansa. Esimerkiksi: new OrderService().processOrder(order). Funktionaalisessa ohjelmoinnissa funktioihin pääsee käsiksi suoraan tai toisten funktioiden kautta: processOrder(order).

OOP:ssa yksi vaihtoehto on sijoittaa toiminnallisuus suoraan luokkiin (staattiset metodit) jolloin niihin pääsee käsiksi suoraan ilman että luokasta joutuu luomaan uuden instanssin. Tämä saattaa näyttää hiukan kielestä riippuen vaikka tältä: OrderService.processOrder(order)

Entä riippuvuudet? Esimerkki luokkamme OrderService tarvitsee todennäköisesti jotain riippuvuuksia (esim. tietokanta) jotta se olisi hyödyllinen. Modernissa olio-ohjelmoinnissa jossa luokat pyritään pitämään muuttumattomina (immutable) ja riippuvuudet välitetään tyypillisesti luokan konstruktorissa. Staattisissa metodeissa riippuvuudet täytyy välittää metodin argumenteissa. FP:ssä riippuvuudet täytyy myös välittää funktion argumenteissa.

Entä kytkentä (coupling) ja koheesio (cohesion)? Objektin metodit ovat luonnollisesti tiukasti kytketty objektiin. Ihanteellisesti kytkös olisi löyhä. Koheesio taas on kiinni sovellusohjelmoijasta kuinka hyvin hän on onnistunut sijoittamaan yhteenkuuluvat metodit omiin luokkiinsa. FP:ssä kytkös on minimaalinen ja koheesio riippuu samalla tavoin kuin OOP:ssa funktioiden ryhmittelystä.

Entä jos toimintoja pitää ryhtyä yhdistelemään ja koostamaan?

Mutta verrataan ensin tarkemmin metodeja funktioihin.

Semantiikkaa

FP:ssä funktiot ovat ensiluokkainen tietotyyppi eikä niiden käyttö eroa muista tietotyypeistä kuten vaikka primitiivi booleania tai jostakin omata tietotyypistä kuten BeerDeliveredEvent. Toisin sanoen funktioita voidaan esimerkiksi asettaa muuttujaan tai antaa argumenttina aivan kuten muitakin tietotyyppejä. Funktion tyyppi voisi olla esimerkiksi String -> Int (ottaa argumenttina String tyyppisen arvon ja palauttaa Int tyyppisen arvon).

Metodit ovat taas objektin omistamia nimettyjä jäsenfunktioita. Toisin kuin funktiot metodeilla ei ole varsinaisesti tyyppiä eikä niitä voi välittää eteenpäin vaan niitä kutsutaan aina nimellä olion kautta. Monissa moderneissa OOP kielissä on kuitenkin kyvykkyys luoda funktio-viite metodiin jolloin metodiin voi viitata kuten se olisi funktio.

Mutta muutoin funktiot ja metodit ovat hyvin samankaltaisia. Molemmat ottavat argumentteja ja palauttavat jotain sekä kielestä riippuen tukevat geneeristä ohjelmointia (generic programming).

Otetaan esimerkki. Mallinnettu toiminnallisuus on tilaustunnisteen validointi. Käytössä on Kotlin ohjelmointikieli.

class UnvalidatedOrderId(unvalidatedOrderId: String)
sealed interface ValidatedOrderId
class ValidOrderId(id: String) : ValidatedOrderId
class InvalidOrderId(invalidId: String): ValidatedOrderId
// OOP approach
interface OrderIdValidator {
fun validate(unvalidatedOrderId: UnvalidatedOrderId): ValidatedOrderId
}
// FP approach
fun interface ValidateOrderId : (UnvalidatedOrderId) -> ValidatedOrderId

OOP lähestymistavassa on havaittavissa pientä seremoniaa. Jotta validate voidaan määrittää, tarvitaan luokka, johon se sijoitetaan. FP lähestymistapa on taas melko minimalistinen tyyppimääritys, joka on hyvin lähellä aliasta (alias voitaisiin määrittää näin: typealias ValidateOrderId = (UnvalidatedOrderId) -> ValidatedOrderId).

Selkein silmiin pistävä ero on kuinka OOP lähestymisessä käytetään substantiiveja (OrderIdValidator) kun taas FP lähestymistavassa käytetään verbejä (ValidateOrderId).

Entä kuinka näitä voisi käyttää?

// OOP
fun oopDemo(
orderIdValidator: OrderIdValidator,
xs: List<UnvalidatedOrderId>,
): List<ValidatedOrderId> =
xs.map(orderIdValidator::validate)
// FP
val fpDemo: (
ValidateOrderId,
List<UnvalidatedOrderId>
) -> List<ValidatedOrderId> = { validateOrderId, xs ->
xs.map(validateOrderId)
}

Erovaisuudet ovat melko vähäiset ainakin Kotlinilla toteutettuna.

Koostaminen ja yhdistely

OOP:ssa toiminnallisuuksien yhdistely tapahtuu aina olion metodien kautta olioX.metodiA(olioB.metodiY()). Yksinkertaista ja toimivaa mutta hiukan kankeaa.

On hyvä huomata että vaikka olioita tai luokkia voidaan koostaa (composition) sekä periyttää (inheritance) niin tämä ei lopulta muuta asetelmaa ratkaisevasti metodien osalta.

FP:ssä yhdistely ja koostaminen voidaan tehdä joustavasti. Koska funktiot ovat ensiluokkainen tietotyyppi, on korkeamman kertaluvun funktiot (higher-order function) luonnollinen osa FP:ia. Menetelmiä joilla funktioita voi koostaa ja yhdistellä on muun muassa: currying, composition ja partial application.

Perspektiiviä

OOP:n etuihin lukeutuu ehkä sen rajoittuneisuus ja kankeus, niin hassulta kuin se saattaakin kuulostaa. Oliot ovat helppo ymmärtää, kuten vaikka luokka Player jolla on joukko metodeja kuten shoot tai move. Tai vaikka HttpServer jolla on metodit joilla käsitellä http pyyntöjä. Myös käytännöt ja menetelmät ovat hioutuneet vuosikymmenten saatossa kuinka OOP:ta tulisi tehdä. Juuri OOP:n helppo ymmärrettävyys saattaa selittää miksi se nousi hallitsevaksi paradigmaksi 90-luvulla.

Mutta OOP:ssa on omat haasteensa. Sen rajoittuneisuus on todennäköisesti ollut yksi syy suunnittelumallien syntymiseen (design pattern) joilla paradigman jämäkkyydestä johtuvia kankeuksia on pyritty ratkomaan.

Ehkä tärkein asia ymmärtää OOP:sta on juuri sen rajoittuneisuus joka myös vaikeuttaa sen sovellettavuutta joidenkin ongelmien ratkaisemiseen. Aiheesta löytyy useampia kirjoituksia kuten esim. The Rise and Fall of Object Oriented Programming.

Entä FP? FP on noussut uudelleen pinnalle ja monet perinteiset OOP -kielet ovat lisänneet tukea FP:lle. Mutta miksi FP ei ole noussut vallitsevaksi paradigmaksi?

FP:n haasteet saattavat liittyä pitkälti siihen yhdistettävään käsitteistöön (Functional Programming Jargon) joka taas kumpuaa FP:n teoreettisesta taustasta.

Hyvä esimerkki saattaisi olla letkautus:

A monad is just a monoid in the category of endofunctors, what’s the problem?

Vaikka unohtaisimme hetkeksi kaiken teoreettisen taustan niin verbien avulla ohjelmointi on yllättävän hankalaa monille, jotka ovat oppineet mallintamaan sovelluksia substantiivien kautta.

Joskus myös FP:ssä käytetyn kielen syntaksi poikkeaa huomattavasti yleisesti käytetyistä kielistä mikä saattaa tehdä niistä ehkä vaikeammin lähestyttäviä. Ainakin Lisp sekä Haskell saattaisi sukeutua tähän kategoriaan.

Vaikka ohjelmointikielet elävät uutta renessanssia ei vielä yksikään ohjelmointikieli ole onnistunut yhdistämään OOP:ta ja FP:tä sekä samalla nousemaan merkittäväksi ohjelmointikieleksi. Lähimmäksi on varmaan päässyt Scala mutta sekin suosio on jäänyt hiukan vaisuksi.

Hauskaa perspektiiviä OOP:n ja FP:n eroavaisuuksiin löytyy myös Scott Wlaschinin blogautuksesta Functional Programming Design Patterns ja sieltä löytyvistä linkeistä.

Lopuksi

Tarkastelimme molempia OOP sekä FP lähestymistapoja melko korkealta tasolta ja vertailimme kevyesti niiden ominaisuuksia, vahvuuksia sekä kipupisteitä.

OOP on edelleen melko hallitseva paradigma mutta FP on ryhtynyt haastamaan sitä.

Seuraavassa osassa tarkastelemme aihetta enemmän konkreettisen esimerkin avulla: kuinka Kotlin taipuu OOP sekä FP tyyleihin.