NodeJS'te Maybe Monad'ını Kullanarak REPL-Destekli Kod Yazma

July 2019 · 7 minute read

http://discuss.ocaml.org 'u açıyorum; uzun zamandır portfolyomu fonksiyonel yaklaşımlar ve diller konusunda zenginleştirmekteyim ve de aktif bir iş arayışındayım. İnternette bir OCaml ilanı var ise orada bulabileceğimden eminim.

Komşu başlıklardan birisinde "Closure’da REPL Driven Development" yapmanın ne kadar keyifli olduğunu, acaba OCaml’da da bunun mümkün olup olmadığı tartışılıyor.

I want to define “Repl based development” not as “occasionally use the REPL”, but something like this:

we start a REPL we type code in some editor, and continuously “send last expression” to the REPL, and we look at the new output eventually, we clean up the buffer a bit and store it as code

REPL Driven Development ne?
Piyasada benim bilmediğim yeni bir moda akımı mı var?
Derhal araştırmaya koyuluyorum…​

Read-Eval-Print-Loop

Herhangi bir "interpreted" dil ile kod yazmış iseniz, adina "REPL" dendiğini bilmeseniz de bir REPL arayüzüne denk gelmişsinizdir.

GIF
Ornegin: Python3 REPL

REPL programı:

  1. Kullanıcıdan bir satır kod alır (Read)

  2. Sonucunu hesaplar (Eval)

  3. Ekrana yazar (Print)

  4. 1’inci adıma dönerek sonsuz bir döngü oluşturur (Loop)

REPL programı (ctrl-c veya ctrl-d ile) sonlandırılıncaya kadar bu döngüde kalarak girdilerinizi hesaplar. Ama ben bu sistemi hesap makinesinden öte bir şey icin henüz kullanabilmiş değilim.

REPL ile Kod Geliştirmek

REPL’i yazılım geliştirme surecinin merkezine oturtmak icin JVM’e hapsolmuş bir LISP kullanmak zorunda değilsiniz (C dili icin bile REPL bulunabiliyor). LISP programcıları uzun bir süredir kodlarını (metodlarını) istedikleri şeyi yapana kadar REPL’de geliştirip sonra da bir dosyaya kaydediyorlar. npm install mocha demeden, kalite mühendisleri tepelerinde dikilmeden doğal bir şekilde Test-Driven-Development yapıyorlar. Bunu yaparken de REPL ile entegre bir kod geliştirme ortamından faydalanıyorlar.

Bir yazılımcının hangi kod geliştirme ortamini kullanacağı tamamen şahsi bir tercihtir; aynen sert zeminli bir ofiste işini yapmaya odaklanmış 20 mühendisin arasından günde 20 defa geçeceğini bile bile yumurta topuklu ayakkabı giymek gibi.

Siz kod yazmak için:

  1. Vim/NeoVim gibi bir metin düzenleyici kullanıyor olabilirsiniz,

  2. NetBeans gibi bir IDE kullanıyor olabilirsiniz,

  3. Emacs gibi bir işletim sistemi kullanıyor olabilirsiniz,

GIF
Ilgili XKCD

Ve de REPL’le entegre bir şekilde kod yazıp yazamayacağınız ortamınızın bunu destekleyip desteklemediğine bakar.

Ben birkaç hafta önce NeoVim'den Kakoune adında bir programa geçmiş, Kakoune’da :repl komutuna denk gelip ne yaptığını anlamamıştım.

Kakoune, "kakuun" diye okunur.

Kakoune/REPL Entegrasyonu

GIF
Ornegin: Kakoune’den Bash REPL’iyle konuşma
Bu arada Bash/Sh’nin de bir REPL olduğunu farkediyoruz

Kakoune’da bu işi yapabilmemizi sağlayan iki komut var:

  1. :repl yeni bir pencere pencere açar

  2. :send-text ise :repl komutuyla açılmış pencereyi bulup, seçilen metni ona yollar

