12 Ehtorakenteet

Funktiot kappaleen funktiot suorittavat aina samat komennot riippumatta syötteestä. Entä jos funktion toiminnassa pitäisi ottaa huomioon erilaisia tapauksia, eli suorittaa tiettyjä komentoja vain joissain tilanteissa? Tätä varten ohjelmointikielissä on ehtorakenteita, eli ns. if/else-rakenteita, jotka ohjaavat ohjelman toimintaa.

Tutustutaan ensin tarkemmin loogisiin operaattoreihin.

12.1 Loogiset operaattorit

Tässä on lyhyt lista loogisista operaattoreista:

Operaattori Toiminto
< pienempi kuin
<= pienempi tai yhtä suuri kuin
> suurempi kuin
>= suurempi tai yhtä suuri kuin
== yhtä kuin
!= ei yhtä kuin
!a ei a (negaatio)
a | b a TAI b alkioittain
a || b a TAI b yksittäisille arvoille
a & b a JA b alkioittain
a && b a JA b yksittäisille arvoille
a %in% b mitkä a:n alkiot ovat myös b:n alkioita

Kaikki loogiset operaattorit palauttavat joko arvon TRUE, FALSE tai NA. Vertailuoperaattorien käyttö on jo tullut tutuksi aikaisemmissa kappaleissa, mutta tutustutaan vähän tarkemmin viimeisten rivien operaattoreihin.

12.1.0.1 Negaatio

Looginen negaatio palauttaa loogisen lauseen vastakohdan, eli muuttaa arvon TRUE arvoksi FALSE ja arvon FALSE arvoksi TRUE.

10 > 12
## [1] FALSE
!(10 > 12)
## [1] TRUE
# Also works without parentheses
!10 > 12
## [1] TRUE
!is.na(NA)
## [1] FALSE

12.1.0.2 Looginen TAI (disjunktio)

Loogiselle TAI operaattorille annetaan kaksi loogista lausetta, ja TAI operaattori palauttaa TRUE, jos vähintään toinen lauseista on TRUE. R:ssä TAI merkitään pystyviivalla | tai kahdella pystyviivalla ||. | käy läpi vektoreita alkioittain, || vertaa kahta loogista lausetta, ja toista lausetta ei edes ajeta, jos ensimmäinen on TRUE (koska || palauttaa TRUE riippumatta toisen lauseen arvosta). Jos tämä tuntui monimutkaiselta, niin riittää muistaa, että ehtorakenteissa kannattaa käyttää muotoa ||.

10 > 12 || "a" < "b"
## [1] TRUE
2 > 1 || 4 > 2
## [1] TRUE
"a" > "c" || 1 > 10
## [1] FALSE

12.1.0.3 Looginen JA (konjunktio)

Loogiselle JA operaattorille annetaan kaksi lausetta. JA palauttaa TRUE, jos kummatkin lauseet ovat TRUE. R:ssä JA-operaattorit ovat & ja &&, jotka käyttäytyvät kuten | ja ||.

10 > 12 && "a" < "b"
## [1] FALSE
2 > 1 && 4 > 2
## [1] TRUE
"a" > "c" && 1 > 10
## [1] FALSE

12.1.0.4 Osajoukko

%in%-operaattorilla voi tarkistaa, kuulvatko jotkin arvot johonkin joukkoon. Tämä voitaisiin toteuttaa myös usealla TAI-operaattorilla, mutta %in% on usein paljon kätevämpi.

dna_bases <- c("A", "C", "G", "T")
rna_bases <- c("A", "C", "G", "U")

"T" %in% dna_bases
## [1] TRUE
"T" %in% rna_bases
## [1] FALSE
# With negation
!"A" %in% dna_bases
## [1] FALSE

Operaattoria voi soveltaa myös vektoreihin, jolloin operaattori palauttaa loogisen vektorin, jonka alkio jokainen alkio kertoo, kuuluiko vastaava operaation vasemman puolen alkio operaation oikeaan puoleen.

dna_bases %in% rna_bases
## [1]  TRUE  TRUE  TRUE FALSE

12.1.0.5 Monimutkaisemmat lauseet

Operaattoreita voidaan myös yhdistellä monimutkaisemmiksi lauseiksi. Tällöin lauseiden evaluointijärjestys määritetään tarvittaessa suluilla.

dog <- list(breed = "golden retriever",
            height = 45,
            weight = 27)

dog$breed == "golden retriever" && dog$weight < 25 || dog$height < 50
## [1] TRUE

12.1.0.6 a < x < b

