SEN2001 · 13. hafta · Functional Programming

Fonksiyonel programlama: Scheme, lambda, map ve özyineleme

Matematiksel fonksiyon fikri, LISP veri yapıları (atom, liste, cons hücresi), Scheme'de car/cdr/cons, define ve lambda, if/cond/let, özyinelemeli liste fonksiyonları, yüksek mertebeden fonksiyonlar (map, filter, compose) ve tail recursion.

functional programmingmathematical functionreferential transparencyside effectLISPSchemeatomlistS-expressioncons cellcarcdrconsquotelambda expressiondefinepredicate functioncondlethigher-order functionfunctional formmapfilterreducefunction compositionclosuretail recursion

Fonksiyonel programlama, programı matematiksel fonksiyonların birleşimi olarak görür. Değişken yok, atama yok, döngü yok; bir fonksiyon aynı argümanla her zaman aynı sonucu verir. Bu dersin fonksiyonel dili Scheme (LISP’in küçük, temiz bir lehçesi). Sınavda iki soru tipi çıkar: Scheme ifadesinin değerini bulmak (car, cdr, cons zincirleri) ve kısa bir özyinelemeli fonksiyonun ne döndürdüğünü izlemek.

Altın kurallar:

  1. İçten dışa değerlendir. Her adımda listeyi yeniden yaz.
  2. car = ilk eleman, cdr = geri kalan liste, cdr her zaman liste döndürür (uygun girdide).
  3. cons ikinci argümanın başına ekler. İkinci argüman liste değilse noktalı çift.
  4. Özyineleme kalıbı: boş liste taban durum, car ile işle, cdr ile devam et.
  5. Tırnak (') değerlendirmeyi durdurur.

Matematiksel fonksiyonlar

Bir matematiksel fonksiyon, tanım kümesinin her elemanını değer kümesinin bir elemanına eşler. Değerlendirme sırası yan etkilerle değil, yalnızca argümanlarla belirlenir:

cube(x)≡x⋅x⋅xcube(2)=8\text{cube}(x) \equiv x \cdot x \cdot x \qquad \text{cube}(2) = 8

Lambda notasyonu isimsiz fonksiyon yazar: λ(x) x⋅x⋅x\lambda(x)\, x \cdot x \cdot x. Uygulama: (λ(x) x⋅x⋅x)(2)=8(\lambda(x)\, x \cdot x \cdot x)(2) = 8. Scheme’in lambda’sı doğrudan buradan gelir.

Functional form (higher-order function): fonksiyonları parametre alan ya da fonksiyon döndüren fonksiyon. Üç klasik:

  • Composition: h≡f∘gh \equiv f \circ g, h(x)=f(g(x))h(x) = f(g(x)).
  • Apply-to-all: bir fonksiyonu listenin her elemanına uygula (map).
  • Construction: bir argümana bir fonksiyon listesini uygula, sonuçları listele.

Imperative dillerle fark: imperative programda değişkenler bellek hücresidir ve durum (state) değişir. Saf fonksiyonel programda durum yoktur; bu yüzden referential transparency sağlanır ve eşzamanlı çalıştırma (concurrency) kolaylaşır. Bedeli: von Neumann makinesinde verimlilik.

LISP veri yapısı

LISP’te (1958, John McCarthy) iki tür veri nesnesi var: atom (a, 17, hello) ve liste ((a b c), (a (b c) d)). Listeler içeride cons hücreleri zinciri olarak tutulur: her hücrenin iki alanı var, car (eleman) ve cdr (sonraki hücre). Son hücrenin cdr’si boş liste ().

(a (b c) d)

[a|•]──►[•|•]──►[d|/]
          │
          ▼
        [b|•]──►[c|/]

Program da aynı biçimde yazılır: (+ 2 3) bir listedir ve değerlendirilince 5 olur. Kod ile verinin aynı biçimde olması LISP’in en ayırt edici özelliği.

Scheme temelleri

Yorumlayıcı bir read-evaluate-print döngüsüdür. Değerlendirme kuralı: liste görünce ilk elemanı fonksiyon olarak al, diğer elemanları değerlendir, fonksiyonu uygula. Sayılar kendilerine değerlendirilir.

İfadeDeğerNot
(+ 2 (* 3 4))14Önek (prefix) yazım
'(a b c)(a b c)quote: değerlendirme
(car '(a b c))aİlk eleman
(cdr '(a b c))(b c)Geri kalan
(cons 'a '(b c))(a b c)Başa ekle
(list 'a (quote b) '(c))(a b (c))Argümanlardan liste
(null? '())#tBoş liste mi?

car, cdr, cons

(car (cdr '(a b c)))          ; b
(cdr '((a b) c d))            ; (c d)
(car '((a b) c d))            ; (a b)
(cdr '(x))                    ; ()
(cons '(a) '(b))              ; ((a) b)
(cons 'a 'b)                  ; (a . b)
(car (car (cdr '(1 (2 3) 4))))  ; 2
b
(c d)
(a b)
()
((a) b)
(a . b)
2

Son ifadenin izlemesi:

(cdr '(1 (2 3) 4))   →  ((2 3) 4)
(car ((2 3) 4))      →  (2 3)
(car (2 3))          →  2

TUZAK · cons ile list farkı. (cons '(a) '(b)) iki elemanlı ((a) b) verir; (list '(a) '(b)) ise ((a) (b)). cons ikinci argümanı liste olarak alıp başına ekler; list her argümanı ayrı eleman yapar. (car '()) ise hata: boş listenin ilk elemanı yok.

Fonksiyon tanımlama ve kontrol

(define pi 3.14159)                         ; isim bağlama
(define (square x) (* x x))                 ; fonksiyon
(define square2 (lambda (x) (* x x)))       ; aynısı, lambda ile

(if (> x 0) x (- x))                        ; iki yönlü seçim
(cond ((< x 0) 'neg)
      ((= x 0) 'zero)
      (else 'pos))                          ; çok yönlü seçim

(let ((a 2) (b 3)) (+ a (* a b)))           ; 8, yerel isimler

Predicate fonksiyonlar boolean döndürür (#t, #f): null?, eq?, equal?, pair?, number?, zero?. eq? iki sembolün ya da aynı nesnenin eşitliğini, equal? iki listenin yapısal eşitliğini sorar: (equal? '(1 (2)) '(1 (2))) → #t.

Özyinelemeli liste fonksiyonları

Döngü yok; her şey özyineleme. Kalıp hep aynı:

(define (f lst)
  (cond ((null? lst) <taban değer>)
        (else (<birleştir> (car lst) (f (cdr lst))))))
(define (sum lst)
  (if (null? lst) 0 (+ (car lst) (sum (cdr lst)))))
(sum '(3 5 7))                               ; 15

(define (member atm lis)
  (cond ((null? lis) #f)
        ((eq? atm (car lis)) #t)
        (else (member atm (cdr lis)))))
(member 'c '(a b c d))                       ; #t

(define (rev l)
  (if (null? l) '() (append (rev (cdr l)) (list (car l)))))
(rev '(1 2 3 4))                             ; (4 3 2 1)
15
#t
(4 3 2 1)

(sum '(3 5 7)) izlemesi:

(sum '(3 5 7)) = 3 + (sum '(5 7))
               = 3 + 5 + (sum '(7))
               = 3 + 5 + 7 + (sum '())
               = 3 + 5 + 7 + 0 = 15

İç içe listelerde car da bir liste olabilir; o zaman iki yöne özyinelenir:

(define (count-atoms l)
  (cond ((null? l) 0)
        ((pair? (car l)) (+ (count-atoms (car l)) (count-atoms (cdr l))))
        (else (+ 1 (count-atoms (cdr l))))))
(count-atoms '(a (b c) ((d)) e))             ; 5

Yüksek mertebeden fonksiyonlar

(map (lambda (x) (* x x)) '(1 2 3))          ; (1 4 9)

(define (filter p lst)
  (cond ((null? lst) '())
        ((p (car lst)) (cons (car lst) (filter p (cdr lst))))
        (else (filter p (cdr lst)))))
(filter (lambda (x) (> x 2)) '(5 1 4 2 3))   ; (5 4 3)

(define (compose f g) (lambda (x) (f (g x))))
((compose car cdr) '(1 2 3))                 ; 2

(define (adder n) (lambda (x) (+ x n)))
(map (adder 3) '(1 2 3))                     ; (4 5 6)

(define (twice f) (lambda (x) (f (f x))))
((twice (lambda (x) (* x 3))) 2)             ; 18
(1 4 9)
(5 4 3)
2
(4 5 6)
18

Aynı fikirler bugün imperative dillerde de var:

console.log([1, 2, 3].map((x) => x * x), [5, 1, 4, 2, 3].filter((x) => x > 2), [1, 2, 3, 4].reduce((a, b) => a * b, 1));
[ 1, 4, 9 ] [ 5, 4, 3 ] 24

Java 8 stream’leri (list.stream().map(...).filter(...)), Python’un map, filter ve lambda’ları, C#‘ın LINQ’i fonksiyonel programlamadan alınmıştır.

Tail recursion

(define (fact1 n) (if (= n 0) 1 (* n (fact1 (- n 1)))))
(define (fact2 n acc) (if (= n 0) acc (fact2 (- n 1) (* n acc))))
(fact2 5 1)                                  ; 120

fact1’de özyinelemeli çağrıdan sonra bir çarpma bekliyor: her çağrının ARI’si yığında kalmalı, n büyükse yığın taşar. fact2’de çağrı son iş: sonucu doğrudan döndürülür. Scheme bunu bir döngüye çevirir (yeni çağrı eski ARI’yi yeniden kullanır). Bir fonksiyonu tail recursive yapmanın yolu genelde bir accumulator parametresi eklemek.

Diğer fonksiyonel diller

DilÖzellik
Common LISPBüyük, birçok LISP lehçesinin birleşimi; makrolar
MLStatik tipli, type inference, sözdizimi Pascal’a yakın
HaskellSaf (yan etki yok), lazy evaluation: değer gerekene kadar hesaplanmaz, sonsuz listeler mümkün
F#.NET üzerinde ML ailesi

Sık yapılan hatalar

YanlışDoğrusu
(cdr '(a b c)) = b(b c); cdr liste döndürür
(cdr '(x)) = x()
(cons '(a) '(b)) = (a b)((a) b)
(cons 'a 'b) = (a b)(a . b), liste değil
Tırnaksız (a b c) liste verisia fonksiyon sanılır; '(a b c) yaz
eq? ile liste karşılaştırmakequal? kullan
Her özyineleme tail recursiveÇağrıdan sonra iş varsa değil
filter 2 > 2’yi tutar> katı; 2 elenir

Kendini test et

Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.

Soru 1kolay

What is the value of the Scheme expression?

(car (cdr '(a b c)))
Cevabı göster
b

İçten dışa: (cdr '(a b c)) ilk elemandan sonrasını verir: (b c). car bunun ilk elemanı: b. car = ilk eleman, cdr = geri kalan liste.

Soru 2kolay

What are the values of these Scheme expressions?

(cdr '((a b) c d))
(car '((a b) c d))
(cdr '(x))
Cevabı göster
(c d), (a b), ()

İlk eleman bir liste olabilir: (a b). car onu olduğu gibi döndürür; cdr geri kalanı: (c d). Tek elemanlı listenin cdr'si boş liste ().

Soru 3orta

What are the values of these Scheme expressions?

(cons 'a '(b c))
(cons '(a) '(b))
(cons 'a 'b)
Cevabı göster
(a b c), ((a) b), (a . b)

cons yeni bir cons hücresi yaratır: car kısmı birinci argüman, cdr kısmı ikinci. İkinci argüman liste ise sonuç başına eleman eklenmiş liste. (a) bir eleman olarak eklenir: ((a) b). İkinci argüman liste değilse sonuç liste değil, noktalı çift (dotted pair): (a . b).

Soru 4orta

What is the value?

(car (car (cdr '(1 (2 3) 4))))
Cevabı göster
2

(cdr '(1 (2 3) 4)) = ((2 3) 4). car → (2 3). car → 2. Bu tür sorularda içten dışa, her adımda listeyi yeniden yaz.

Soru 5kolay

What does (f 5) return?

(define (f n)
  (if (= n 0)
      1
      (* 2 (f (- n 1)))))
Cevabı göster
32

f(n) = 2 · f(n - 1), f(0) = 1. Yani 2n2^n. f(5) = 2 · 2 · 2 · 2 · 2 · 1 = 32. Fonksiyonel dillerde döngü yerine özyineleme kullanılır.

Soru 6kolay

What is the value?

(map (lambda (x) (* x x)) '(1 2 3))
Cevabı göster
(1 4 9)

lambda isimsiz bir fonksiyon: x'i alıp x · x döndürür. map bu fonksiyonu listenin her elemanına uygular ve sonuçları yeni bir listede toplar. Orijinal liste değişmez. JavaScript karşılığı [1, 2, 3].map(x => x * x).

Soru 7kolay

What is the value?

(let ((a 2) (b 3)) (+ a (* a b)))
Cevabı göster
8

let yerel isimleri değerlere bağlar (değişken değil, değiştirilemez isimler). a = 2, b = 3 → 2 + 2 · 3 = 8. let aslında bir lambda uygulamasının kısaltmasıdır: ((lambda (a b) (+ a (* a b))) 2 3).

Soru 8orta

Using the function below, what is (filter (lambda (x) (> x 2)) '(5 1 4 2 3))?

(define (filter p lst)
  (cond ((null? lst) '())
        ((p (car lst)) (cons (car lst) (filter p (cdr lst))))
        (else (filter p (cdr lst)))))
Cevabı göster
(5 4 3)

cond koşulları sırayla dener. Liste boşsa (). İlk eleman p'yi sağlıyorsa onu kalanın filtrelenmiş haline cons'la; sağlamıyorsa atla. 5 ✔, 1 ✘, 4 ✔, 2 ✘ (2 > 2 değil), 3 ✔ → (5 4 3). Sıra korunur.

Soru 9zor

What is the value?

(define (compose f g) (lambda (x) (f (g x))))
((compose car cdr) '(1 2 3))
Cevabı göster
2

compose bir functional form (yüksek mertebeden fonksiyon): iki fonksiyon alır, yeni bir fonksiyon döndürür. (compose car cdr), x'e önce cdr sonra car uygular: cdr → (2 3), car → 2. Matematikteki (f∘g)(x)=f(g(x))(f \circ g)(x) = f(g(x)).

Soru 10zor

What does (count-atoms '(a (b c) ((d)) e)) return?

(define (count-atoms l)
  (cond ((null? l) 0)
        ((pair? (car l)) (+ (count-atoms (car l)) (count-atoms (cdr l))))
        (else (+ 1 (count-atoms (cdr l))))))
Cevabı göster
5

İlk eleman bir listeyse hem içine hem kalana özyinele; atomsa 1 say ve devam et. Atomlar: a, b, c, d, e → 5. Bu "car ve cdr üzerinde çift özyineleme" kalıbı iç içe listelerde standarttır. Sadece en üst seviyeyi sayan len 4 verirdi.

Soru 11orta

Which of these two factorial definitions is tail recursive, and why does it matter?

(define (fact1 n) (if (= n 0) 1 (* n (fact1 (- n 1)))))
(define (fact2 n acc) (if (= n 0) acc (fact2 (- n 1) (* n acc))))
Cevabı göster
fact2; the recursive call is the last operation, so Scheme can run it in constant stack space

fact1'de özyinelemeli çağrı döndükten sonra hâlâ bir çarpma yapılacak, her çağrının ARI'si saklanmalı. fact2'de çağrının sonucu doğrudan döndürülür; yapacak iş kalmadığı için yeni ARI eskisinin yerini alabilir (döngü gibi). Scheme standardı tail call'ların böyle uygulanmasını zorunlu tutar. (fact2 5 1) = 120.

Soru 12orta

What is the value?

(define (adder n) (lambda (x) (+ x n)))
(map (adder 3) '(1 2 3))
Cevabı göster
(4 5 6)

(adder 3) bir closure döndürür: x'e 3 ekleyen fonksiyon, n = 3'ü yanında taşır. map onu her elemana uygular. Fonksiyonların değer olarak döndürülebilmesi ve parametre olarak geçilebilmesi (first-class functions) fonksiyonel programlamanın temeli.

Doğrulama: Scheme kurulu olmadığı için node ile küçük bir Scheme yorumlayıcısı yazıldı (verify/SEN2001/lib/scheme.mjs: quote, lambda, define, if, cond, let, let*, car/cdr/cons/list, null?, eq?, equal?, map, apply) ve sayfadaki her Scheme ifadesi bu yorumlayıcıda çalıştırıldı (verify/SEN2001/13.mjs). Liste yazımı Scheme'in standart çıktı biçimine göredir. JavaScript karşılıkları node ile çalıştırıldı.

Deftere not kartı

saf fonksiyonel: değişken yok, atama yok, döngü yok → özyineleme aynı argüman → hep aynı sonuç (referential transparency), yan etki yok LISP veri: atom ya da liste. liste = cons hücreleri zinciri, son cdr = () 'x = (quote x): değerlendirme, olduğu gibi al (car '(a b c)) = a (cdr '(a b c)) = (b c) (cdr '(x)) = () (cons 'a '(b c)) = (a b c) (cons '(a) '(b)) = ((a) b) (cons 'a 'b) = (a . b) (list 'a 'b '(c)) = (a b (c)) (define (f x) gövde) (lambda (x) gövde) (let ((a 1)) gövde) if: (if test then else) cond: ((test sonuç) ... (else sonuç)) predicate: null? eq? equal? pair? number? zero? (#t / #f) eq? sembol/aynı nesne; equal? yapı karşılaştırır özyineleme kalıbı: (null? l) taban, (car l) ile işle, (cdr l) ile devam map: her elemana uygula; filter: koşulu sağlayanları tut; compose: f(g(x)) tail recursion: çağrı en son iş → sabit yığın. accumulator parametresi kullan