Değişebilirlik(Mutability) Suratınızda Patlayınca

December 2019 · 7 minute read

Sıradan bir iş günü. JavaScript’te tablolarımızdan birisine sayfalar arasında geçiş yaparken veri indir/kaldır yapma kabiliyeti ekliyorum (halk arasında buna "pagination" diyorlar). retrieveItems diye ve drawItems diye birer fonksiyon yazdım. retrieveItemsAndDraw yazıp çözüme ulaşmak üzereyken şu kodu yazdığımı farkediyorum:

const retrieveAndDrawItems = R.compose(drawItems, retrieveItems)

Ekip arkadaşlarımdan birisi bu satırda n’aptığımı sorduğunda "Basit!" diye cevaplıyorum:

Bu iki fonksiyonu birleştirip yeni bir fonksiyon oluşturuyorum.

De ki f(x) = 2x + 3 demek istiyorum. Benim de add3 ve mul2 isimli iki fonksiyonum var:

add3 = x => x + 3
mul2 = x => x * 2
compose2 = (f, g) => x => f(g(x))

f = compose2(add3, mul2) // f = x => add3(mul2(x))

Peki ya 2 fonksiyondan fazlasını birleştirmek istiyor isem? İşte compose bunu yapabiliyor!

Hemencecik kendimiz yazalım bu composeun nasıl olabileceğini:

fonksiyonları birleştirmeye calışan masum bir kod
compose = (...functions) => x0 => (functions
                                    .reverse() (1)
                                    .reduce((x, f) => f(x), x0)
                                  )
f = compose(add3, mul2)
1 compose(f, g)(x) demek g(f(x)) değil de f(g(x)) demek oldugu icin önce listeyi tersine çeviriyorum. Çünkü reduce sondan başa doğru değil, baştan sona doğru gidiyor.

Gördün mü?

Derken fonksiyonu bir daha çalıştırıyorum:
f(5) //16
Sonuç mu değişti?!
f(5) //13
f(5) //16
f(5) //13
f(5) //16
...

Bu compose fonksiyonu bozuk!

Peki siz sorunu farkettiniz mi?

Array'in reverse fonksiyonu, üzerinde çağırıldığı array'i değiştiriyor.
Yani reverse fonksiyonu mutative .

Mutativein, yani değiştirebilirlikin ne olduğunu açıklayacağım önce inşaa etmeye calıştığım bu bileşke fonksiyonunun ne olduğunu hatırlayalım.

Bileşke fonksiyon

Bileşke fonksiyonu matematikten hatırlıyorsunuz değil mi? Birden fazla fonksiyonu tek bir fonksiyon altinda birleştirmeye yarıyor. Bileşke, birleştirebilme (yani "composability"); fonksiyonel programlamada "modülerlik" elde edebilmek için yegane silahımız.

f,g,fog Matematikte
\$f(x) = x + 2\$

\$g(x) = 3x\$

\$(f@g)(x) = f(g(x)) = (3x) + 2\$
f,g,fog JavaScript’te
f = x => x + 2
g = x => x * 3
fog = x => f(g(x))

Bileşke fonksiyonlara Programming with Types adlı (henüz yayınlanmamış) yazımda daha detayli olarak değindim.

Problemleri fonksiyonlara parçalayıp, fonksiyonların birleşimi şeklinde çözümler üretmek fonksiyonel programlamadaki en temel yaklaşımlardan. Bu yüzdendir ki Haskell'de iki fonksiyonun bileşkesini almak . yazmak kadar kolay:

Haskell’de bileşke fonksiyon
fog = (f . g)

JavaScript’te ise bunu kendimiz inşaa etmek durumundayız:

f,g,fog JavaScript’te
//compose :: (('b -> 'c), ('a -> 'b)) -> 'a -> 'c  (1)
compose = (f, g) => x => f(g(x))

// f :: int -> int
f = x => x + 2
// g :: int -> int
g = g => x * 3
// fog :: int -> int
fog = compose(f, g)

// stringLength :: string -> int
stringLength = s => s.length
// nameOfStudent :: student -> string
nameOfStudent = s => s.name
// nameLengthOfStudent :: student -> int
nameLengthOfStudent = compose(stringLength, nameOfStudent)
1 Fonksiyonların girdi/çıktı tiplerini tanımlamak için kullandığım bu gösterim-şeklinden Programming with Types yazımda detaylıca bahtettim: nameLengthOfStudent örneğindeki gibi birleştirilen iki fonksiyonun girdi/çıktı tiplerinin birbiriyle uyum saglaması önemli. Ve compose'a verdiğimiz fonksiyonların tiplerini biliyorsak o zaman composeun bize döndüğü fonksiyonun tipini de bilebiliriz.