usein tulee vastaan tilanteita, joissa halutaan tarkistaa, onko jokin luku halutulla välillä. Tämä kirjoitetaan matemaatiisesti esim. näin: \(a < x < b\), jossa tarkastetaan, onko \(x\) välillä \((a, b)\). Tämä ei kuitenkaan valitettavasti toimi R:ssä, vaan tarkistus pitää jakaa kahteen osaan:

# Are x and y between 0 and 1?
x <- 3
y <- 0.3
0 <= x && x <= 1
## [1] FALSE
0 <= y && y <= 1
## [1] TRUE

12.2 Ehtorakenteet

Aloitetaan esimerkistä: tehtävänä on kirjoittaa funktio, jolle annetaan syötteenä potilaan hemoglobiiniarvo. Funktion on tarkoitus hälyttää, jos hemoglobiini laskee alle viitearvojen alarajan 117. Kyseinen funktio voisi näyttää vaikka tältä:

hb_alert <- function(hb) {
  if (hb < 117) {
    return("Hemoglobin is low!")
  }
}

Funktiolla on siis yksi argumentti, hb eli hemoglobiiniarvo. Funktion sisällä on if-rakenne. Rakenteessa on kaksi osaa: ehto, ja rakenteen sisäinen koodi. Rakenteen sisäinen koodi ajetaan vain, jos ehto täyttyy. Ehto merkitään if-komennon jälkeen sulkeisiin, ja rakenteen sisäinen koodi kirjoitetaan sulkeiden jälkeen aaltosulkeiden sisään. (Jos aaltosulkeiden sisään tulisi vain yksi rivi koodia, aaltoasulkeet voi jättää pois, mutta näissä esimerkeissä käytetään aina aaltoasulkeita).

Kokeillaan, miten funktio toimii eri hemoglobiiniarvoilla:

# Nothing happens
hb_alert(130)
# returns alert
hb_alert(110)
## [1] "Hemoglobin is low!"

Funktio siis toimii oletetusti, eli se hälyttää vain, jos hemoglobiinitaso on alle 117. Käyttäjän kannalta olisi kuitenkin kätevää saada jonkinlainen palaute myös silloin, kun hemoglobiinitaso on tarpeeksi korkea. Tätä varten voidaan käyttää else-komentoa:

hb_alert <- function(hb) {
  if (hb < 117) {
    return("Hemoglobin is low!")
  } else {
    return("Hemoglobin is OK")
  }
}

hb_alert(130)
## [1] "Hemoglobin is OK"

else-komennon jälkeinen koodi siis ajetaan, jos ehto hb < 117 ei täyty.

Tällä hetkellä funktiomme toimii oikein vain naispotilaille, sillä miehillä hemoglobiiniarvojen alaraja on 134. Lisätään siis funktioomme argumentti sex sukupuolta varten ja muokataan funktion toimintaa niin, että se osaa ottaa huomioon sukupuolen. Nyt if-rakenteen ehdosta tulee jo hieman monimutkaisempi:

hb_alert <- function(hb, sex) {
  if (sex == "female" && hb < 117 || sex == "male" && hb < 134) {
    return("Hemoglobin is low!")
  } else {
    return("Hemoglobin OK")
  }
}

hb_alert(hb = 120, sex = "female")
## [1] "Hemoglobin OK"
hb_alert(hb = 120, sex = "male")
## [1] "Hemoglobin is low!"

Entä jos haluaisimme tulostaa eri varoituksen mies- ja naispotilaille? Tähän tarvitaan else if-rakennetta:

hb_alert <- function(hb, sex) {
  if (sex == "female" && hb < 117) {
    return("Hemoglobin is low for a female!")
  } else if (sex == "male" && hb < 134) {
    return("Hemoglobin is low for a male!")
  } else {
    return("Hemoglobin OK")
  }
}

hb_alert(hb = 110, sex = "female")
## [1] "Hemoglobin is low for a female!"
hb_alert(hb = 120, sex = "male")
## [1] "Hemoglobin is low for a male!"

Nyt funktio tarkistaa ensin, onko potilas nainen ja onko hänen hemoglobiininsa alle 117. Jos ei, siirrytään eteenpäin ja tarkistetaan, onko potilas mies ja onko hänen hemoglobiininsa alle 130. Jos ei, siirrytään viimeiseen kohtaan, ja tulostetaan “Hemoglobin is OK”.

else if-rakenteita voi olla rajoittamaton määrä ensimmäisen if-rakenteen jälkeen. Lisätään funktioon hälytys kriittisestä hemoglobiinin määrästä (hb < 50) riippumatta sukupuolesta:

