Strömmar (streams) De sista dagarna objekt med tillstånd modellerades som beräkningsobjekt med tillstånd. Isådana modeller är tiden modelerad (implicit) som en sekvens av tillstånd. För att kunna modellera detta måste vi utöka det funktionella språket med tilldelningar, som rubbar tydligheten och introducerar svåra problem. Kan vi ändra synvinkel? Om tiden betraktas som diskret (d.v.s. kan betraktas som heltal) så kan vi betrakta sekvenser av värde som en modell på tidsändring. En ström en datastruktur som modellerar en (möjligtvis oändlig) sekvens. Strömmar tillåter oss att glömma tilldelningen (men introducerar sina egna problem istället). Signalflödesmodellen Två (gamla) exempel: Kvadratera alla jämna löv. (define (sum-even-squares tree) (cond ((null? tree) 0) ((not (pair? tree)) (if (even? tree) (* tree tree) 0)) (else (+ (sum-even-squares (car tree)) (sum-even-squares (cdr tree)))))) Jacek Malec, Dept. of Computer Science, Lund University 247 Jacek Malec, Dept. of Computer Science, Lund University 248 Mönstret: Skapa en lista med alla udda Fibonacci tal F (k) för k<n. (define (odd-fibs n) (define (next k) (if (> k n) () (let ((f (fib k))) (if (odd? f) (cons f (next (+ k 1))) (next (+ k 1)))))) (next 0)) En samling av följande sorters procedurer: uppräkning; filtrering; mappning; samling; som agerar på listor! sum-even-squares: uppräkning, filtrering, mappning, samling (+, 0) odd-fibs: uppräkning, mappning, filtrering, samling (cons, ()) En sträm är en sekvens av data (en signal ) som flöder mellan procedurerna.
Strömmar För att kunna bearbeta strömmar behöver vi verktyg. Det finns ingen skillnad mellan den här beskrivningen och listspecifikationen vi hade tidigare! Dataabstraktion: konstruktorn cons-stream selektorer stream-car, stream-cdr predikat stream-null? tom objekt (nyhet!) the-empty-stream Semantik: För alla objekt a och b gäller att om x = ab) såär (stream-car x) = a x) = b cons-stream - cons stream-car - car stream-cdr - cdr stream-null? - null? the-empty-stream - () Vi kommer faktiskt att se strömmar som vanliga listor i början. Senare blir vi tvungna att skaffa en annan implementation för att effektivt kunna hantera mycket stora sammansättningar av data, t.o.m. oändligt stora!? Hur är det möjligt? Jacek Malec, Dept. of Computer Science, Lund University 251 Jacek Malec, Dept. of Computer Science, Lund University 252 Betrakta (igen) följande problem: Summera alla primtal mellan gränserna a och b. ; iterativt (define (sum-primes a b) (define (iter count accum) (cond ((> count b) accum) ((prime? count) (iter (+ count 1) (+ count accum))) (else (iter (+ count 1) accum)))) (iter a 0)) Signalflödesmodellen föreslår följande: ; sekvensbearbetning (define (sum-primes a b) (accumulate + 0 (filter prime? (enumerate-interval a b)))) 1. I första exemplet (iterativt) tillståndet sparas i count och accum 2. I det andra fallet: (a) enum-interval skapar hela listan; (b) filter går genom hela listan och skapar en ny lista; (c) accumulate går genom hela listan. Mycket jobb och plats! Ett även mer tilltalande exempel: Hitta det tredje primtalet mellan 10.000 och 1.000.000. Enkelt: (caddr (filter prime? (enumerate-interval 10000 1000000))) Nästan alla beräkningar görs i onödan!
Strömmar tillåter oss att manipulera sekvenser utan att vara tvungen att använda listor för detta ändamål! Innan vi pratar om implementationen, låt oss titta på grundläggande funktioner: (define (stream-ref s n) (if (= n 0) (stream-car s) (stream-ref s) (- n 1)))) (define (stream-map proc s) (if (stream-null? s) the-empty-stream (proc (stream-car s)) (stream-map proc s))))) (define (stream-for-each proc s) (if (stream-null? s) done (begin (proc (stream-car s)) (stream-for-each proc s))))) (define (display-stream s) (stream-for-each display-line s)) (define (display-line x) (newline) (display x)) Jacek Malec, Dept. of Computer Science, Lund University 255 Jacek Malec, Dept. of Computer Science, Lund University 256 Hjälpfunktionerna Vårt primtalexempel igen: (stream-car (stream-filter prime? (stream-enumerate-interval 10000 1000000)))) (define (stream-enumerate-interval low high) (if (> low high) the-empty-stream low (stream-enumerate-interval (+ low 1) high)))) (define (stream-filter pred stream) (cond ((stream-null? stream) the-empty-stream) ((pred (stream-car stream)) (stream-car stream) (stream-filter pred stream)))) (else (stream-filter pred stream)))))
Var ligger vinsten? Implementation: Vi låter cons-stream bygga endast början på en ström. Den innehåller det första elementet och information om hur man beräknar resten. Vi låter sedan stream-cdr tvinga fram beräkningen av resten. Det inebär att det inte beräknas fler element än vad som behövs. Användarenanenström behöver inte ändra på något eftersom syntaxen fortfarande är densamma. Hjälpfunktioner: (delay uttr) returnerar ett fördröjt objekt. Det innehåller information om hur man beräknar uttr. delay kan implementeras på följande sätt: (define (delay uttryck) (lambda () uttryck) (Sedan kommer vi att optimera detta.) (force fördröjt-objekt) evaluerar det födröjda uttrycket: (define (force fördröjt-uttryck) (fördröjt-uttryck)) Jacek Malec, Dept. of Computer Science, Lund University 259 Jacek Malec, Dept. of Computer Science, Lund University 260 Nu kan vi definiera om våra strömfunktioner. cons-stream är en särskild form, ab) ekvivalent till (cons a (delay b)) Resten är en direkt följd: (define (stream-car stream) (car stream)) (define stream) (force (cdr stream))) (define stream-null? null?) (define the-empty-stream ()) Varför är det effektivare? Behovstyrd beräkningsmodell (istället för applikativ) (stream-car (stream-filter prime? (stream-enumerate-interval 10000 1000000)))) Gör bara de cons ar som behövs för att beräkna värdet. I detta fall kommer vi att generera talen 10000, 10001, 10002,... Det första talet stream-filter släpper genom är 10007, det andra 10009, det tredje...?(x) x returneras som värde. Vi kommer aldrig att generera listor med mellanresultat som ej behövs!
En viss ineffektivitet Om vi beräknat början av en lista och vill återanvända den flera gånger så kommer det att forcera fram beräkning varje gång. T.ex. (define s 1 2 3 the-empty-stream)))) s = (1 "(lambda () 2...))") (stream-car s)) s)) medför att (lambda () 2...)) beräknas två gånger. Lösning: memorisering Efter första beräkningen av det fördröjda argumentet, så minns man värdet. (delay uttr) ersätts nu med (memo-proc (lambda () uttr)) Vi definierar memo-proc som (define (memo-proc proc) (let ((already-run? #f) (result #f)) (lambda () (if (not already-run?) (begin (set! result (proc)) (set! already-run? #t) result) result)))) call-by-name beräkna varje gång call-by-need beräkna en gång, minns resultatet Jacek Malec, Dept. of Computer Science, Lund University 263 Jacek Malec, Dept. of Computer Science, Lund University 264 Med den fördröjda evaluering kan vi hantera mycket stora (t.o.m. oändliga) strömmar. Några intressanta exempel: (define (integers-starting-from n) n (integers-starting-from (+ n 1)))) (define integers (integers-starting-from 1)) (define (divisible? x y) (= (remainder x y) 0)) (define no-sevens (stream-filter (lambda (x) (not (divisible? x 7))) integers)) (define (fibgen a b) a (fibgen b (+ a b)))) (define fibs (fibgen 0 1)) Eratosthenes såll 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16... 3 5 7 9 11 13 15 17 19 21 23... 5 7 11 13 17 19 23 25 29 31... 7 11 13 17 19 23 29 31... (define (sieve stream) (stream-car stream) (sieve (stream-filter (lambda (x) (not (divisible? x (stream-car stream)))) stream))))) (define primes (sieve (integers-starting-from 2))) Det 100:ade primtalet: (stream-ref primes 100)
Några andra konstiga (rekursiva) definitioner: (define ones 1 ones)) (define (add-streams s1 s2) (cond ((stream-null? s1) s2) ((stream-null? s2) s1) (else (+ (stream-car s1) (stream-car s2)) (add-streams s1) s2)))))) (define integers 1 (add-streams ones integers))) (define fibs 0 1 (add-streams fibs) fibs)))) Fördröjd beräkning Varför bara fördröja cons andra argument? Kan inte all beräkning av argument ske fördröjt? Om man fördröjer, när skall man tvinga fram en beräkning? Beräkning enligt normal ordning! Använd Lisp om du behöver det. Fördröjd evaluering och tilldelning (förändring av data) samspelar inte så bra.vivetinte exakt vad som kan hända. Svårt problem. Bra teori saknas.