2 puan yazan GN⁺ 2024-11-18 | 1 yorum | WhatsApp'ta paylaş

David Beazley ve SICP dersine dair notlar: 1 haftalık deneyim

2022'nin sonunda David Beazley'nin SICP dersine katılma deneyimini paylaşıyorum. Pek çok ücretsiz kaynak var, ancak Dave'in dersi belirli konuları seçip derinlemesine açıklayarak oldukça etkiliydi.

Başlangıç noktası

SICP dersi Scheme diliyle yürütüldü; burada ise temel kavram olan yerine koyma (substitution) modelini açıklamak için Python ile basit bir Scheme yorumlayıcısı geliştirildi.

Scheme dilinin temelleri

  • Primitive: temel değerler (ör. tamsayılar)
  • Operatörler: +, -, *, / gibi temel işlemler önek gösterimiyle kullanılır
  • define: değişken tanımı
> (define x 2)  
> (+ x 3) ; sonuç: 5  
  • if: koşul ifadesi
  • lambda: anonim fonksiyon tanımı
> ((lambda (x) (* x x)) 3) ; sonuç: 9  

Python'da Scheme yorumlayıcısı

Scheme kodunu değerlendiren basit bir yorumlayıcı Python kullanılarak geliştirildi. Temel işlemler Python fonksiyonları olarak tanımlandı.

definitions = {  
    "+": lambda x, y: x + y,  
    "*": lambda x, y: x * y,  
}  

Örnek:

> evaluate(("+", 2, 3)) # sonuç: 5  

Buna define ve lambdanın uygulanışı ile koşul ifadesi ifin işlenmesi de dahildi.

Yerine koyma modeli (Substitution Model)

Yerine koyma modeli, basit programları yorumlamanın bir yoludur; program değerlendirilirken değişkenler değerleriyle değiştirilir. Ancak atama (assignment) işin içine girdiğinde bu model başarısız olur.

Durum (State)

Yerine koyma modelinin bozulmasına örnek olarak atama (assignment) verilebilir. Örneğin bir banka hesabı bakiyesini modellenirken değişkeni güncellemek için set! kullanılır.

(define balance 100)  
  
(define (withdraw amount)  
  (set! balance (- balance amount))  
  balance)  

Bu durumda yerine koyma modeli, önceki ve sonraki bakiye durumlarını ayırt edemez.

Bu yüzden ortam (Environment) modeline ihtiyaç doğar. Değişkenler ortam içinde tanımlanır ve her prosedürün kendine ait bir ortamı vardır.

Akışlar (Streams)

Durumu modellemenin bir başka yolu da streams kullanmaktır. Streams, tembel değerlendirme (lazy evaluation) sayesinde gelecekteki değerleri de modelleyebilir.

Sonsuz döngü ve değerlendirme sırası

Değerlendirme sırasındaki fark: Çoğu dil, argümanları önce değerlendirerek applicative-order evaluation kullanır.

> (square (+ 1 2)) ; sonuç: 9  

Buna karşılık normal-order evaluation, argümanların değerlendirilmesini gerçekten ihtiyaç duyulana kadar erteler. Bu sayede sonsuz döngüden kaçınılabilir.

> (define (p) (p))  
> (define (test x y) (if (= x 0) 0 y))  
> (test 0 (p)) ; normal order'da 0 döner, applicative order'da sonsuz döngü  

Lambda kalkülüsü ve Church sayıları

Church encoding sayesinde sayılar prosedürler olarak ifade edilebilir. Bu, fonksiyonel programlamanın önemli kavramlarından biridir.

(define (zero f) (lambda (x) x))  
(define (increment n) (lambda (f) (lambda (x) (f ((n f) x)))))  
  • zero, argümanını olduğu gibi döndüren bir fonksiyondur (identity fonksiyonu).
  • increment, fonksiyon çağrısını bir kez daha uygular.

Örnek

> ((zero (lambda (x) (+ x 1))) 0) ; sonuç: 0  
> (((increment zero) (lambda (x) (+ x 1))) 0) ; sonuç: 1  

İterasyon vs özyineleme

Scheme, for döngüsü yerine tekrar eden işleri yapmak için özyineleme kullanır.

Özyineleme örneği: faktöriyel

(define (factorial n)  
  (if (= n 1)   
    1   
    (* n (factorial (- n 1)))))  

