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.
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:
- İçten dışa değerlendir. Her adımda listeyi yeniden yaz.
- car = ilk eleman, cdr = geri kalan liste, cdr her zaman liste döndürür (uygun girdide).
- cons ikinci argümanın başına ekler. İkinci argüman liste değilse noktalı çift.
- Özyineleme kalıbı: boş liste taban durum, car ile işle, cdr ile devam et.
- 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:
Lambda notasyonu isimsiz fonksiyon yazar: . Uygulama: . 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: , .
- 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.
| İfade | Değer | Not |
|---|---|---|
(+ 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? '()) | #t | Boş 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 LISP | Büyük, birçok LISP lehçesinin birleşimi; makrolar |
| ML | Statik tipli, type inference, sözdizimi Pascal’a yakın |
| Haskell | Saf (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 verisi | a fonksiyon sanılır; '(a b c) yaz |
eq? ile liste karşılaştırmak | equal? 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ç.
What is the value of the Scheme expression?
(car (cdr '(a b c)))Cevabı göster
İç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.
What are the values of these Scheme expressions?
(cdr '((a b) c d))
(car '((a b) c d))
(cdr '(x))Cevabı göster
İ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 ().
What are the values of these Scheme expressions?
(cons 'a '(b c))
(cons '(a) '(b))
(cons 'a 'b)Cevabı göster
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).
What is the value?
(car (car (cdr '(1 (2 3) 4))))Cevabı göster
(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.
What does (f 5) return?
(define (f n)
(if (= n 0)
1
(* 2 (f (- n 1)))))Cevabı göster
f(n) = 2 · f(n - 1), f(0) = 1. Yani . f(5) = 2 · 2 · 2 · 2 · 2 · 1 = 32. Fonksiyonel dillerde döngü yerine özyineleme kullanılır.
What is the value?
(map (lambda (x) (* x x)) '(1 2 3))Cevabı göster
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).
What is the value?
(let ((a 2) (b 3)) (+ a (* a b)))Cevabı göster
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).
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
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.
What is the value?
(define (compose f g) (lambda (x) (f (g x))))
((compose car cdr) '(1 2 3))Cevabı göster
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 .
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
İ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.
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
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.
What is the value?
(define (adder n) (lambda (x) (+ x n)))
(map (adder 3) '(1 2 3))Cevabı göster
(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ı.