Hatta ayni şeyi OCaml’de yapıp compose'un tipini sorgulatacak olursam:

OCaml compose

OCaml’da bir f adindaki bir fonksiyonu x ve y argümanlarıyla cağırmak için f(x, y) değil f x y deriz.

+ ve - gibi işlemler x + 2 örneğindeki gibi iki argümanın arasına konulur. Eğer istersek biz de başa-konulan (prefix) fonksiyon yazabildiğimiz gibi araya-konulan (infix) fonksiyon yazabiliriz:

let (><) f g x = f (g x)
let f x = x + 2
let g x = x * 3
let fog = f >< g

Ama OCaml’da |> operatörü `compose`a tercih ediliyor:

let fog x = x |> g |> f

|> operatörünün JavaScript’e eklenmesi için bir öneri de var.

Kakoune’da imleci istediğim fonksiyonun üzerine getirip :lsp-hover dediğimde, o değerin tipini ve (var ise) tanımını görebiliyorum.

Ben de zaten 2’den fazla fonksiyonu birleştiren bir kodu yazmaya calışmış, ama mutative olduğu için sorun yaşamıştım. Şimdi de mutativein ne olduğunu irdeleyelim…​

Değiştirebilir (Mutative) Fonksiyon

Değiştirebilir (mutative) fonksiyon adi ne diyorsa onu yapiyor; değiştirebilir fonksiyon programın vaziyetini değiştir(ebil)iyor.

Vaziyet tanımını daha açık hale getirmek adına önce "fonksiyon"un tanımını yapmak lazım.

Ruby’de mutative fonksiyonlarin adlarının sonunda ! oluyor. Bir fonksiyonun adı reverse değil de reverse! ise biliyorsunuz ki bu fonksiyon bir yerlere müdahele ediyor.

Saf Fonksiyon

Matematikte (hatırlarsanız) fonksiyon bir kümeden bir kümeye giden bir bağıntıya denir.

fonksiyon semasi
Orneğin:A’dan B’ye giden bir fonksiyon

Bu fonksiyona fab ismini verip JavaScript’te yazalım:

const A2B = { a: 1, b: 2, c: 2 }
fab = x => A2B[x]

fab A’dan B’ye giden bir fonksiyon olduğu gibi, f(x) = 2x + 3 gibi tam sayılarda tanımlı bir fonksiyon da olabilir.

fonksiyon semasi
Örneğin:Z’den Z’ye giden f(x) = 2x + 3 fonksiyonu

fab ve f(x) = 2x + 3 gibi tek yaptığı bir kümeyi başka bir kümeye bağlamak olan fonksiyonlara programlamada saf denir. Saf fonksiyon tanımı yan etki açıklanınça daha anlaşılır olacak:

Yan Etki

fab fonksiyonu A kümesini B kümesine bağlamaktan fazlasını yapıyor olsun, mesela bir web sayfasına yazı yazsın:

fabAndEdit = x => {
	document.querySelector("#text").innerHTML = x
	return A2B[x]
}
yan etkili fonksiyon semasi
Örneğin:fabAndEdit

Bu innerHTML'nin atanışı gibi, girdi-çıktı bağıntısı dışında olan etkilere yan etki diyoruz.

Yan etki pröğramın vaziyetini değiştiren her şey olarak da düşünülebilir.

Vaziyet (state) pröğramın/bilgisayarın o anki halidir. Örneğin siz klavyede bir tuşa basınca bilgisayarin vaziyetini bir-tuşu-basılı-bilgisayar vaziyetine dönüştürürsünüz. Bir fonksiyonda bir metin-kutusuna (textbox) müdahele ederseniz artık programın vaziyeti değişmiştir, o metin-kutusunda evvelde yazmayan bir şey yazıyordur; başka bir kod o metin-kutusunun değerini degiştikten sonra okuyacak olursa, değiştikten önce okuyacağından başka bir değer bulacaktır.

  • Programın vaziyetine müdahele etmeyen fonksiyonlara saf fonksiyon denir.

  • Bir fonksiyon programın vaziyetine müdahele ediyor ise, bu müdaheleye yan etki denir.

  • Saf fonksiyonlarin yan etkisi yoktur. Yan etkisi olan fonksiyonlar saf değildir.

  • Saf fonksiyonlar ne zaman cağırılırsa çağırılsın, her zaman aynı girdi için aynı sonucu verir.

  • Değiştirebilir fonksiyonların yan etkileri vardır. Değiştirebilir fonksiyonlar saf değildir.

