diff --git a/lectures/index.md b/lectures/index.md index 8ed262a..2053881 100644 --- a/lectures/index.md +++ b/lectures/index.md @@ -11,6 +11,7 @@ introduces the programming language [Racket](https://racket-lang.org/). [Slides](https://github.com/aicenter/FUP/blob/main/lectures/lecture01.pdf). [Log](https://github.com/aicenter/FUP/blob/main/lectures/lecture01.rkt). + ## [Lecture 02](lecture02): Lists & Trees Focuses on Racket lists and trees. Further, it introduces the unit testing library [Rackunit](https://docs.racket-lang.org/rackunit/index.html). @@ -18,12 +19,6 @@ Focuses on Racket lists and trees. Further, it introduces the unit testing libra [Slides](https://github.com/aicenter/FUP/blob/main/lectures/lecture02.pdf). [Log](https://github.com/aicenter/FUP/blob/main/lectures/lecture02.rkt). -## [Lecture 3](lecture03): Higher Order Functions -Deals with higher-order functions like (`map`, `filter`, `foldl`), function closures and Racket -structures. - -[Slides](https://github.com/aicenter/FUP/blob/main/lectures/lecture03.pdf). -[Log](https://github.com/aicenter/FUP/blob/main/lectures/lecture03.rkt). ## [Lecture 3](lecture03): Higher Order Functions Deals with higher-order functions like (`map`, `filter`, `foldl`), function closures and Racket @@ -32,6 +27,7 @@ structures. [Slides](https://github.com/aicenter/FUP/blob/main/lectures/lecture03.pdf). [Log](https://github.com/aicenter/FUP/blob/main/lectures/lecture03.rkt). + ## [Lecture 4](lecture04): Lazy Evaluation Introduces pattern matching, and explains how to implement lazy evaluation and streams in Racket. diff --git a/lectures/lecture04.md b/lectures/lecture04.md index 14a40db..999a8a0 100644 --- a/lectures/lecture04.md +++ b/lectures/lecture04.md @@ -3,8 +3,7 @@ outline: deep --- -# Pattern matching, Lazy evaluation, Streams - +# Pattern matching & Lazy evaluation ## Pattern matching @@ -341,7 +340,7 @@ In Racket, the implicit definition of the stream of natural numbers based on the (define nats2 (stream-cons 0 (add-streams ones nats2))) ``` -### Applications of streams +## Applications of streams We have seen that streams provide an exciting way to deal with potentially infinite structures. Let us see some concrete situations where streams could be helpful. @@ -365,7 +364,14 @@ cpu time: 875 real time: 869 gc time: 640 49999995000000 ``` -Streams are further helpful because they can improve code modularity. When we need to generate a potentially infinite data structure, we must insert some tests into the generating code to stop the generation process. Consequently, the generating code and the tests are inseparable. On the other hand, if we use lazily evaluated data structures like streams, we can pretend that the infinite structure is first generated entirely, and then we do its post-processing independently. For more details on this idea, see the paper by John Hughes.[^why-fp-matters] +### Newton-Raphson + +Streams are further helpful because they can improve code modularity. When we need to generate a +potentially infinite data structure, we must insert some tests into the generating code to stop the +generation process. Consequently, the generating code and the tests are inseparable. On the other +hand, if we use lazily evaluated data structures like streams, we can pretend that the infinite +structure is first generated entirely, and then we do its post-processing independently. For more +details on this idea, see the paper by John Hughes.[^why-fp-matters] [^why-fp-matters]: John Hughes: Why Functional Programming Matters. Comput. J. 32(2): 98-107 (1989) @@ -377,7 +383,7 @@ A terminating condition could be $|1-\frac{g_i}{g_{i+1}}|\leq\varepsilon$ for a Now, we compare the code that mixes the generating code with the terminating condition and the modular code utilizing streams. -```scheme +```scheme:line-numbers (define eps 0.000000000001) (define (mean . xs) (/ (apply + xs) (length xs))) (define (next-guess n g) (mean g (/ n g))) @@ -434,11 +440,24 @@ Joining these two pieces gives us the desired square-root function: ``` It is reasonable to consider a solution using streams because of code modularity. When the solution is modular, the coder can independently focus on smaller pieces of code. This is mentally easier than devising one complex function. The above example should give you an idea of modularity. On the other hand, it is perhaps too simple to illustrate the advantage of streams, as the first solution is pretty straightforward. -For a more exciting example, consider a situation when our application needs to explore a graph of some states or configurations. For instance, we can look for a path in a digraph leading to a goal state, or the states can represent states of a game like chess, and we need to find the next move based on the graph exploration. During the exploration, we typically build a tree of already explored states as we explore the graph. We start in the initial state, which is the root node. Its children are the neighbors of the initial state. Other nodes are generated by getting neighbors of neighbors, and so on. This tree could be large or infinite, e.g., if the explored graph has cycles. -Without lazy evaluation, we usually generate the tree and simultaneously test conditions telling us when to stop the generating process. Using a lazily evaluated tree, we can first create the tree completely (even if it is infinite) and then traverse it to reveal the necessary portion of the tree nodes. Let us discuss an example using lazy evaluation to make it more concrete. +### Depth First Search (DFS) +For a more exciting example, consider a situation when our application needs to explore a graph of +some states or configurations. For instance, we can look for a path in a digraph leading to a goal +state, or the states can represent states of a game like chess, and we need to find the next move +based on the graph exploration. During the exploration, we typically build a tree of already +explored states as we explore the graph. We start in the initial state, which is the root node. Its +children are the neighbors of the initial state. Other nodes are generated by getting neighbors of +neighbors, and so on. This tree could be large or infinite, e.g., if the explored graph has cycles. + +Without lazy evaluation, we usually generate the tree and simultaneously test conditions telling us +when to stop the generating process. Using a lazily evaluated tree, we can first create the tree +completely (even if it is infinite) and then traverse it to reveal the necessary portion of the tree +nodes. Let us discuss an example using lazy evaluation to make it more concrete. -Suppose we are given a digraph to explore, i.e., we have a function generating neighbors of a node. Initially, we are in the state $1$, and we look for a path leading from $1$ to node $3$; see the picture below. Note that the edge between $1$ and $2$ is bidirectional. +Suppose we are given a digraph to explore, i.e., we have a function generating neighbors of a node. +Initially, we are in the state $1$, and we look for a path leading from $1$ to node $3$; see the +picture below. Note that the edge between $1$ and $2$ is bidirectional. ![](/img/digraph.png){ style="width: 80%; margin: auto;" } diff --git a/lectures/lecture04.pdf b/lectures/lecture04.pdf new file mode 100644 index 0000000..72693e7 Binary files /dev/null and b/lectures/lecture04.pdf differ diff --git a/lectures/lecture04.rkt b/lectures/lecture04.rkt index 6314d66..7f8142d 100644 --- a/lectures/lecture04.rkt +++ b/lectures/lecture04.rkt @@ -1,6 +1,7 @@ #lang racket (require rackunit) +(require plot) ;(require racket/stream) ;;; Lecture 4 - Lazy evaluation @@ -15,35 +16,18 @@ (define (my-lazy-if c a b) (if c (a) (b))) (my-lazy-if #t (thunk 0) (thunk (/ 1 0))) -; pass a thunk executed in a new thread -(thread - (thunk - (let ([x (foldl + 0 (range 0 10000000))]) - (displayln x)))) - ;;; Streams -; My stream application -(define (ints-from n) - (cons n (delay (ints-from (+ n 1))))) - -(define nats (ints-from 0)) -(force (cdr nats)) - -; Racket stream implementation -(define (stream-from n) - (stream-cons n (stream-from (+ n 1)))) - -(define stream-nats (stream-from 0)) -(stream->list (stream-take stream-nats 10)) +(time (stream-fold + 0 (in-range 1000000))) +(time (foldl + 0 (range 1000000))) -(define (log x) - (printf "Logging: ~a~n" x) - x) +(define ps (stream-cons (/ 1 0) (/ 1 0))) +;(stream-first ps) +(stream-rest ps) -(define st (stream (log 1) (log 2) (log 3))) -(stream-first st) -(stream-rest st) -(stream-first (stream-rest st)) +;;; Explicit definitions of streams +; naturals +(define (nats n) + (stream-cons n (nats (+ n 1)))) ; Return the first successful application of f on elements of lts (define (first-success f lst) @@ -53,44 +37,129 @@ (if (eqv? res #f) ; is the application successul? (first-success f (cdr lst)) ; no - try next element res ; yes - return it - ) - ) - ) - ) + )))) ; Lazy version (define (lazy-first-success f st) - (stream-first (stream-filter identity (stream-map f st))) - ) + (stream-first (stream-filter identity (stream-map f st)))) (define (test n) - ;(displayln n) - (if (> n 500) n #f) - ) + (if (> n 500) n #f)) (time (car (filter identity (map test (range 0 100000))))) (time (lazy-first-success test (range 0 100000))) - -;; Macros - -; lazy if -(define-syntax macro-if - (syntax-rules () - ((macro-if c a b) - (my-lazy-if c (thunk a) (thunk b))))) - -(macro-if (null? '()) '() (car '())) - -; list comprehension -(define-syntax list-comp - (syntax-rules (: <- if) - [(list-comp : <- ) - (map (lambda () ) )] - - [(list-comp : <- if ) - (map (lambda () ) - (filter (lambda () ) ))] - ) - ) - -(list-comp (+ x 2) : x <- '(2 3 5) if (>= 3 x)) + + +;;; Explicit definitions +(define (ints-from n) + (cons n (delay (ints-from (+ n 1))))) + +(define naturals (ints-from 0)) + +(define (repeat f a0) + (stream-cons a0 (repeat f (f a0)))) + + +;;; Implicit definitions +; constant stream +(define ones (stream-cons 1 ones)) +(stream->list (stream-take ones 10)) + +; cyclic streams +(define ab (stream-cons 'a (stream-cons 'b ab))) +(stream->list (stream-take ab 10)) + +(define abc (stream* 'a 'b 'c abc)) +(stream->list (stream-take abc 10)) + +; stream-append arguments are not delayed +; we can redefine it to define cyclic streams +(define (stream-append s1 s2) + (if (stream-empty? s1) + (force s2) + (stream-cons (stream-first s1) (stream-append (stream-rest s1) s2)))) + +(define week-days '(mon tue wed thu fri sat sun)) +(define stream-days (stream-append week-days (delay stream-days))) + +; summing infinite streams +(define (add-streams s1 s2) + (stream-cons (+ (stream-first s1) + (stream-first s2)) + (add-streams (stream-rest s1) + (stream-rest s2)))) + +; natural numbers defined implicitly +(define nats2 (stream-cons 0 (add-streams ones nats2))) +(stream->list (stream-take nats2 10)) + +;;; Eager Newtwon-Raphson + +(define eps 0.000000000001) +(define (mean . xs) (/ (apply + xs) (length xs))) +(define (next-guess n g) (mean g (/ n g))) +(define (good-enough? n1 n2 eps) (< (abs (- 1 (/ n1 n2))) eps)) + +; generation & termination are intertwined +(define (my-sqrt n [g 1.0]) + (define new-g (next-guess n g)) + (if (good-enough? g new-g eps) + new-g + (my-sqrt n new-g))) + +;;; Lazy Newton-Raphson +(define (within eps seq) + (define fst (stream-first seq)) + (define rest (stream-rest seq)) + (define snd (stream-first rest)) + (if (good-enough? fst snd eps) + snd + (within eps rest))) + +(define (lazy-sqrt n [g 1.0]) + (within eps (repeat (curry next-guess n) g))) + + +;;; Depth First Search +(struct arc (source target) #:transparent) +(struct node (data children) #:transparent) + +(define g (list (arc 1 2) (arc 2 1) + (arc 1 5) (arc 2 4) + (arc 4 5) (arc 5 6) + (arc 6 3) (arc 3 4))) + +(define (get-neighbors g v) + (map arc-target (filter (lambda (arc) (equal? v (arc-source arc))) g))) + +(define (make-tree get-successors v) + (define successors (get-successors v)) + (node v (stream-map (curry make-tree get-successors) successors))) + +(define t (make-tree (curry get-neighbors g) 1)) + +(define (get-ext-paths g path) + (define end (car path)) + (define neighbors (get-neighbors g end)) + (map (curryr cons path) neighbors)) + +(define t-en (make-tree (curry get-ext-paths g) '(1))) + +(define (filter-children pred tree) + (match tree + [(node data children) + (node data (stream-map (curry filter-children pred) + (filter pred (stream->list children))))])) + +(define t-en-f + (filter-children (compose not check-duplicates node-data) t-en)) + +(define (dfs goal? tree) + (match tree + [(node path children) + (if (goal? path) + (reverse path) + (ormap (curry dfs goal?) (stream->list children)))])) + +(dfs (compose (curry eqv? 3) car) t-en-f) +; => '(1 2 4 5 6 3)