Bu özyinelemeli çağrı, stack kullandığı için fazla bellek tüketebilir.

Kuyruk çağrısı optimizasyonu (Tail-call optimization)

Scheme, tail-call optimization sayesinde bellek kullanımını azaltır. Bu da onun yinelemeli (iterative) bir süreç gibi çalışmasını sağlar.

(define (factorial n)  
  (define (iter product counter)  
    (if (> counter n)  
        product  
        (iter (* product counter) (+ counter 1))))  
  (iter 1 1))  

Kapanış

David Beazley'nin dersi, SICP'nin ana kavramlarını seçip derinlemesine ele alıyor. Özellikle fonksiyonel programlama, lambda kalkülüsü ve değerlendirme sırası gibi farklı programlama paradigmalarını anlamaya yardımcı oluyor.

Knuth'tan alıntı

Yalnızca teori çalışıyorsanız, bu pratik tarafa odaklanma zamanınızın geldiği anlamına gelir; yalnızca pratik yapıyorsanız da teoriye odaklanma zamanınızın geldiği anlamına gelir.

1 yorum

 
GN⁺ 2024-11-18
Hacker News yorumları
  • Uyaralım: SICP/Lisp/Scheme dünyasına derinlemesine girerseniz programlamayı düşünme biçiminiz değişebilir; böyle bir zihinsel uyarım da her zaman memnuniyetle karşılanır.
    Ancak bu fikirleri nesne yönelimli bir kod tabanına olduğu gibi uygularsanız çoğu zaman ters teper ya da ekip arkadaşlarının tepkisini çeker.
    Örneğin Lisp’ten sonra tüm for döngülerini forEach ile değiştirmek ya da her şeyi map/reduce zincirlerine çevirmek isteyebilirsiniz; ama dil fonksiyonel programlamayı tam anlamıyla benimsemiyorsa bu, hem okunabilirliğe hem de performansa zarar verebilir.
    Sonuçta değiştirilebilir bellek ve CPU’nun kodu işlediğini hatırlamak insanı zeminde tutar; günümüzde Church sayıları gibi soyut kavramlardan çok, veri odaklı tasarım ve donanım gerçeklerine göre “mekanik empati”nin günlük hayatta daha pratik olduğunu düşünüyorum.

    • Değişmezliğin daha güvenli olduğunu düşünüyorum.
      Yan etkiler ne kadar azsa o kadar öngörülebilir olur; nesne yönelimlilik de yalnızca fonksiyonların yeterli olduğu yerlerde bile sıkça kullanılıyor.
      Oyun geliştirmede çok sayıda hesaplama ve riskli kod olduğundan nesne yönelimlilik oldukça iyi oturur; ancak web geliştirmede fonksiyonel programlama çok daha doğal, SaaS’ta ise Elixir gibi diller daha güvenilir, daha az hatalı ve test etmesi daha kolay kod yazdırır.
    • Common Lisp Object System diye bir şey de yok mu?
      Üstelik forEach, map/reduce gibi şeyler Smalltalk koleksiyonlarında zaten vardı; birinci sınıf sözdizimi olmasa da Object Pascal ve C++’a da kopyalandılar.
      Alttaki bellek değiştirilebilir olduğu için, gerçekten gerektiğinde ML ailesi dillerde de mutasyonu ele alan mekanizmalar var.
    • CPU’nun denetim akışını uygulamasının tek yolu gotoya yakın olsa da, goto Lisp’ten bile daha az popüler.
      Daha tuhaf olan, modern CPU’larda performans için cache şart olmasına rağmen bellek hiyerarşisini birinci sınıf yurttaş olarak ele alan bir dilin hâlâ olmaması.
      Buna en yakın şey, alt çizgilerin deniz gibi taştığı Linux tarzı C gibi görünüyor.
  • Saf fonksiyonlarla durumu kodlamaya iyi bir giriş sunuyor.
    Aslında ağaçlar, tamsayılar, toplam/çarpım tipleri, görüntüler, monad’lar gibi her tür veri için çok daha fazla saf fonksiyonel kodlama var.
    Kodlama biraz kafa karıştırıcı olabilir ama aynı zamanda zarif ve küçüktür.
    Örneğin JavaScript ile Maybe monad’ını fonksiyonel biçimde uygularsanız şöyle olur:
    Nothing = nothing => just => nothing
    Just = v => nothing => just => just(v)
    pure = Just
    bind = mx => f => mx(mx)(f)
    evalMaybe = maybe => maybe("Nothing")(v => "Just " + v)
    console.log(evalMaybe(bind(Nothing)(n => pure(n + 1)))) // Nothing
    console.log(evalMaybe(bind(Just(42))(n => pure(n + 1)))) // Just 43

    • Böyle bir uygulama, ilgili tipin özyineleme ilkesinden türetilebilir.
      data Maybe a = Nothing | Just a
      foldMaybe :: (Unit -> r) -> (a -> r) -> Maybe a -> r
      foldMaybeye geçirilen iki yüksek dereceli fonksiyon sırasıyla Nothing ve Justa karşılık gelir.
      Yalnız Nothing tarafında, biraz daha doğru olmak için Unit parametresi eklenmiş bir biçim kullanılmıştır.
    • Bunu, endüktif bir tipi o tipin özyineleyici fonksiyon tipine dönüştürmek olarak görebiliriz.
      Tip teorisinde oldukça havalıdır ama pratik programlama için pek iyi değildir.
    • Matematiksel olarak zarif olabilir; fakat sıralama ya da içsel gerekçelendirme olmadan yalnızca ASCII karakter kümesinden oluşan bir dil üzerinden aktarıldığında görünüşte epey acı vericidir.
    • Kısaca, tipsiz lambda hesabı Turing-tamdır.
  • Eskiden gerçek SICP derslerini, yani MIT OCW’nin 1986 kayıtlarını izlemiştim
    Bilgi yoğunluğu yüksek diye sık sık övülür ama gerçekte öğrenci soru-cevapları, sınıfta “multimedya” sunum denemelerine eğitmenin dikkat çekmesi için harcanan zaman, tüm ders planı önceden tamamen netleşmediği için soru-cevapları proaktif biçimde engelleyemeyen kısımlar vb. yüzünden epey zaman boşa gider
    Tahtaya yazma süresi de birikince hatırı sayılır hale gelir
    Elbette materyalin sırası her türlü tartışılıp yeniden düzenlenebilir; bir gün bu içeriği kendi sezgilerime uygun biçimde anlatan bir video serisini bizzat hazırlamayı da planlıyorum
    Bu dersin Python gibi daha modern bir dil kullanırken köklerini koruyor gibi görünmesi sevindirici; ayrıca Python pratik bir çok-paradigmalı dil olduğu için, tam bir saflık olmasa da fonksiyonel idiomlar üzerinden sağladığı ifade gücünün insanlar tarafından yeterince takdir edilmediğini düşünüyorum

    • Bu ders, Python ile Scheme’i implemente ettikten sonra yeniden Scheme ile Scheme’i implemente ediyor
      Python’ın bu dersten çıkarılabileceğini ve çıkarılması gerektiğini düşünüyorum
      Python’ın fonksiyonel programlama desteği çok zayıf
      Listeler cons tabanlı değil, lambda’lar çok kısıtlı, pattern matching berbat ve ifade tabanlı da değil, ad alanları da garip
      Python’a modern bir dil demek de zor; 1990’larda kalmış durumda ve iyi bir C-API’si olduğu için ne yazık ki daha iyi dilleri feda ettirerek büyüdü
    • Python’ın fonksiyonel programlaması oldukça zayıf; bu açıdan JavaScript’in bile daha iyi olduğunu düşünüyorum
      Kuyruk çağrısı optimizasyonu olmadığı için bazı kod alıştırmaları Scheme’den tamamen farklı çözümler gerektiriyor
      1:1 çevrilmiş kod başarısız olursa eğitmen öğrencilere, seçilen dil yüzünden böyle çalışmadığını söyleyip sadece inanmalarını mı istemeli, yoksa her şeyi yığını dışsallaştırma problemi olarak görüp öyle mi çözmeli, emin değilim
      SICP’yi Python’a zorla uydurmaya çalışmak epey aptalca görünüyor
    • ArsDigita University’nin dersleri de var
      Site şu anda çevrimdışı ama dersleri archive.org’da görmek mümkün
      https://en.m.wikipedia.org/wiki/ArsDigita#ArsDigita_Foundati...
      https://archive.org/details/arsdigita_01_sicp/
      Tüm müfredatı içeren bir USB anahtar satıyorlardı; biri ISO’yu yüklese gerçekten harika olurdu
      https://web.archive.org/web/20190222145553/aduni.org/drives/
    • Python’ın ve Lisp ailesinden olmayan dillerin çoğunun sorunu, programı veri gibi ele almayı kolayca mümkün kılmamaları
      Scheme’de basitçe ifade edilen şeyler başka dillerde karmaşık alıştırmalara dönüşüyor
      Öğrenciler altta yatan kavramlara odaklanmak yerine, Scheme’de umursamak zorunda kalmayacakları uygulama dilinin ayrıntılarına odaklanıyor
  • cons/car/cdr’nin lambda ile implemente edildiğini ilk gördüğümde sihir gibi gelmişti
    Ama nihayetinde dil çalışma zamanının bir anahtar/değer sözlüğü implemente ettiğini ve bu implementasyonu ödünç alarak başka veri yapıları oluşturabileceğinizi gösterdiğini düşünüyorum

    • Elixir’in yapısöküm mantığının çok daha ilginç, ES6’daki zayıflatılmış sürümünün ise çok daha pratik olduğunu düşünüyorum
      Elixir’de istediğiniz kadar baştan çekip alabilirsiniz
    • Anahtar/değer sözlüğü değil, yalnızca pointer yeterli
      Davranışı olmayan bir closure, kapanmış değişkenlere giden pointer’dan ibarettir; 2 pointer’a sahip bir closure ise car ve cdr elde edilebilen bir çifttir
      Runtime, işaret edilen nesnenin tanımın dışından da kullanılabilmesini sağlamalıdır; bu yüzden escape analysis, garbage collection vb. gerekir, ama sözlük gerekmez
  • Yakınlarda teorem ispatında 0 != 1 gibi şeyleri ispatlamak için yalnızca Church encoding’in yetmediği, indüktif veri tipleri gerektiği kavramıyla karşılaştım
    Bununla ilgili şeyleri kabaca buraya koydum; SICP’ye yönelik ayrı bir eleştiri de içinde var: https://intellec7.notion.site/Drinking-SICP-hatorade-and-why...
    “Her şey sadece fonksiyondur” bakış açısının sınırlarını daha iyi anlamak istiyorum

    • Eşitsizlik hakkında kullanılabilecek başka somut olgular varsa 0 ≠ 1 ispatlanabilir gibi görünüyor
      f = g -> f x = g x teoreminden çıkarım yapıp sağ tarafta bir eşitsizlik olgusu oluşturabilir ve bunun karşıt tersini alabilirsiniz
      Church sayıları arasındaki eşitsizliğin, eşitsizliğe dair başka olgular olmadan doğrudan ispatlanamayacağını söylemek doğru görünüyor
      Buna karşılık indüktif veri tiplerinde ispat sistemi, aynı indüktif tipin iki somut örneğinde en dıştaki constructor uygulamasını özyinelemeli olarak kaldırarak eşitliği veya eşitsizliği doğrudan “gözlemleyebilir”
    • Teorem ispatı için Sigma tipleri ve Pi tipleri, ayrıca bir tür eşitlik kavramı gerekir
      Scott encoding veya Church encoding ile bunun başarıp başarılamayacağını merak ediyorum
    • Düzeltilmiş bağlantı: https://goosetaco.notion.site/Drinking-SICP-hatorade-and-why...
  • Kitabın kendisi zaten burada tartışılıyor: https://news.ycombinator.com/item?id=42157558
    Bağlantının o sayfanın başlangıcına değil de aşağıdaki tartışmaya gitmesinin bir nedeni var mı, merak ediyorum
    Bu yazı mevcut tartışmayla birleştirilemez mi diye düşünüyorum

  • David Beazley, Python dünyasında oldukça efsanevi bir isim; bu ders ilk başta şaşırtıcı bir fikir gibi görünse de yaklaşık 2 saniye düşününce kusursuz bir eşleşme gibi geldi ve bir sonraki derse kaydoldum.
    Esas nokta, bu biçimin gelecekte yazılım mühendisliği sürekli eğitiminin nasıl görüneceğine dair bir örnek olması.

      1. bölümdeki derleme konusu işlenmediyse SICP’nin en iyi kısmı işlenmemiş demektir.
  • “the substitution model” bölümündeki kodda bir yazım hatası var.
    ("+", ("fib", ("-", "n", 2)), ("fib", ("-", "n", 1))),
    Tanımlanan şey fibonacci; fib tanımlı değil, dolayısıyla iki fib çağrısı kesinlikle fibonacci olmalı.
    Gerçek GitHub deposundaki kod doğru: https://github.com/savarin/pyscheme/blob/0f47292c8e5112425b5...

  • SICP’nin harika olduğunu düşünüyorum.
    Ancak daha çok öğrendikçe ve daha fazla matematik çalıştıkça, ilişkilerin daha temel bir ilkel kavram olduğu sonucuna yaklaştım.
    Her fonksiyon, kısıtlı bir ilişki biçimi olarak ifade edilebilir; fakat tersi, ciddi ek mekanizmalar koymadan geçerli değildir.
    İlişkisel veritabanları ve SQL, ilişkisel programlamanın en bilinen ve başarılı örnekleri olsa da bu alanın hâlâ büyük ölçüde keşfedilmemiş olduğunu düşünüyorum.
    Şu anki ilgim, programlama dili tasarımından çok, çok küçük çocuklara matematiğin temellerini öğretmeye daha yakın.
    İlginçtir ki “büyük” gibi yüklemleri tekli ilişki, “daha büyük”ü ikili ilişki olarak öğretmek, aynı kavramı fonksiyon olarak yakalamaya çalışmaktan çok daha kolay.

  • “Her şey fonksiyondur” bakışını fazla basitleştirilmiş ve çoğu zaman pek yardımcı olmayan bir yaklaşım bulduğum için pek sevmiyorum.
    Örneğin cache’e, RAM’e, diske vb. sığmayan fonksiyonlar var; N-way JOIN ya da arama/eşleştirme gibi Big-O’su patlayan fonksiyonlar var; idempotent olmayan davranışlar dahil yan etkili fonksiyonlar var.
    Fonksiyonlara yönelik yan kanal saldırılarını düşünen neredeyse kimse yok.
    Tarih, saat, süre vb. şeylere bağlı deterministik olmayan fonksiyonlar da var; ayrıca fonksiyonlar çalışmanın ortasında başarısız olabilir ya da düzgün biçimde başarısız olamayabilir.
    Paylaşılan kaynak havuzu kullanan başka “fonksiyonları” etkileyen kaynaklar tüketmediklerini varsaymak da zor.
    Fonksiyon argümanları keyfi derecede büyük ya da karmaşık olabilir ama gerçek dünyada sınırlar var; o zaman işaretçilere, sonra da web ya da disk gibi uzak referanslara ihtiyaç duyuluyor.
    Ne zaman durmam gerektiğini söylerseniz iyi olur; devam edebilirim.

    • Aşırı basitleştirme bazen harika olabilir.
      Bu durumda lambda hesabı modeli, “her şey yalnızca fonksiyondur” yaklaşımının temelidir; çok basit olmasına rağmen Turing makinelerine vb. kıyasla kullanması ve hakkında akıl yürütmesi daha kolay olduğu için iyi bir hesaplama modelidir.
      Bu yüzden bilgisayar mantığının ve ispat sistemlerinin çoğunun temelinde yer alır.
    • Bunların çoğu fonksiyon olarak modellenebilir; yeter ki gerçekten o şekilde yazılsın.
      Bir fonksiyon bir kaynağa ihtiyaç duyuyorsa o kaynağı talep eder hâle getirilir; tarih/saate bağlıysa tarih/saate bağlı hâle getirilir; deterministik olmayan bir değer döndürüyorsa deterministik olmayan değer döndürür hâle getirilir.
      Fonksiyonel programlama yaklaşımının parladığı noktalardan biri, bu tür şeyleri ciddiye almaya zorlamasıdır.
      Örtük olarak paylaşılan bir kaynak kullanmak istiyorsanız onu modellemeniz gerekir; örtük paylaşılan kaynağa bağımlı bir “fonksiyon” da gerçek fonksiyondan açıkça ayrılır.
    • Burada fonksiyon dediğiniz şeylerin yarısı aslında fonksiyon değil; fonksiyonel programlama dünyasında da birçok kişi bunlara fonksiyon demez.
      Daha çok prosedür gibiler.
      Fonksiyon bir prosedürdür, ama her prosedür fonksiyon değildir.
    • Teorik amaçlar için bunun tek umut olduğunu düşünüyorum.