piątek, 17 lutego 2012

Funkcje i metody

Język Scala posiada możliwość zdefiniowania samodzielnych funkcji, tak jak to ma miejsce w C++, a niemożliwe jest w Javie, gdzie musimy posiłkować się metodami statycznymi. Jest wiele sposobów na zdefiniowanie funkcji lub wartości funkcjopodobnych. Poniższe przykłady zaczerpnięte zostały zaczerpnięte ze strony Matta Mighta. Przyjrzyjmy się im.

Funkcja podstawowa

Do jej deklaracji używa się słowa kluczowego def. Za znakiem '=' może znajdować jednoliniowa lub, ujęta w nawiasy klamrowe {}, wieloliniowa definicja. Jawne użycie słowa kluczowego return nie jest konieczne, bo zwracana jest zawsze wartość ostatniego wyrażenia w funkcji. Dlatego w poniższym przykładzie funkcja id zwraca wartość parametru x:

def id(x : Int) : Int = x

println(id(3))
// Wypisuje:
// 3

def plus1(x : Int) : Int = {
  x + 1
}

println(plus1(2))
// Wypisuje:
// 3

Funkcje bezparametrowe

Funkcje bezparametrowe można wywoływać z pominięciem nawiasów okrągłych:

def three() = 1 + 2

println(three())
println(three)
// Oba wywołania wypisują:
// 3
// 3
Dodatkowo w powyższym przykładzie widać, że nie jest potrzebne jawne deklarowanie zwracanego typu, ponieważ kompilator sam się go "domyśli" (dotyczy to również funkcji z parametrami).

Funckja anonimowa

val anonId = (x : Int) => x

println(anonId(3))
// Wypisuje:
// 3
W powyższym przykładzie wartość anonId posiada wywiedziony (ang. implicit) typ funkcyjny (Int) => Int = <function1>.

Metoda apply

Obiekty lub klasy z metodą apply mogą udawać funkcje:

object Id {
  def apply(x : Int) = x
}

// f(x) => f.apply(x)

println(Id.apply(3))
println(Id(3))
// Wypisują:
// 3
// 3

class Identity {
  def apply(x : Int) = x
}
val myId = new Identity
println(myId.apply(3))
println(myId(3))
// Wypisują:
// 3
// 3

// anonimowa klasa z metodą apply:
val myOtherId = new {
  def apply(x : Int) = x
}
println(myOtherId(3))
// Wypisuje:
// 3
Właściwie w specyfikacji języka Scala jest to zdefiniowane na odwrót: to funkcje są skrótem myślowym dla obiektów ze zdefiniowaną metodą apply. Przykładowa obiektowa definicja funkcji może wyglądać tak:
object funId {
  def apply(x : Int) = x
  override def toString() = ""
}
Czyli, jak to już zostało stwierdzone wcześniej, w Scali wszystko jest obiektem i dotyczy to również funkcji.

Funkcje wieloargumentowe

Sprawa jest prosta: argumenty oddziela się przecinkami - tak samo jak w Javie czy C++.

def h(x : Int, y : Int) : Int = x + y
Wywołanie funkcji z jawnie podanymi niektórymi parametrami, a ich resztą zastąpioną znakiem '_' zwraca nową funkcję z pomniejszoną listą parametrów. Jej kod zmienia się w stosunku do pierwowzoru o tyle, że w jej wnętrzu parametry podane jawnie zostają zastąpione ich wartościami podanymi w wywołaniu.
val h3 = h(3, _)

println(h3(4))
// Wypisuje:
// 7

Ale Scala ma jeszcze w zanadrzu funkcje rozwijane (ang. Curried)

def hC (x : Int) (y : Int) : Int = x + y

// źle: hC 3 4
// dobrze: hC (3) (4)

// źle: hC (3)
// dobrze: hC (3) _

// źle: hC _ (4)
// dobrze: hC (_:Int) (4)

val plus3 = hC (_:Int) (3)
val plus_3 = hC (3) _

println(plus3(10))
// Wypisuje:
// 13
Powyżej widać przykłady prawidłowego użycia takiej funkcji oraz możliwe złe wywołania.
Funkcje rozwijane mają tę cechę, że kolejne zestawy parametrów mogą być aplikowane etapami. Dzięki temu możliwe są złożone konstrukcje tworzące nowe języki domenowe (ang. Domain Specific Language, DSL)
 def whileLoop(cond: => Boolean)(body: => Unit): Unit =
  if (cond) {
      body
      whileLoop(cond)(body)
  }
  var i = 10
  whileLoop (i > 0) {
    println(i)
    i -= 1
  }
Funkcja whileLoop posiada dwa argumenty: cond i body. W momencie zastosowania tej funkcji właściwe parametry nie są obliczane aż do momentu ich użycia w ciele pętli (linie 8 i 9). Dzięki temu powstaje podobna do javowej pętla while z rekurencyją implementacją.

Procedury

Procedura to funkcja zwracjąca domyślnie wartość typu Unit, który jest odpowiednikiem void w Javie i C++:

def proc(a : Int) {
  println("Jestem procedurą!")
}

proc(10)
// Wypisuje:
// Jestem procedurą!
Warto zapamiętać, że skrótowym zapisem wartości typu Unit jest '()'.

Funkcja bezargumentowa

def argless : Unit = println("zawołano bezargumentową")

argless
argless
// wypisuje:
// zawołano bezargumentową
// zawołano bezargumentową

Leniwie inicjowane pola

Pola typu lazy są bezargumentowymi funkcjami, które agregują swój wynik:
class LazyClass {
  lazy val x = { println("Obliczam x") ; 3 }
}

val lc = new LazyClass
println(lc.x)
println(lc.x)
println(lc.x)
// Wypisuje:
// Obliczam x
// 3
// 3
// 3
Oznacza to, że tylko pierwsze odwołanie się do pola x powoduje wywołanie bloku inicjującego. Pola typu lazy mają opóźnioną inicjalizację do momentu pierwszego odwołania się do takiego pola i to jest podstawowym zastosowaniem tego słowa kluczowego. Używanie tego jako funkcji jest używaniem efektów ubocznych i nie jest zalecane.

Metody

Jak można wywnioskować z powyższych przykładów deklaracje metod nie różnią się właściwie od deklaracji funkcji, poza tym, że zdefiniowane są w ciele klasy lub obiektu i mają dostęp do pól prywatnych swojego właściciela. Ponieważ temat klas i obiektów jest dużo szerszy i wykracza poza temat tego artykułu, zajmę się nim w osobnych wpisach.

Brak komentarzy: