1 Scala + NetBeans AKA: Nowoczesne obiektowe języki programowania i środowiska programistyczne na przykładzie Scali i środowiska NetBeans (spotkanie 4)
2 For-Comprehensions Przypominają zapytania Ułatwiają stosowanie funkcji wyższego rzędu Przykład: z listy osób wybrać imiona osób mających ponad 20 lat persons filter (p => p.age > 20) map (p => p.name) for (p <- persons if p.age > 20) yield p.name
3 Składnia for ( s ) yield e s to lista generatorów, definicji oraz filtrów (otoczona () lub {) generatory: x <- e, gdzie wyrażenie e jest typu listowego definicje: val x = e filtry: if f, gdzie f to wyrażenie typu Boolean zawsze na początku musi być generator późniejsze generatory iterują szybciej
4 Kilka przykładów że jest prościej Dla danego n znaleźć wszystkie pary 1 <= j < i < n takie że i+j jest pierwsza Poprzednie rozwiązania w dwóch krokach (generujemy pary i,j a potem filtrujemy) List.range(2, n)//generuje wartości od 2 do n-1.map(i => List.range(1, i).map(x => (i, x))).foldright(list[(int, Int)]()) {(xs, ys) => xs ::: ys.filter(pair => isprime(pair._1 + pair._2)) List.range(2, n).flatmap(i => List.range(1, i).map(x => (i, x))).filter(pair => isprime(pair._1 + pair._2)) Z For-Comprehensions for { i <- List.range(2, n) j <- List.range(1, i) if isprime(i+j) yield (i, j) Gdzie def isprime(n: Int) = List.range(2, n) forall (x => n % x!= 0)
5 Kilka przykładów że jest prościej Iloczyn skalarny dwóch wektorów xs i ys Przykładowe rozwiązanie (xs zip ys).map(t => t._1 * t._2).foldleft(0){(x,y) => x + y Z For-Comprehensions sum(for ((x, y) <- xs zip ys) yield x * y)
6 Łamigłówki Problem n królowych Rekurencyjnie sprawdzamy kolejne ustawienia kolejnych królowych Rozwiązanie częściowe reprezentujemy jako listę gdzie ostatnia królowa jest na początku Generujemy pełne listy wszystkich rozwiązań częściowych o zadanej długości def queens(n: Int): List[List[Int]] = { def placequeenincol(col: Int): List[List[Int]] = if (col == 0) List(List()) else for { queens <- placequeens(col 1) row <- List.range(1, n + 1) if issafe(col, row, queens) yield row :: queens placequeens(n)
7 Zapytania Przykładowa baza danych case class Book(title: String, authors: List[String]) Przykładowe dane val books: List[Book] = List( Book("Structure and Interpretation of Computer Programs", List("Abelson, Harold", "Sussman, Gerald J.")), Book("Principles of Compiler Design", List("Aho, Alfred", "Ullman, Jeffrey")), Book("Programming in Modula-2", List("Wirth, Niklaus")), Book("Introduction to Functional Programming"), List("Bird, Richard")), Book("The Java Language Specification", List("Gosling, James", "Joy, Bill", "Steele, Guy", "Bracha, Gilad")))
8 Zapytania c.d. Tytuły wszystkich książek napisanych przez Ullmana for (b <- books; a <- b.authors if a startswith "Ullman") yield b.title Tytuły wszystkich książek, których tytuł zawiera napis Program for (b <- books if (b.title indexof "Program") >= 0) yield b.title Imiona wszystkich autorów co najmniej dwóch książek for (b1 <- books; b2 <- books if b1!= b2; a1 <- b1.authors; a2 <- b2.authors if a1 == a2) yield a1 Z pominięciem duplikatów def removeduplicates[a](xs: List[A]): List[A] = if (xs.isempty) xs else xs.head :: removeduplicates(xs.tail filter (x => x!= xs.head)) lub xs.head :: removeduplicates(for (x <- xs.tail if x!= xs.head) yield x)
9 Foc-Comprehensions przy pomocy innych funkcji For-Comprehensions można przetłumaczyć na map, flatmap, filter for (x <- e) yield e' e.map(x => e') for (x <- e if f; s) yield e for (x <- e.filter(x => f); s) yield e for (x <- e; y <- e ; s) yield e e.flatmap(x => for (y <- e ; s) yield e )
10 Przykład tłumaczenia for { i <- range(2, n) j <- range(1, i) if isprime(i+j) yield (i, j) for (x <- e; y <- e ; s) yield e e.flatmap(x => for (y <- e ; s) yield e ) range(2, n).flatmap( i => for {j <- range(1, i) if isprime(i+j) yield (i, j) range(2, n).flatmap(i => for {j <- range(1,i).filter(j=>isprime(i+j) yield (i,j) for (x <- e if f; s) yield e for (x <- e.filter(x => f); s) yield e for (x <- e) yield e' e.map(x => e') range(2,n).flatmap( i => range(1,i).filter( j => isprime(i+j).map(j => (i,j) ) )
11 Tłumaczenie w drugą stronę object Demo { def map[a, B](xs: List[A], f: A => B): List[B] = for (x <- xs) yield f(x) def flatmap[a, B](xs: List[A], f: A => List[B]): List[B] = for (x <- xs; y <- f(x)) yield y def filter[a](xs: List[A], p: A => Boolean): List[A] = for (x <- xs if p(x)) yield x Jak złożymy tłumaczenia w obie strony, to dostaniemy definicję map przy pomocy map, itd.
12 Pętle for Jest też wersja jedynie wyliczająca wyrażenia, ale nie zbierająca ich wyniku for ( s ) e Przykład: wypisywanie macierzy reprezentowanej jako lista list for (xs <- xss) { for (x <- xs) print(x + "\t") println() Translacja jest prostsza, po trzeba jedynie foreach
13 For-Comprehensions nie tylko dla list Scala zawiera jeszcze inne typy danych wspierające For-Comprehensions Typ danych powinien definiować map, flatmap i filter Należy też zadbać o właściwe typy, np. dla C[A] def map[b](f: A => B): C[B] def flatmap[b](f: A => C[B]): C[B] def filter(p: A => Boolean): C[A] Niestety w Scali nie da się zdefiniować stosownego trait, bo te deklaracje w typie C odwołują się do typu C (jako konstruktora typu stosowanego z różnymi parametrami) Zastosowania: bazy danych, drzewa XML,...
14 Stan Jedyne efekty uboczne, które dotychczas widzieliśmy, to wypisywanie na konsolę Bez efektów ubocznych nie ma znaczenia kolejność wykonywania operacji (wszystkie prowadzą do tego samego wyniku) Innymi słowy czas jest nieistotny Co to stan obiektu/stanowość? Stan to wartość wszystkich atrybutów atrybutów obiektu Obiekt ma stan jeżeli jego zachowanie zależy od historii (np. konto bankowe) Deklarowanie atrybutów var zamiast val var x: String = "abc" Zmienne należy od razu zainicjować (var x: T = _ inicjuje wartością domyślną dla danego typu) Niezainicjowanie atrybutu czyni klasę abstrakcyjną
15 Przykład class BankAccount { private var balance = 0 def deposit(amount: Int) { if (amount > 0) balance += amount def withdraw(amount: Int): Int = if (0 < amount && amount <= balance) { balance -= amount balance else error("insufficient funds") val myaccount = new BankAccount
16 Przykład c.d. scala> myaccount deposit 50 scala> myaccount withdraw 20 res1: Int = 30 scala> myaccount withdraw 20 res2: Int = 10 scala> myaccount withdraw 15 java.lang.runtimeexception: insufficient funds at scala.predef$.error(predef.scala:76) at BankAccount.withdraw(<console>:13) at.<init>(<console>:7) at.<clinit>(<console>) at RequestResult$.<init>(<console>:3) at RequestResult$.<clinit>(<console>) at RequestResult$result(<console>) at sun.reflect.nativemethodaccessorimpl.invoke0(native Method) at sun.reflect.nativemethodacce...
17 Kiedy zmienne wskazują tę samą wartość? Referential transparency jak nie ma przypisań, to następujące definicje dają ten sam efekt: val x = E; val y = E val x = E; val y = x Czy to się zmienia dla val x = new BankAccount; val y = new BankAccount Operational equivalence x oraz y definiują tę samą wartość jeżeli w żadnym eksperymencie nie da się ich odróżnić Test: (1) niech S to dowolny ciąg operacji z x oraz y, (2) niech S' to ciąg otrzymany z S przez zamianę wszystkich wystąpień y na x (3) jeżeli zawsze wyniki S i S' są takie same, to x i y są takie same
18 Przykład Dla val x = new BankAccount; val y = new BankAccount Wynik S > val x = new BankAccount > val y = new BankAccount > x deposit 30 30 > y withdraw 20 java.lang.runtimeexception: insufficient funds Wynik S' > val x = new BankAccount > val y = new BankAccount > x deposit 30 30 > x withdraw 20 10 A jak jest dla val x = new BankAccount; val y = x
19 Imperative control structures Dla wygody Scala ma instrukcje z imperatywnych języków jak C i Java (brakuje break i continue, a zamiast pętli for są For-Comprehensisons) Przykład: x^n=x^(ak*2^k)*...*x^(a0*2^0), gdzie n binarnie to ak...a0 def power(x: Double, n: Int): Double = { var r = 1.0; var i = n; var j = 0 while (j < 32) { r = r * r if (i < 0) r *= x i = i << 1 j += 1 r Zawsze można sobie bez nich poradzić def whileloop(condition: => Boolean)(command: => Unit) { if (condition) { command; whileloop(condition)(command) else ()
20 Przykład: symulacja dyskretnych zdarzeń Mały język do reprezentacji obwodów scalonych Przewody przenoszą sygnały (true/false), które są przetwarzane przez bramki Podstawowe bramki: inverter, and-gate, or-gate Za pomocą podstawowych bramek można budować inne Bramki wprowadzają opóźnienia
21 Przykład c.d. type Action = () => Unit class Wire { private var sigval = false private var actions: List[Action] = List() def getsignal = sigval def setsignal(s: Boolean) = if (s!= sigval) { sigval = s actions.foreach(action => action()) def addaction(a: Action) { actions = a :: actions; a() //case class żeby były domyślne metody dostępu, itp. case class WorkItem(time: Int, action: Action)
22 private type Agenda = List[WorkItem] private var agenda: Agenda = List() private var currenttime = 0 private def insert(ag: Agenda, item: WorkItem): Agenda = if (ag.isempty item.time < ag.head.time) item :: ag else ag.head :: insert(ag.tail, item) def afterdelay(delay: Int)(block: => Unit) { val item = WorkItem(currentTime + delay, () => block) agenda = insert(agenda, item) private def next() { agenda match { case WorkItem(time, action) :: rest => agenda = rest; currenttime = time; action() case List() => def run() { afterdelay(0) { println("*** simulation started ***") while (!agenda.isempty) next()
23 private val InverterDelay = 3; private val OrGateDelay = 1 private val AndGateDelay = 5 def inverter(input: Wire, output: Wire) { def invertaction() { val inputsig = input.getsignal afterdelay(inverterdelay) { output setsignal!inputsig input addaction invertaction def andgate(a1: Wire, a2: Wire, output: Wire) { def andaction() { val a1sig = a1.getsignal val a2sig = a2.getsignal afterdelay(andgatedelay) { output setsignal (a1sig & a2sig) a1 addaction andaction a2 addaction andaction def orgate(a1: Wire, a2: Wire, output: Wire) {...
24 def halfadder(a: Wire, b: Wire, s: Wire, c: Wire) { val d = new Wire val e = new Wire andgate(a, b, c) orgate(a, b, d) inverter(c, e) andgate(d, e, s) a b c d e s --------------------- 0 0 0 0 1 0 0 1 0 1 1 1 1 0 0 1 1 1 1 1 1 1 0 0
25 def fulladder(a: Wire, b: Wire, cin: Wire, sum: Wire, cout: Wire) { val s = new Wire val c1 = new Wire val c2 = new Wire halfadder(a, cin, s, c1) halfadder(b, s, sum, c2) orgate(c1, c2, cout) a b cin s c1 c2 sum cout 0 0 0 0 0 0 0 0 0 0 1 1 0 0 1 0 0 1 0 0 0 0 1 0 1 0 0 1 0 0 1 0 0 1 1 1 0 1 0 1 1 1 0 1 0 1 0 1 1 0 1 0 1 0 0 1 1 1 1 0 1 0 1 1
26 def probe(name: String, wire: Wire) { wire addaction { () => println(name +" " +currenttime +" new_value=" +wire.getsignal) def main(args: Array[String]) = { val input1, input2, sum, carry = new Wire probe("sum", sum) probe("carry", carry) halfadder(input1, input2, sum, carry) input1 setsignal true; run input2 setsignal true; run sum 0 new_value=false carry 0 new_value=false *** simulation started *** sum 8 new_value=true *** simulation started *** carry 13 new_value=true sum 21 new_value=false
27 Jeszcze o upływie czasu Czy upływ czasu można modelować jedynie funkcjami, których wartości się nie zmieniają Parametr zamieniający się w matematyce przedstawia się jako funkcję f(t) Zmieniającą się zmienną var x: T można zamienić na niezmieniającą się var x: List[T] Wymieniamy pamięć na możliwość zaglądania w przyszłość i przeszłość
28 Strumienie Strumienie dają to samo co listy, ale ogon jest wyliczany tylko jak naprawdę jest potrzebny Przykład definicji Stream.cons(1, Stream.cons(2, Stream.empty)) Przykład użycia def range(start: Int, end: Int): Stream[Int] = if (start >= end) Stream.empty else Stream.cons(start, range(start + 1, end)) Stream.cons wylicza ogon dopiero wtedy gdy jest do niego odwołanie Dostępne są podstawowe metody: head, tail, isempty np. def print(xs: Stream[A]) { if (!xs.isempty) { Console.println(xs.head); print(xs.tail) Dostępna jest też większość pozostałych metod Stream.range(1000, 10000) filter isprime at 1
29 Strumienie Nie ma natomiast operatorów prawego argumentu :: ani ::: Zamiast x :: xs używamy Stream.cons(x, xs) Zamiast xs ::: ys używamy xs append ys.
Leniwość object lazylib { def delay[a](value: => A): Susp[A] = new SuspImpl[A](value) implicit def force[a](s: Susp[A]): A = s() abstract class Susp[+A] extends Function0[A] class SuspImpl[A](lazyValue: => A) extends Susp[A] { private var maybevalue: Option[A] = None override def apply() = maybevalue match { case None => val value = lazyvalue maybevalue = Some(value) value case Some(value) => value override def tostring() = maybevalue match { case None => "Susp(?)" case Some(value) => "Susp(" + value + ")" 30
31 Leniwość c.d. object Main { import lazylib._ def main(args: Array[String]) = { val s: Susp[Int] = delay { println("evaluating..."); 3 println("s = " + s) // show that s is unevaluated println("s() = " + s()) // evaluate s println("s = " + s) // show that the value is saved println("2 + s = " + (2 + s)) // implicit call to force() val sl = delay { Some(3) val sl1: Susp[Some[Int]] = sl val sl2: Susp[Option[Int]] = sl1 // the type is covariant println("sl2 = " + sl2) println("sl2() = " + sl2()) println("sl2 = " + sl2) s = Susp(?) evaluating... s() = 3 s = Susp(3) 2 + s = 5 sl2 = Susp(?) sl2() = Some(3) sl2 = Susp(Some(3))