PO* - Scala (typy uogólnione, listy)sroka/dydaktyka/po_gw/scala3.pdfprintln(dup[Int](3, 4))...
Transcript of PO* - Scala (typy uogólnione, listy)sroka/dydaktyka/po_gw/scala3.pdfprintln(dup[Int](3, 4))...
Przygotował: Jacek Sroka
1
PO* - Scala (typy uogólnione, listy)
przygotował Jacek Srokaw oparciu o materiały Martina Oderskiego
Przygotował: Jacek Sroka
2
Przykład: stos
abstract class IntStack { def push(x: Int): IntStack = new IntNonEmptyStack(x, this) def isEmpty: Boolean def top: Int def pop: IntStack}
class IntEmptyStack extends IntStack { def isEmpty = true def top = error("EmptyStack.top") def pop = error("EmptyStack.pop")}
class IntNonEmptyStack(elem: Int, rest: IntStack) extends IntStack { def isEmpty = false def top = elem def pop = rest}
No to teraz jeszcze raz dla stosu napisów?
Przygotował: Jacek Sroka
3
Typy uogólnione
abstract class Stack[A] { def push(x: A): Stack[A] = new NonEmptyStack[A](x, this) def isEmpty: Boolean def top: A def pop: Stack[A]}
class EmptyStack[A] extends Stack[A] { def isEmpty = true def top = error("EmptyStack.top") def pop = error("EmptyStack.pop")}
class NonEmptyStack[A](elem: A, rest: Stack[A]) extends Stack[A] { def isEmpty = false def top = elem def pop = rest}
val x = new EmptyStack[Int]val y = x.push(1).push(2)println(y.pop.top)
Przygotował: Jacek Sroka
4
Metody polimorficzne
● Metody i ich parametry też można parametryzować typem
def isPrefix[A](p: Stack[A], s: Stack[A]): Boolean = { p.isEmpty || p.top == s.top && isPrefix[A](p.pop, s.pop)}
val s1 = new EmptyStack[String].push("abc")val s2 = new EmptyStack[String].push("abx").push(s1.top)println(isPrefix[String](s1, s2))
● Niezależnie od generyczności klasy
object PolyTest extends Application { def dup[T](x: T, n: Int): List[T] = if (n == 0) Nil else x :: dup(x, n – 1) println(dup[Int](3, 4)) println(dup("three", 3))//scala ma dobry algorytm type inference}
Przygotował: Jacek Sroka
5
Ograniczanie typu
● Dowolny typ to czasami za dużo
abstract class Set[A] { def incl(x: A): Set[A] def contains(x: A): Boolean}
//dla implementacji z drzewami BSTdef contains(x: A): Boolean = if (x < elem) left contains x ^ < not a member of type A.
● Można go ograniczyć (do typów które da się porównywać)
trait Set[A <: Ordered[A]] { def incl(x: A): Set[A] def contains(x: A): Boolean}
Przygotował: Jacek Sroka
6
Ograniczanie typu c.d.
/** A class for totally ordered data. */trait Ordered[A] { /** Result of comparing ‘this’ with operand ‘that’. * returns ‘x’ where * x < 0 iff this < that * x == 0 iff this == that * x > 0 iff this > that */ def compare(that: A): Int
def < (that: A): Boolean = (this compare that) < 0 def > (that: A): Boolean = (this compare that) > 0 def <= (that: A): Boolean = (this compare that) <= 0 def >= (that: A): Boolean = (this compare that) >= 0 def compareTo(that: A): Int = compare(that)}
Przygotował: Jacek Sroka
7
Parametryzowane drzewa BST
class EmptySet[A <: Ordered[A]] extends Set[A] { def contains(x: A): Boolean = false def incl(x: A): Set[A] = new NonEmptySet(x, new EmptySet[A], new EmptySet[A])}
class NonEmptySet[A <: Ordered[A]] (elem: A, left: Set[A], right: Set[A]) extends Set[A] { def contains(x: A): Boolean = if (x < elem) left contains x else if (x > elem) right contains x else true def incl(x: A): Set[A] = if (x < elem) new NonEmptySet(elem, left incl x, right) else if (x > elem) new NonEmptySet(elem, left, right incl x) else this}//brakujące parametry typu w wywołaniach konstruktorów są uzupełniane tak
jak dla metod
Przygotował: Jacek Sroka
8
Drzewa BST na liczbach
● Element drzewa
case class Num(value: Double) extends Ordered[Num] { def compare(that: Num): Int = if (this.value < that.value) -1 else if (this.value > that.value) 1 else 0 }
● Poprawne użycie
val s = new EmptySet[Num].incl(Num(1.0)).incl(Num(2.0)) s.contains(Num(1.5))
● Niepoprawne użycie
val s = new EmptySet[java.io.File] ^ java.io.File does not conform to type parameter bound Ordered[java.io.File].
Przygotował: Jacek Sroka
9
View bounds
● A co jeśli nie możemy decydować o nadklasie? trait Set[A <% Ordered[A]] ...
class EmptySet[A <% Ordered[A]] ... class NonEmptySet[A <% Ordered[A]] ...
● O konwersjach i view bounds jeszcze opowiemy
Przygotował: Jacek Sroka
10
Subtyping
● co-variant subtypingclass Stack[+A] {...
– Jeśli T jest podtypem S to Stack[T] jest podtypem Stack[S]
● contra-variant subtypingclass Stack[-A] {...
– Jeśli T jest podtypem S to Stack[S] jest podtypem Stack[T]
● non-variant subtyping(domyślnie)
– nie ma zależności
Przygotował: Jacek Sroka
11
Czemu co-variant nie jest domyślne?
● W świecie funkcyjnym mogłoby być● Jeżeli obiekty mogą zmieniać stan trzeba kontrolować typy w chwili wykonania
class Array[A] { def appply(index: Int): A def update(index: Int, elem: A)}//Scala stara się to mapować na natywne tablice środowiska
val x = new Array[String](1)val y: Array[Any] = xy(0) = new Rational(1, 2) //lukier syntaktyczny dla //y.update(0, new Rational(1, 2))
– czyli w tablicy napisów znalazłaby się liczba!
● W Javie dla tablic mamy co-variant subtyping i dodatkową kontrolę w trzeciej linijce (ArrayStoreException)
– Ze względu na non-variant subtyping w Scali druga linia jest niedozwolona
– Tak! W Javie dla tablic jest co-variant mimo że dla innych typów jest non-variant!
Przygotował: Jacek Sroka
12
Statyczne ograniczanie co-variant subtyping
● Parametr co-variant może być w Scali użyty jedynie jako typ: atrybutu klasy, wartości zwrotnej metody, argument jakiejś innej klasy co-variant
● Nie może jako typ parametru metody (bo metody mogą zmieniać stan obiektu) class Array[+A] { def apply(index: Int): A def update(index: Int, elem: A) ^ covariant type parameter A appears in contravariant position.
● A co z metodami nie zmieniającymi stanu (stos jest w pełni funkcyjny)
class Stack[+A] {def push(x: A): Stack[A] = ^ covariant type parameter A appears in contravariant position.
Przygotował: Jacek Sroka
13
Co-variant subtyping i niezmieniające stanu metody
● Można uogólnić metodę push
class Stack[+A] { def push[B >: A](x: B): Stack[B] = new NonEmptyStack(x, this)
● T >: S oznacza, że T może być nadtypem S
– można łączyć oba zapisy T >: S <: U
● Czyli w wyniku push możemy dostać zbiór bardziej ogólnego typu
● Rozwiązując problem parametrów co-variant uogólniliśmy nasze rozwiązanie
Przygotował: Jacek Sroka
14
Najmniejsze typy
● W Scali typami nie można parametryzować obiektów
– dlatego musimy zdefiniować klasę EmptyStack[A]
– dla stosów co-variant moglibyśmy sobie poradzić mając podtyp wszystkich typów
● Typ Nothing jest najmniejszym typem i nie zawiera żadnej wartości (czyli Stack[Nothing] nie ma żadnych elementów)
object EmptyStack extends Stack[Nothing] { ... }
● val s = EmptyStack.push("abc").push(new AnyRef())
– EmptyStack jest typu Stack[Nothing], więc ma metodępush[B >: Nothing](elem: B): Stack[B]
– z type inference wiemy, że B to String, więc wynikiem jest Stack[String]push[B >: String](elem: B): Stack[B]
– teraz B to AnyRef, więc wynikiem jest Stack[AnyRef]
Przygotował: Jacek Sroka
15
Null
● Nothing jest podtypem wszystkich typów
● Null jest podtypem AnyRef i wszystkich jego podtypów
● Null posiada tylko jedną wartość null
● Czyli null jest kompatybilne ze wszystkimi obiektami, ale nie z wartościami typów podstawowych
Przygotował: Jacek Sroka
16
Hierarchia klas
Przygotował: Jacek Sroka
17
Przykład
Jaki ma typ?
if (true) 1 else true
Przygotował: Jacek Sroka
18
Przykład
Jaki ma typ?
if (true) 1 else true
AnyVal (oraz Any)
Przygotował: Jacek Sroka
19
Podsumowanie
abstract class Stack[+A] { def push[B >: A](x: B): Stack[B] = new NonEmptyStack(x, this) def isEmpty: Boolean def top: A def pop: Stack[A]}
object EmptyStack extends Stack[Nothing] { def isEmpty = true def top = error("EmptyStack.top") def pop = error("EmptyStack.pop")}
class NonEmptyStack[+A](elem: A, rest: Stack[A]) extends Stack[A] { def isEmpty = false def top = elem def pop = rest}
Przygotował: Jacek Sroka
20
Krotki
● Czasami funkcja zwraca kilka wartości
case class TwoInts(first: Int, second: Int)def divmod(x: Int, y: Int): TwoInts = new TwoInts(x / y, x % y)
● Scala ma wbudowany typ generyczny do reprezentowania krotek
package scalacase class Tuple2[A, B](_1: A, _2: B)def divmod(x: Int, y: Int) = new Tuple2[Int, Int](x / y, x % y)
● Są też krotki o większej liczbie elementów (oczywiście są jakieś ograniczenia)
● Dostęp do elementów krotki (tak jak do innych case classes)– val xy = divmod(x, y)
println("quotient: " + xy._1 + ", rest: " + xy._2)
– divmod(x, y) match { case Tuple2(n, d) => println("quotient: " + n + ", rest: " + d)}//we wzorcach nie używamy type parameters (Tuples2[Int, Int](n,d))
Przygotował: Jacek Sroka
21
Lukier syntaktyczny
● Zamiast Tuplen(x1,..., xn) można napisać (x1, ..., xn)
● def divmod(x: Int, y: Int): (Int, Int) = (x / y, x % y)
● divmod(x, y) match { case (n, d) => println("quotient: " + n + ", rest: " + d)}
Przygotował: Jacek Sroka
22
Funkcje
● Wszystko w Skali jest obiektem● Funkcje w Scali są zwykłymi wartościami● Czyli funkcje są obiektami
– (T1, ..., Tn) => S
– Functionn[T1, ..., Tn, S]
● Przykładowy trait dla funkcji z jednym argumentem
package scalatrait Function1[-A, +B] { def apply(x: A): B}
– f(x) to skrót dla f.apply(x)
– to dlatego Array miało metodę apply
class Array[A] { def apply(index: Int): A //stąd skrót a(i) odpowiada a.apply(i) def update(index: Int, elem: A)}
Przygotował: Jacek Sroka
23
Tu się przydaje contra-variant subtyping
● val f: (AnyRef => Int)val g: (String => Int)g("abc")
● Dla funkcji mamy co-variant dla wartości zwrotnej i contra-variant dla wartości argumentów
– S=>T jest podtypem S'=>T' jeżeli S' jest podtypem S i T jest podtypem T'
Przygotował: Jacek Sroka
24
Przykład: funkcje
● Deklaracja: val plus1: (Int => Int) = (x: Int) => x + 1plus1(2)
● Rozwija się do:
val plus1: Function1[Int, Int] = new Function1[Int, Int] { def apply(x: Int): Int = x + 1}plus1.apply(2)
● new Function1[Int, Int] {...} to klasa anonimowa
val plus1: Function1[Int, Int] = { class Local extends Function1[Int, Int] { def apply(x: Int): Int = x + 1 } new Local: Function1[Int, Int]}plus1.apply(2)
Przygotował: Jacek Sroka
25
Dalszy lukier syntaktyczny
● Zamiast ((x,y)=>x+y)) można pisać (_ * _)
– kolejne _ reprezentują kolejne parametry funkcji
● Uwaga: listy parametrów funkcji i krotki to nie to samo!
Przygotował: Jacek Sroka
26
Listy
Są podobne do tablic, ale● niemutowalne● mają rekurencyjną budowę● udostępniają więcej operacji
val fruit = List("apples", "oranges", "pears")val nums = List(1, 2, 3, 4)val diag3 = List(List(1, 0, 0), List(0, 1, 0), List(0, 0, 1))val empty = List()
Przygotował: Jacek Sroka
27
Typ listy
● Tak jak tablice są homogeniczne
● Ich typ zapisujemy List[T]
val fruit: List[String] = List("apples", "oranges", "pears")val nums : List[Int] = List(1, 2, 3, 4)val diag3: List[List[Int]] = List(List(1, 0, 0), List(0, 1, 0), List(0, 0, 1))val empty: List[Int] = List()
Przygotował: Jacek Sroka
28
Jeszcze trochę lukru
● Można korzystać z notacji (tak naprawdę poprzedni zapis się do niej sprowadza) Nil, ::
val fruit = "apples" :: ("oranges" :: ("pears" :: Nil))
● :: jest prawostronnie łączny
val nums = 1 :: 2 :: 3 :: 4 :: Nil val diag3 = (1 :: 0 :: 0 :: Nil) :: (0 :: 1 :: 0 :: Nil) :: (0 :: 0 :: 1 :: Nil) :: Nil val empty = Nil
● List ma też podstawowe operacje
empty.isEmpty = true fruit.isEmpty = false fruit.head = "apples" fruit.tail.head = "oranges" diag3.head = List(1, 0, 0) nums.reverse = 4::3::2::1::Nil nums indexOf 5 = -1 nums contains 5 = false
Przygotował: Jacek Sroka
29
Listy i pattern matching
● Nil – lista pusta
● x::xs – lista z głową i ogonem
● x::Nil – lista z jednym elementem
● List() – lista pusta
● List(x) – lista z jednym elementem
● List(x1,x2,x3) – lista z trzema elementami
● 1::2::3::xs – co najmniej trzy elementy, zaczyna się od 1,2,3
● List(1::xs) – ???
Przygotował: Jacek Sroka
30
Przykład: sortowanie
● isEmpty, head i tail oraz rekurencja pozwalają zdefiniować na listach wszystkie operacje, np. sortowanie przez wstawianie
def isort(xs: List[Int]): List[Int] = if (xs.isEmpty) Nil else insert(xs.head, isort(xs.tail))
● :: to tak naprawdę case class
def isort(xs: List[Int]): List[Int] = xs match { case List() => List() case x :: xs1 => insert(x, isort(xs1)) }
def insert(x: Int, xs: List[Int]): List[Int] = xs match { case List() => List(x) case y :: ys => if (x <= y) x :: xs else y :: insert(x, ys) }
Przygotował: Jacek Sroka
31
Definicja w pakiecie scala
● Klasa List jest zdefiniowana w pakiecie scala z parametrem co-variant
package scalaabstract class List[+A] {
● Przykładowe implementacje metod
def isEmpty: Boolean = this match { case Nil => true case x :: xs => false}
def head: A = this match { case Nil => error("Nil.head") case x :: xs => x}
def tail: List[A] = this match { case Nil => error("Nil.tail") case x :: xs => xs}
Przygotował: Jacek Sroka
32
Inne funkcje
● Długość listy(oczywiście da się zastosować rekursję ogonową)
def length: Int = this match { case Nil => 0 case x :: xs => 1 + xs.length}
● Ostatni element na liście
def last: A = this match { case Nil => error("Nil.last") case x :: Nil => x case x :: xs => xs.last}
● Wszystko bez ostatniego elementu
def init: A = this match { case Nil => error("Nil.last") case x :: Nil => Nil case x :: xs => x::xs.init}
Przygotował: Jacek Sroka
33
Inne funkcje c.d.
● Pierwsze n, wszystko bez pierwszych n oraz podział
def take(n: Int): List[A] = if (n == 0 || isEmpty) Nil else head :: tail.take(n-1)
def drop(n: Int): List[A] = if (n == 0 || isEmpty) this else tail.drop(n-1)
def split(n: Int): (List[A], List[A]) = (take(n), drop(n))
– xs.drop(m).take(n – m) //daje elementy od m+1 do n
– def apply(n: Int): A = drop(n-1).head//czyli działa lukier xs.apply(3) = xs(3)
● Z pary list tworzy listę par (ucina dłuższą listę)
def zip[B](that: List[B]): List[(A,B)] = if (this.isEmpty || that.isEmpty) Nil else (this.head, that.head) :: (this.tail zip that.tail)
Przygotował: Jacek Sroka
34
Operator ::
● Operatory kończące się dwukropkiem są szczególne
– Są traktowane jako metody prawego operandu
x :: y = y.::(x) zamiast x + y = x.+(y)
– Mimo to zachowują kolejność wyliczania operandów od lewej
D :: E tłumaczy się do {val x = D; E.::(x)}
– Są prawostronnie łączne
x :: y :: z = x :: (y :: z) zamiast x + y + z = (x + y) + z
● Przykładowa definicja
def ::[B >: A](x: B): List[B] = new scala.::(x, this)
Przygotował: Jacek Sroka
35
Konkatenacja i odwracanie
● ::: to operator konkatenacji (też kończy się dwukropkiem)
– xs ::: ys ::: zs =xs ::: (ys ::: zs) =zs.:::(ys).:::(xs)
– def :::[B >: A](prefix: List[B]): List[B] = prefix match { case Nil => this case p :: ps => this.:::(ps).::(p)}
● Odwrócona lista
def reverse[A](xs: List[A]): List[A] = xs match { case Nil => Nil case x :: xs => reverse(xs) ::: List(x)}
– Ta implementacja jest kwadratowa (bo konkatenacja jest liniowa względem długości pierwszego operandu), ale można lepiej
● ++ to konkatenacja dla dowolnych obiektów iterowalnych
Przygotował: Jacek Sroka
36
Merge sort
def msort[A](less: (A, A) => Boolean)(xs: List[A]): List[A] = {
def merge(xs1: List[A], xs2: List[A]): List[A] =
if (xs1.isEmpty) xs2
else if (xs2.isEmpty) xs1
else if (less(xs1.head, xs2.head)) xs1.head :: merge(xs1.tail, xs2)
else xs2.head :: merge(xs1, xs2.tail)
val n = xs.length/2
if (n == 0) xs
else merge(msort(less)(xs take n), msort(less)(xs drop n))
}
msort((x: Int, y: Int) => x < y)(List(5, 7, 1, 3))
val intSort = msort((x: Int, y: Int) => x < y)
val reverseSort = msort((x: Int, y: Int) => x > y)
Przygotował: Jacek Sroka
37
scala.math.Ordering[T]
● Klasa ze standardowej biblioteki zbierająca porządki scala.math.Ordering[T] z metodą ord.lt(x,y)
– np. Ordering.Int lub Ordering.String
Przygotował: Jacek Sroka
38
Wbudowane sortowanie
val dania = List("Pad Thai", "Bun Nem", "Bun Dau Phu")
dania sortWith (_.length < _.length)
//List("Bun Nem", "Pad Thai", "Bun Dau Phu")
dania sorted
//List("Bun Dau Phu", "Bun Nem", "Pad Thai")
Przygotował: Jacek Sroka
39
Typowe operacje wyższego rzędu
● Inny rodzaj polimorfizmu● Często spotykane, warto znać i stosować
● Przetworzenie każdego elementu przy pomocy jakiejś funkcji
● Wydobycie wszystkich elementów spełniających zadane kryterium
● Agregacja wszystkich elementów przy pomocy jakiegoś operatora
Przygotował: Jacek Sroka
40
map
● Motywacja – skalowanie każdego elementu przez jakiś współczynnik
def scaleList(xs: List[Double], factor: Double): List[Double] = xs match { case Nil => xs case x :: xs1 => x * factor :: scaleList(xs1, factor)}
● Uogólniona definicja z List
def map[B](f: A => B): List[B] = this match { case Nil => this case x :: xs => f(x) :: xs.map(f)}
● Skalowanie jeszcze raz
def scaleList(xs: List[Double], factor: Double) = xs map (x => x * factor)
● Zwracanie kolumny z tablicy reprezentowanej jako lista wierszy
def column[A](xs: List[List[A]], index: Int): List[A] = xs map (row => row(index))
Przygotował: Jacek Sroka
41
foreach
● Podobna do map, ale nie produkuje wyniku (tylko dla efektów ubocznych)
def foreach(f: A => Unit) { this match { case Nil => () case x :: xs => f(x); xs.foreach(f) }}
● Przykładem efektu ubocznego jest wypisywanie
xs foreach (x => println(x))
Przygotował: Jacek Sroka
42
filter
● Motywacja – zwracanie wszystkich elementów dodatnich
def posElems(xs: List[Int]): List[Int] = xs match { case Nil => xs case x :: xs1 => if (x > 0) x :: posElems(xs1) else posElems(xs1)}
● Uogólniona definicja z List
def filter(p: A => Boolean): List[A] = this match { case Nil => this case x :: xs => if (p(x)) x :: xs.filter(p) else xs.filter(p)}
● Elementy dodatnie jeszcze raz
def posElems(xs: List[Int]): List[Int] = xs filter (x => x > 0)
Przygotował: Jacek Sroka
43
Kwantyfikatory
● Definicja
def forall(p: A => Boolean): Boolean = isEmpty || (p(head) && (tail forall p))def exists(p: A => Boolean): Boolean = !isEmpty && (p(head) || (tail exists p))
● Przykład: sprawdzanie czy liczba jest pierwsza
def isPrime(n: Int) = List.range(2, n) forall (x => n % x != 0)
//gdziepackage scalaobject List { ... def range(from: Int, end: Int): List[Int] = if (from >= end) Nil else from :: range(from + 1, end)
Przygotował: Jacek Sroka
44
Dygresja: zakresy
● To rodzaj Sequence, ale nie przechowują elementów tylko sposób wyliczania
– val r1: Range = 1 until 3 //1,2
– val r2: Range = 1 to 3 //1,2,3
– 10 to 1 by -2 //10,8,6,4,2
Przygotował: Jacek Sroka
45
Agregowanie
● Motywacja
sum(List(x1 , ..., xn )) = 0 + x1 + ... + xnproduct(List(x1 , ..., xn )) = 1 * x1 * ... * xn
● Implementacja wprost:
def sum(xs: List[Int]): Int = xs match { case Nil => 0 case y :: ys => y + sum(ys)}
def product(xs: List[Int]): Int = xs match { case Nil => 1 case y :: ys => y * product(ys)}
Przygotował: Jacek Sroka
46
reduceLeft
● Wspólny motyw
List(x1 , ..., xn ).reduceLeft(op) = (...(x1 op x2 ) op ... ) op xn
def sum(xs: List[Int]) = (0 :: xs) reduceLeft {(x, y) => x + y}def product(xs: List[Int]) = (1 :: xs) reduceLeft {(x, y) => x * y}
● Przykładowa definicja
def reduceLeft(op: (A, A) => A): A = this match { case Nil => error("Nil.reduceLeft") case x :: xs => (xs foldLeft x)(op)}
def foldLeft[B](z: B)(op: (B, A) => B): B = this match { case Nil => z case x :: xs => (xs foldLeft op(z, x))(op)}
– foldLeft można też użyć bezpośrednio
– (List(x1 , ..., xn ) foldLeft z)(op) = (...(z op x1 ) op ... ) op xn
– def sum(xs: List[Int]) = (xs foldLeft 0) {(x, y) => x + y}def product(xs: List[Int]) = (xs foldLeft 1) {(x, y) => x * y}
Przygotował: Jacek Sroka
47
reduce...
● Redukcję można też wykonywać w prawo
List(x1 , ..., xn ).reduceRight(op) = x1 op ( ... (xn−1 op xn )...)(List(x1 , ..., xn ) foldRight acc)(op) = x1 op ( ... (xn op acc)...)
● Przykładowa definicja
def reduceRight(op: (A, A) => A): A = this match { case Nil => error("Nil.reduceRight") case x :: Nil => x case x :: xs => op(x, xs.reduceRight(op))}
def foldRight[B](z: B)(op: (A, B) => B): B = this match { case Nil => z case x :: xs => op(x, (xs foldRight z)(op))}
● Synonimy
def /:[B](z: B)(f: (B, A) => B): B = foldLeft(z)(f)def :\[B](z: B)(f: (A, B) => B): B = foldRight(z)(f)
– Co pasuje do zasady że operator kończący się : jest wywoływany dla prawego operandu(z /: List(x1 , ..., xn ))(op) = (...(z op x1 ) op ... ) op xn(List(x1 , ..., xn ) :\ z)(op) = x1 op ( ... (xn op z)...)
Przygotował: Jacek Sroka
48
flatten
● Łączy listy z listy list
def flatten[A](xs: List[List[A]]): List[A] = (xs :\ (Nil: List[A])) {(x, xs) => x ::: xs}
● A co z alternatywną definicją (która działa szybciej)?(konkatenacja działa proporcjonalnie do dł pierwszego operandu)
def flatten[A](xs: List[List[A]]): List[A] = ((Nil: List[A]) /: xs) ((xs, x) => xs ::: x)
● Prawdziwy flatten jest w obiekcie List
List.flatten
Przygotował: Jacek Sroka
49
Liniowe reverse
● Odgadnijmy czym zastąpić ?
def reverse: List[A] = (z? /: this)(op?)
● Nil= Nil.reverse // by specification= (z /: Nil)(op) // by the template for reverse= (Nil foldLeft z)(op) // by the definition of /:= z // by definition of foldLeft
● List(x)= List(x).reverse // by specification= (Nil /: List(x))(op) // by the template for reverse, with z = Nil= (List(x) foldLeft Nil)(op) // by the definition of /:= op(Nil, x) // by definition of foldLeft // czyli op(x,y) = y :: x
Przygotował: Jacek Sroka
50
Liniowe reverse
● Czyli
def reverse: List[A] = ((Nil: List[A]) /: this) {(xs, x) => x :: xs}
– adnotacja typu jest potrzebna
● Nil= Nil.reverse // by specification= (z /: Nil)(op) // by the template for reverse= (Nil foldLeft z)(op) // by the definition of /:= z // by definition of foldLeft
● List(x)= List(x).reverse // by specification= (Nil /: List(x))(op) // by the template for reverse, with z = Nil= (List(x) foldLeft Nil)(op) // by the definition of /:= op(Nil, x) // by definition of foldLeft
Przygotował: Jacek Sroka
51
Przykład
● Przy pomocy tych funkcji można rozwiązać wiele problemów dla których imperatywnie potrzebowaliśmy zagnieżdżonych pętli
● Przykład: dla danego n znaleźć wszystkie pary 1 <= j < i < ntakie że i+j jest pierwsza
– Dla n=7 i 2 3 4 4 5 6 6 j 1 2 1 3 2 1 5i+j 3 5 5 7 7 7 11
● Rozwiązanie w dwóch krokach
– Generujemy pary i,j
– Filtrujemy
List.range(2, n) .map(i => List.range(1, i).map(j => (i, j))) .foldRight(List[(Int, Int)]()) {(xs, ys) => xs ::: ys} .filter(pair => isPrime(pair._1 + pair._2))
Przygotował: Jacek Sroka
52
Przykład c.d.
● Kombinacja mapowania i konkatenacji jest tak często spotykana, że jest skrót (lub przy pomocy naszego flatten)
abstract class List[+A] { ... def flatMap[B](f: A => List[B]): List[B] = this match { case Nil => Nil case x :: xs => f(x) ::: (xs flatMap f) }}
● Nowe rozwiązanie
List.range(2, n) .flatMap(i => List.range(1, i).map(j => (i, j))) .filter(pair => isPrime(pair._1 + pair._2))