Değiştirebilir Fonksiyonlarin Zihinsel Yükü

Bir insanın short-term memory’sinde tutabileceği sey sayısı 7±2’dir

— George A. Miller
The Magical Number Seven

Zihinsel kapasitemiz sınırsız değil; zaten benim gibiyseniz bir kulağınızla podcast dinliyorsunuz, bir kulağınızla da "acaba benimle ilgili bir şey mi konuşuluyor" diye toplantı odasından gelen sesleri dinler iken kod yazıyorsunuz.

Bu haldeyken yazdığınız koddaki her bir fonksiyon cağrısının acaba programın vaziyetini nasıl değiştirdiğini, global değişkenlere nasıl müdahele ettiğini, verdiğiniz argümanlari değiştirip değiştirmedigini kendinize zihinsel yük edinmemekten kaçınmak gerek.

Kodunuzu olabildiğince saf yazmak, Yan etkileri (var ise) sınırlı kullanmak kodun yazılabilirliğini, okunabilirliğini ve sürdürülebilirliğini arttıracaktır.

(Zaten yan etkisi olmayan program olmaz; kullanıcıdan girdi almayan ve kullanıcıya çıktı vermeyen programın manası yoktur. Felsefe yapıp çıktısı olmayan programın var olmadığını bile iddia edebiliriz).

Değişebilirlik (Mutability), Değişemezlik (Immutability)

Aslında değiştirebilirlik ile iç içe bir kavram. JavaScript’teki Array ile aşağıda yazdığım ImmutableArray’in davranışını bi' karşılaştırın:

Array’in push fonksiyonunda programda varolan değerleri deşiştirir iken ImmutableArray'in push fonksiyonu varolan değerlere asla dokunmuyor. ImmutableArray gibi kendi vaziyetini koruyan nesnelere/değişken-tiplerine değişemez (immutable) deniyor. Değişemez değerler kullanıldıkları fonksiyonların saflaşmasını da teşvik ediyor.

Fonksiyonel programlama dillerinde değişemez değerleri kullanmak için çabalamaya gerek yok; doğal ve kolay bir şekilde destekleniyorlar.

Mesela OCaml’da imperative dillerden alışık olunan değişken yok. Zaten matematikteki değişken de aslında ismiyle çakışıyor; g = 9.8 dedikten sonra formülün içerisinde g'nin değerini değiştirmiyorsak değişken aslında değişken değildir, değil mi?

OCaml’da let a = 5 dendikten sonra a etiketi her zaman 5 değerine eşittir. let counter = ref 5 deyip counter ← !counter + 1 diyebilirsiniz.

Record kullanımında ise:

ocaml mutable records
type sex = Male | Female
type person = {name: string;
               age: int;
               sex: sex;
              }

let a_person = {name="Bruce"; age=60; sex=Male}
a_person.sex <- Female (* HATALI *)

let a_new_person = {a_person with name="Caitlynn"; sex=Female} (* OK *)

type changable_person = {mutable name: string;
                         age: int;
                         mutable sex: sex;
                        }
let a_person2 = {name="Bruce"; age=60; sex=Male}
a_person2.name <- "Caitlynn"
a_new_person.sex <- Female

Siz de immutable kod yazmak istiyorsanız ama ekibinizde JavaScript’i terkedip OCaml yazmak mümkün değilse immutableJS kütüphanesi sayesinde primitive değerler olmasa da liste, küme, map gibi veri tiplerini immutable kullanabilirsiniz.

"Yok istemem" diyorsanız da o zaman size bol clone()'lamalar, bol slice()'lamalar…​

Ee…​ Sonuç?

Sonuç basit, çalışan bir compose kodu:

compose
reverseList = l => l.slice().reverse()

compose = (...fs) => x0 => reverseList(fs).reduce((x, f) => f(x), x0)

Tabii bana R.compose'un n’olduğunu soran çocuk tüm bu hikayeyi dinlemeye kalmadı…​ Olsun, yarın kilitlerim ben onu aynı çok-bilmişlikle~