Kakoune’un pencereleri pencere yöneticisi seviyesinde (X11) veya Tmux’un içinde olabiliyor. Kakoune hangi ortamda çalıştığını anlayıp ona göre davranıyor.

Ben :send-text yazmaya usendigim icin # tusunu :send-text komutuna kisayol atadim

kakrc
map global normal <#> ":send-text"

REPL destegiyle NodeJS yazmak

REPL’le nasıl konuşacağımı oğrendiğime göre, örnek bir problem çözerek REPL’i etkince kullanarak kod yazmayı denemek istiyorum. http://exercism.io 'da geldiğim son probleme bakalım:

Luhn doğrulama problemi

Kredi kartı vb. numaralarının geçerliligini bulmada kullanılıyor.+ Bir string alıp bunun Luhn algoritmasınca gecerli bir sayi olup olmadığını hesaplayacağız.

  1. String’in boyu 1’den az olamaz

  2. Boşluk olabilir ama hesaplamaya başlanmadan silinmelidir

    mesela bize verilen metin şu olsun: 4539 1488 0343 6467

    boşluklarını silelim: 4539148803436467

  3. Bunlar haricinde rakam dışında bir karakter olamaz

  4. En sağdan baslayıp her ikinci rakamı ikiyle carpın

  5. Carpım sonucunda 10 veya daha büyük bir sayı bulduysanız 9 çıkarın

    bu sayıyı elde ettik: 8569247803833437

  6. Tum rakamları toplayın

    sonuç 80

  7. Sonuç 10 ile bölunebiliyor ise geçerli bir sayıdır, yoksa değildir

Sizi bilmiyorum ama, ben iç içe (ya da peş peşe )bu kadar if else yazmak istemem. Hele de böylesine beni fonksiyonel yaz! diyen, once bunu yap, sonra bunu yap şeklinde bir algoritma var ise Maybe kullanmak isterim.

Maybe haznesi

Maybe; doğrulugundan emin olmadığınız değerleri içerisine koyup, uzerinde işlem yapabileceğiniz bir hazne.

Önce bir örnek gösterip sonra açıklamasını yapacağım.

Bir metod yazalım, bu metod bir sayıyla çağırılmış olabilir, veya hiçbir sey verilmeden çağırılmış da olabilir. Biz bunu 3’le çarpıp 2 ekleyelim…​