hb_alert <- function(hb, sex) {
  if (sex == "female" && hb < 117) {
    return("Hemoglobin is low for a female!")
  } else if (sex == "male" && hb < 134) {
    return("Hemoglobin is low for a male!")
  } else if (hb < 50) {
    return("Hemoglobin is critical")
  } else {
    return("Hemoglobin OK")
  }
}

hb_alert(hb = 32, sex = "female")
## [1] "Hemoglobin is low for a female!"

Kuten huomataan, yllä oleva koodi ei toimikaan, kuten piti. Näin alhaisella hemoglobiinilla pitäisi tulla varoitus kriittisestä tilasta. Koodi suoritus ei kuitenkaan ikinä etene kriittisen tilan varoitukseen asti, sillä ensimmäinen ehto täyttyy. Korjataan tilanne siirtämällä kriittisen tilan ehto ensimmäiseksi:

hb_alert <- function(hb, sex) {
  if (hb < 50) {
    return("Hemoglobin is critical")
  } else if (sex == "male" && hb < 134) {
    return("Hemoglobin is low for a male!")
  } else if (sex == "female" && hb < 117) {
    return("Hemoglobin is low for a female!")
  } else {
    return("Hemoglobin OK")
  }
}

hb_alert(hb = 32, sex = "female")
## [1] "Hemoglobin is critical"
hb_alert(hb = 120, sex = "female")
## [1] "Hemoglobin OK"
hb_alert(hb = 120, sex = "male")
## [1] "Hemoglobin is low for a male!"

Nyt funktio toimii haluamallamme tavalla!

Funktioissa voi myös olla useampi ehtorakenne. Ehtorakenteita käytetään usein tarkistamaan argumenttien arvoja. Lisätään ehtorakenteet argumenttien tarkistamiseksi:

hb_alert <- function(hb, sex) {
  # Hemoglobin should be numeric and positive
  if (!is.numeric(hb) || hb < 0) {
    return("Hemoglobin should be numeric and positive")
  }
  if (!sex %in% c("female", "male")) {
    return("This function can only deal with binary sex: female or male")
  }
  
  if (hb < 50) {
    return("Hemoglobin is critical")
  } else if (sex == "male" && hb < 134) {
    return("Hemoglobin is low for a male!")
  } else if (sex == "female" && hb < 117) {
    return("Hemoglobin is low for a female!")
  } else {
    return("Hemoglobin OK")
  }
}

hb_alert(hb = "120", sex = "female")
## [1] "Hemoglobin should be numeric and positive"
hb_alert(hb = 120, sex = "FEMALE")
## [1] "This function can only deal with binary sex: female or male"

12.3 Alkioiden poimiminen vektorista tietyn ehdon perusteella

Seuraava tilanne on melko tyypillinen: on käytävä läpi vektorin arvot, ja säilytettävä niistä ne, jotka täyttivät tietyn ehdon. Tätä ongelmaa voi lähestyä esimerkiksi seuraavalla tavalla:

  • Luo apufunktio, joka ottaa syötteeksi yhden arvon, ja tarkistaa täyttyykö ehto. Tämän funktion tulee palauttaa TRUE, jos ehto täyttyy ja FALSE, jos ehto ei täyty.
  • Käytä funktiota Vectorize, jolla voit muuttaa funktiosi vektoroiduksi funktioksi. Vektorointi tarkoittaa tässä yhteydessä sitä, että yhden alkion sijaan vektoroitua funktiota voidaankin kutsua vektoriargumentilla, ja jokaiselle argumentin alkiolle suoritetaan alkuperäisen funktion määrittelemä operaatio.
  • Käytä vektoroitua apufunktiota vektorin indeksointiin.

Tässä on esimerkki, jossa käydään läpi vektori DNA:n emäksiä, joista poimitaan vain sytosiinit ja guaniinit.

# Helper function
is_cg <- function(base) {
  if (base %in% c("C", "G")) {
    return(TRUE)
  } else {
    return(FALSE)
  }
}

# Vectorize
is_cg_vector <- Vectorize(is_cg)

# Main function
pick_cg <- function(bases) {
  only_cg <- bases[is_cg_vector(bases)]
  return(only_cg)
}

# NOTE: this only checks the first value of the vector
my_bases <- c("A", "C", "C", "T", "G", "T")
#is_cg(my_bases) # This produces error in 4.2.x

# This works as expected
is_cg_vector(my_bases)
##     A     C     C     T     G     T 
## FALSE  TRUE  TRUE FALSE  TRUE FALSE
# Pick only C and G
pick_cg(my_bases)
## [1] "C" "C" "G"