kod
const foo = num => {
  return Maybe.of(num)         (1)
              .map(x => x * 3) (2)
              .map(x => x + 2) (3)
              .withDefault(0)  (4)
}
1 num’un "truthy" olup olmadigina gore Just num veya Nothing'imiz olacak
2 map komutu Just x’i `Just 3x yapar, Nothing’e ise dokunmaz
3 Just 3x+2 veya Nothing
4 3x+2 veya 0

Maybe’ye koyduğunuz değerler ya Bir Sey olarak tutulur, ya da Hicbir Sey. Eğer Hicbir Sey'iniz var ise yapacak bir şey yok zaten. Ama eğer Bir Sey'iniz var ise o değeri metodlar vererek güncelleyebilirsiniz.+

  1. X a: içinde a olan X tipinde bir hazne demek. Orneğin Maybe int, List string gibi.

  2. Bir hazne içerisine başka bir hazne de konulabilir. Orneğin List (Maybe int) içinde Maybe int olan bir liste

Maybe/BirSey/HicbirSey üçlemesi farklı dillerde farklı isimlerde duyulabiliyor. Benim bildiklerim:

  1. OCaml’da option/Some/None

  2. Haskell’de Maybe/Just/Nothing

JavaScript’te Maybe ve benzeri fonksiyonel tanımları içeren kutüphaneler mevcut ama kendimizinkini yazmak da 2dk’mızı alacak.

JavaScript Maybe kodu
class Maybe {
  static of(x)   { return x
                    ? new Just(x)
                    : new Nothing() }
}
class Just extends Maybe {
  constructor(x) { super();
                   this.$value = x }
  map(f)         { return new Just( f(this.$value) ) }
  chain(f)       { return f(this.$value) }
  withDefault()  { return this.$value }
  toString()     { return `Just ${this.value}` }
}
class Nothing extends Maybe {
  map()          { return this }
  chain()        { return this }
  withDefault(x) { return x }
  toString()     { return "Nothing" }
}

NodeJS REPL’ine test değerleri yolluyorum…​

Unutmayayım diye bu denemeleri de koduma kopyalıyorum. İstediğim sonucu veriyor mu diye de kontrol edeceğim. Bunları koduma koyduğum için de bu kod yaşadığı sürece bu test yapılıyor olacak.

Test kodu
const fail = msg => { throw new Error(msg) }

new Just(4).map(x => x + 2).withDefault(0) == 6 || fail("Just map withDefault")
new Nothing().map(x => x + 2).withDefault(0) == 0 || fail("Nothing map withDefault")

Maybe’ye dair yapmak istediğim 1 adet şey kaldı.

Maybe.of Maybe’nin içerisine değer koymak için ideal bir yöntem değil. Bir değerin truthy olup olmamasından ötesiyle ilgileniyor olabiliriz. Onun için yardımcı bir metod yazacağım:

safe()
const safe = pred => x => (pred(x)
                            ? new Just(x)
                            : new Nothing())
safe(x => x > 5)(10) // Just 5
safe(x => x > 5)(0)  // Nothing

(Maybe.of metoduna da artık ihtiyacım kalmadı)

chain metodu farklı isimlerle anılabiliyor:

  1. chain

  2. flatMap

  3. bind

Kategori Teorisi konusunda bu tip dönüşümlere ayrıça değineceğim, ama şimdilik pratikte şu ikisinin farkını anlamanız şimdilik yeterli:

map vs chain
const isEven = safe(x => x % 2 == 0) (1)
const double = x => x * 2
new Just(4).map(double) (2)
new Just(4).map(isEven) (3)
new Just(4).chain(isEven) (4)
1 bir sayı alıp (x diyelim), eğer çift ise Just x, degil ise Nothing dönen bir metod
2 sonuç: Just 8
3 sonuç: Just (Just 4)
4 sonuç: Just 4

Luhn doğrulama - devam

Bundan sonrası oldukça basit. Tüm doğrulama adımlarını sırasıyla ekleyeceğim.

Koduma feature’lari ekledikçe onları REPL’e yollayarak test ediyorum. Bu test davranışlarını da koduma kaydediyorum.

Sonuç

luhn metodu
const sumList = l => l.reduce((a, b) => a + b, 0)

const luhn = str => (
  new Just(str)
    .map(x => x.split(""))
    .map(l => l.filter(x => x != " "))
    .map(l => l.map(x => parseInt(x)))
    .chain(safe(l => !l.includes(NaN)))
    .chain(safe(l => l.length > 1))
    .map(l => l.reverse())
    .map(l => l.map((x, i) => i % 2 == 1
                                ? (x * 2)
                                : x))
    .map(l => l.map(x => x >= 10
                           ? (x - 9)
                           : x))
    .map(sumList)
    .map(x => x % 10 == 0)
    .withDefault(false)
)

luhn("4539 1488 0343 6467") == true || fail("valid num")
luhn("1") == false || fail("too short")
luhn("123 a 456") == false || fail("has unknown chars")
sumList([1, 2, 3]) == 6 || fail("sumList 1 2 3")

Luhn Online REPL

Repl.it'i henüz keşfettim. Hem tüm kodu sizlerle paylaşmama, hem de sizlere kod ile oynama imkanı sunuyor.


Birkaç saat geçmiş, çoktan öğlen olmuş ve benim tek yapmak istediğim OCaml ilanlarına bakmak idi…​
"sonraki sefere artık" diyerek öğle yemeğine çıkacağım
Etkin-calışma-suresi tutan bir komut satırı programı yazmaya baslayıp yolun sonunda yine NodeJS’te Smalltalk’taki gibi ileti al-ver arayüzü inşaa ederken bulacagimiz bir sonraki yazıda görüşmek üzere…​