Skip to content

Latest commit

 

History

History
580 lines (387 loc) · 22.1 KB

2.NixLanguage-Exercises.md

File metadata and controls

580 lines (387 loc) · 22.1 KB

Nix Dili ile ilgili Alıştırmalar (Nix Öğreniyoru 2)

Bir önceki yazımızda Nix diline hızlı bir giriş yapmıştık. Nix dünyasını tanımaya çalıştık.

  1. NixOS: İşletim Sistemlerine Fonksiyonel Yaklaşım
  2. Nix Dili ve Özellikleri
  3. Nix Dili ile ilgili Alıştırmalar
  4. Nix Dilinde Builtins Fonksiyonlar
  5. Nix Paket Yöneticisi
  6. Nix Paket Yöneticisi Shell, Profile Kavram ve Komutları
  7. Nix Flake Nedir?
  8. Birden Çok Paketi Aynı Repo Üzeriden Yayınlamak
  9. Override ve Overlay Kavramları
  10. Nix Paket Yöneticisi ile Developer ve Profile Ortamları Oluşturmak
  11. Nix ile NixOs Konfigürasyonu
  12. NixOs Module ve Option Kullanımı
  13. NixOs Kurulumu ve Konfigürasyonu
  14. NixOs'u Cloud ve Uzak Ortamlara Deploy Etmek

Bu yazıda ilk makalede çok hızlı değindiğimiz Nix dilini aktif olarak kullanmak. Tamamen syntax ve dil özelliklerine değineceğiz. Herhangi bir kütüphane ve paket kullanmayacağız.

Belki ilk etapta Nix paket yöneticisini veya NixOs'u kullanırken baştan sonra sıfırdan bir şeyler yazmayacağız, büyük ihtimal bir çoğumuz için sadece NixOS'a paketlerimizi kurmak yeterli olacak. Ancak hazır kodları alıp kullanırken bile kodun ne yapmaya çalıştığını anlamak isteyeceksiniz.

Nix Kurulumu ve repl

NixOs dışında herhangi bir Linux distribution'da kurulum için sadece alttaki komutu çalıştırmak yetecektir.

sh <(curl -L https://nixos.org/nix/install) --daemon

Amacımız sadece dili biraz öğrenmek uzmanı olmak değil. Bir nix dosyasına baktığımızda onu anlamak, isteğimize göre değişiklikler yapabilmek. Kendi nix dosyalarımızı yazmaya başladığımızda zaten dilde ilerlemek mümkün olacak. Sıklıkla dokümanlara bakarak diğer kullanıcıların dosyalarını inceleyerek kendimizi geliştireceğiz.

Terminalde nix repl yazarak nix-repl uygulamasını çalıştırıyoruz. Bundan sonra altta bahsettiğimiz bütün komutları bu komut ekranında çalıştıracağız. Artık örnek uygulamalarla dili biraz daha pekiştirmeye çalışalım.

Nix File Kullanımı ve Örnek Uygulamalar

Yukarıdaki bazı başlıkları sadece okuyarak anlamak zorunda kaldık çünkü dosyaları dolayısıyla da modülleri kullanamadık

Nix-repl içindeyken :? komutu ile yardım alabiliriz.

:?
The following commands are available:

  <expr>                       Evaluate and print expression
  <x> = <expr>                 Bind expression to variable
  :a, :add <expr>              Add attributes from resulting set to scope
  :b <expr>                    Build a derivation
  :bl <expr>                   Build a derivation, creating GC roots in the
                               working directory
  :e, :edit <expr>             Open package or function in $EDITOR
  :i <expr>                    Build derivation, then install result into
                               current profile
  :l, :load <path>             Load Nix expression and add it to scope
  :lf, :load-flake <ref>       Load Nix flake and add it to scope
  :p, :print <expr>            Evaluate and print expression recursively
  :q, :quit                    Exit nix-repl
  :r, :reload                  Reload all files
  :sh <expr>                   Build dependencies of derivation, then start
                               nix-shell
  :t <expr>                    Describe result of evaluation
  :u <expr>                    Build derivation, then start nix-shell
  :doc <expr>                  Show documentation of a builtin function
  :log <expr>                  Show logs for a derivation
  :te, :trace-enable [bool]    Enable, disable or toggle showing traces for
                               errors
  :?, :help                    Brings up this help menu

:l komutunun bir nix doyasını (daha doğrusu bniz ifadesini) load ettiğini görebiliyoruz.

Repl'de biraz detaya girmek isterseniz şu sayfayı tavsiye ederim.

Öncelikle bir klasör oluşturalım ve içine default.nix adında bir dosya oluşturalım. Bir tane de functions.niz adında bir dosya oluşturalım. Bu dosyaları oluşturduktan sonra functions.nix dosyasına şu kodları yazalım.

Nix dilinde birçok builtin fonksiyon bulunmaktadır. Bunların birçoğunu zaten göreceğiz. Altta bunlardan bir kaçını görebilirsiniz. Ne yaptıkları belli olduğu için detaylarına girmiyorum. Zaten resmi sayfasından her biri hakkında detaylı bilgi alabilirsiniz.

# functions.nix
{ numbers ? [1 2], strings ? ["merhaba" "nasılsın"] }:

let
  sumNumbers = list: if builtins.length list  > 0 then builtins.foldl' (x: y: x + y) 0 list else 0;
  concatenateStrings = list: if builtins.length list  > 0 then builtins.concatStringsSep " " list else "";
in
{
  sumNumbers = sumNumbers numbers;
  concatenateStrings = concatenateStrings strings;
}

Üstteki kodda builtins.foldl' fonksiyonunun adı hakikaten böyle bu arada. Yani adında bir apostrof var.

  • builtins.foldl' fonksiyonu, bir listenin elemanlarına bir fonksiyon uygular
  • x: önceki toplam değeri, y: mevcut eleman
  • 0: başlangıç değeri

sumNumbers ise aslında bir fonksion ve parametre olarak bir liste alıyor. Örneğin alttaki örnekte 2 parametreli bir örnek görebiliriz.

# 2 parametreli örnek
toplama = a: b: a + b 

toplama 1 2

# sonuç
3

default.nix doyası içeriği de alttaki gibi olacak.

# default.nix

let
  functions = import ./functions.nix;
in
{
  result = functions { numbers = [1 2 3 4];   strings = ["Hello" " " "Nix"];};
}

Öncelikle repl içinden functions.nix dosyasını load edelim. öncelikle absolute path'i alıyoruz. daha sonra bunu import edip kullanıyoruz.

# path alınıyor
functionsPath = ./functions.nix

functions = import functionsPath

Ardından functions nesnesini çağıralım

functions { numbers = [1 2 3 4]; strings = ["Hello" " " "Nix"]; }

# ve sonuc

{ concatenateStrings = "Hello   Nix"; sumNumbers = 10; }

Burada şunu hatırlamamızda fayda var. Nix bir fonksiyonel programlama dili. Buda functions nesnesi aslında bir fonksiyon ve functions { numbers = [1 2 3 4]; strings = ["Hello" " " "Nix"]; } satırı ile bu fonksiyonu çağırmış oluyoruz. fonksiyon da bize bir set dönüyor ve içinde de concatenateStrings ve sumNumbers değişkenleri var.

functions.nix dosyasını biraz daha inceleyelim. sumNumbers = list: if builtins.length list > 0 then builtins.foldl' (x: y: x + y) 0 list else 0; bloğunu anlamak için alttaki gibi yazıyoruz.

# Fonksiyon tanımı
sumNumbers = list:

# Eğer liste boş değilse
if builtins.length list > 0 then
  # builtins.foldl' fonksiyonu, bir listenin elemanlarına bir fonksiyon uygular
  # x: önceki toplam değeri, y: mevcut eleman
  # 0: başlangıç değeri
  builtins.foldl' (x: y: x + y) 0 list
else
  # Eğer liste boşsa, toplam değeri 0 olarak döndür
  0;

Şimdi kaldığımız yerden yani default.nix dosyasıyla devam edelim.

repl içinden bu sefer default.nix dosyamızı yükleyelim.

:l ./default.nix

# sonuç
Added 1 variables.

Artık result değişkenimizin sonucunu görebiliriz

result
{ concatenateStrings = "Hello   Nix"; sumNumbers = 10; }

Daha basit örnek üzerinden devam edelim.

calculator.nix adından bir dosya oluşturup alttaki kodları kopyalayalım.

let
  numbers = {number1 = 10; number2 = 20;};
  sum = x: y: x + y;
  mul = x: y: x * y;

in
{
  sum = sum numbers.number1 numbers.number2;
  mul = mul numbers.number1 numbers.number2;
}

Daha sonra kodumuzu test etmek için nix repl komutu repl'i açıp alttaki kodları çalıştıralım.

calcPath = ./calculator.nix                

calc = import calcPath                     

calc
# sonuç
{ mul = 200; sum = 30; }

Bu haliyle calculator.nix dosyası sadece üzerindeki kodu çalıştırıyor. Yani kendi rakamlarımızla toplama veya çarpma yapamıyoıruz.

Şimdi bu kodu biraz daha geliştirelim. Örneğin calculator.nix dosyasını bir fonksiyon olarak yazalım. Soru işareti default değeri atamış oluyoruz.

# calculator.nix

{ number1 ? 10, number2 ? 20 }:

let
  sum = x: y: x + y;
  mul = x: y: x * y;
in
{
  sum = sum number1 number2;
  mul = mul number1 number2;
}

Artık yazdığımız fonksiyonu repl içinden çağırabiliriz.

calc = import ./calculator.nix

result = calc { number1 = 30; number2 = 40; }

result
# sonuç
{ mul = 1200; sum = 70; }

şimdi bu dosyayı main.nix dosyasından çağıralım. main.nix dosyası içeriği alttaki gibi olacak.

let
  calc = import ./calculator.nix;
  result = calc { number1 = 30; number2 = 40; };
in
{
  inherit result;
}

Inherit anahtar kelimesini hatırlarsanız let bloğundaki değişkenleri inherit ile dışarıya aktarabilmemizi sağlıyordu.

repl içinden main.nix dosyasını yükleyelim.

main = import ./main.nix

main.result
# sonuç
{ mul = 1200; sum = 70; }

Şimdi calculator modülümüzü biraz daha karmaşık hale getirelim. calcSet değişkeninden sonra gelen soru işareti eğer set için dışarıdan bir değer gelmezse default değerin atanmasını sağlıyor. Onlarda zaten sumSet ve numSet değişkenleri.

{ 
  calcSet ? { 
    sumSet = { numbers = { number1 = 10; number2 = 20; }; message = ""; }; 
    mulSet = { numbers = { number1 = 10; number2 = 20; }; message = ""; };
    }
}:
let
  sum = x: y: x + y;
  mul = x: y: x * y;
  
  

in
{
  sum = sum calcSet.sumSet.numbers.number1  calcSet.sumSet.numbers.number2;
  mul = mul calcSet.mulSet.numbers.number1  calcSet.mulSet.numbers.number2;
}

Artık veri yapımız iç içe set'lerden oluşuyor. Repl üzerinde alttaki komutlarla sonuçları görebiliriz.

Bu sefer calc fonksiyonumuza parametre geçmedik.

calc = import ./calculator.nix

result = calc {}
# sonuç
{ mul = 200; sum = 30; }

Şimdi örneği biraz daha geliştirelim. "Overriding" kavramını da nix dosyaları üzerinden görmüş olacağız.

Alttaki kodu calculator.nix dosyamıza kopyalıyoruz. Daha doğrusu önceki dosyamızın içeriğini alttaki kodla değiştiriyoruz.

İlk bakışta karışık geldiğinin farkındayım ama aslında çok basit bir mantığı var. Öncelikle ilk makalede bahsettiğimiz dile ait bir özelliği hatırlatmada fayda var. Nix dilinde bir değişkenin değeri bir kere atandıktan sonra değiştirilemez. Yani immutable. Bu durumda bir değişkenin değerini değiştirmek istediğimizde aslında yeni bir değişken oluşturuyoruz.

{ 
  calcSet ? { 
    sumSet = { numbers = { number1 = 10; number2 = 20; }; sumResult = 0; }; 
    mulSet = { numbers = { number1 = 10; number2 = 20; }; mulResult = 0; };
    }
}:
let
  sum = x: y: x + y;
  mul = x: y: x * y;
  
  sumResult = sum calcSet.sumSet.numbers.number1  calcSet.sumSet.numbers.number2;
  mulResult = mul calcSet.mulSet.numbers.number1  calcSet.mulSet.numbers.number2;
  result = calcSet // { sumSet = calcSet.sumSet // {sumResult = sumResult; }; mulSet = calcSet.mulSet // { mulResult = mulResult; };};

in
{
   result = result;
}

Örnekteki result = calcSet // { sumSet = calcSet.sumSet // {sumResult = sumResult; }; mulSet = calcSet.mulSet // { mulResult = mulResult; };}; satırı ile sumSet ve mulSet değişkenlerinin içindeki sumResult ve mulResult değişkenlerini değiştirmiş oluyoruz. "//" operatörünü hatırlarsanız sağdaki seti soldaki sete override ediyordu. Yani soldaki setin değerlerini sağdaki setin değerleriyle değiştiriyordu. Yada farklı etiketli veriler varsa bunlarda ekleniyordu. Bu satırda amacımız mulResult değerlerini değiştirmek dolayısıyla diğer bütün değerlerin kalmasını istiyoruz. Bu da diğer değerleri yeni oluşturduğumuz sete taşımamız gerektiği anlamına geliyor.

Kodumuzu nix-repl ile çalıştırıyoruz. Repl içindeyken alttaki komutlarla sonuçları görebiliriz.

calc = import ./calculator.nix
CalcResult = calc{}   
CalcResult.result   
# result { mulSet = { ... }; sumSet = { ... }; }
CalcResult.result.mulSet
# result { mulResult = 200; numbers = { ... }; }

Kodumuzu biraz daha değiştirip with anahtar kelimesinin kullanımına bakalım. With anahtar kelimesi kullanılan set'i bir scope olarak size set içindeki attribute'lara set'in adını tekrar tekrar yazmadan kullanmanıza olanak tanır. Alttaki örnek çok anlamlı gelmeyebilir. Ancak paket kullanımlarında ve NixOs konfigürasyonlarında bir çok paketin özelliğini değiştirmek durumunda olacağız. Bu gibi yerlerde aynı kelimeyi yüzlerce kez tekrar etmek hem vakit kaybı hem de okumayı zorlaştırıyor.

In bloğu içindeki resul değişkenine bakacak olursak with calcResult; satırı ile calcResult değişkenini scope olarak tanımlamış oluyoruz. Bu sayede sumResult ve mulResult değişkenlerinin başında calcResult kelimesini tekrar kullanmak zorunda kalmadık.

{ 
  calcSet ? { 
    sumSet = { numbers = { number1 = 10; number2 = 20; }; sumResult = 0; }; 
    mulSet = { numbers = { number1 = 10; number2 = 20; }; mulResult = 0; };
    }
}:
let
  sum = x: y: x + y;
  mul = x: y: x * y;
  
  calcResult = {
                  sumResult = with calcSet; sum sumSet.numbers.number1  sumSet.numbers.number2;
                  mulResult = with calcSet; mul mulSet.numbers.number1  mulSet.numbers.number2;
                  allResult = calcSet // { 
                                            sumSet = calcSet.sumSet // {sumResult = sumResult; }; 
                                            mulSet = calcSet.mulSet // { mulResult = mulResult;};
                                          };
                              
              };
in
{
   result = with calcResult;[sumResult mulResult (if sumResult>mulResult 
                                                    then "toplama sonucu çarpmadan daha büyük" 
                                                    else "çarpma sonucu toplamadan daha büyük")
                            ];
}

Sonucu görmek için repl içinden alttaki komutları çalıştırabiliriz.

calc = import ./calculator.nix

result = calc{}                

result.result

# sonuç [ 30 200 "çarpma sonucu toplamadan daha büyük" ]

Bu arada biraz araştırdığınızda diğer kullanıcıların yazmış oldukları nix dosyalarında with kullanımının çok yaygın olduğunu görebilirsiniz. Ancak genellikle paket kullanımı ile ilgili olduğunu göreceksiniz. Bunun dışında kullanımı çok tavsiye edilmiyor. En büyük sebepleri ise namespace yapısını bozması, hataya çok açık olması ve kodun okunabilirliğini azaltması diyebiliriz.

Diğer bilinmesi gereken bir konu da with'de scope'un dışı önceliklidir. Yani with anahtar sözcüğü set dışını override etmez. Örnekle daha iyi anlayacağız.

let
  myattrset = { a = 1; b = 2; };
in
  with myattrset; "a'nın değeri: ${toString a} ve b'nin değeri:  ${toString b}"

Bu yazımda myattrset kelimesini a ve b elemanlarının başına yazmadan kullanabildik. Buradan dönecek değer a'nın değeri: 1 ve b'nin değeri: 2 şeklinde olacaktır. Bu zaten beklediğimiz bi durum. Şimdi alttaki kodu inceleyelim.

let
  a = 3;
in
  with { a = 1; b = 2; }; "a'nın değeri: ${toString a} ve b'nin değeri:  ${toString b}"

Buranın sonucu ise a'nın değeri: 3 ve b'nin değeri: 2 olacaktır. Gördüğünüz üzere burada scop dışındaki a'nın değeri hala geçerlidir. Yani Nix dili shadow variable kullanımına izin vermez.

İlerleyen yazılarda veya incelemelerinizde with anahtar sözcüğünün paketlerle ilgili işlemlerde ne kadar çok kullanıldığını göreceksiniz. Community bunu çok sık kullanıyor çünkü paketlerle alakalı işlemlerde kodu daha okunabilir kılıyor ve kısaltıyor. Diğer bir fayda olarak let myValue = ...; with lib; doSomethingWith myValue gibi bir kod yazan developer doğal olarak myvalue değişkeninin lib içinde ezilmesini istemeyecektir.

Shadow variable kullanımına bir tek nested with kullanımında izin verilir. Alltaki örnekte sonuç inner olacaktır.

nix-repl> with { x = "outer"; }; with { x = "inner"; }; x
"inner"

Recursive List Kullanımı

Temel amacı set içindeki herhangi bir elementin set içindeki diğer elementlerle birlikte kullanılmasını sağlamaktır.

Ayrıca ağaç yapısında bir veri yapısı oluşturmak istediğimizde de kullanabiliriz.

basit bir örnekle başlayalım. Alttaki kodu rec.nix adında bir dosyaya kopyalayalım.

mySet tanımına bakacak olursanız d değişkenine ait veri a değişkeninin bir arttırılmasıyla oluşturuluyor.

let

mySet = rec {a= 1; b= 2; c= 3; d= a + 1;};

in
{
   inherit mySet;
}

kodu çalıştırmak için repl içinden alttaki komutları çalıştırabiliriz.

:l ./rec.nix 

# Added 1 variables.
 mySet
{ a = 1; b = 2; c = 3; d = 2; }

Daha karmaşık bir örnek yapalım. Bu sefer bir ağaç yapısı oluşturacağız. alttaki kodları family.nix adında bir dosyaya kopyalayalım.

Dikkat ederseniz bütün bireylerin soyadları lastname elementinden alınmış. Annenin yaşı babaya göre ve çocuğun yaşı da anneye göre hesaplanmış.

Ayrıca birbirleriyle oan ilişkileri de recursive list içindeki diğer element ile belirtilmiş. Örneğin baba elementi içindeki wife elementi annenin kendisi. Aynı şekilde anne elementi içindeki husband elementi babanın kendisi. Çocuk elementi içindeki mother elementi annenin kendisi ve father elementi babanın kendisi.

let

  family = rec {lastname = "Yılmaz";  
                father = {name="Mehmet"; lastname=lastname; age = 40; wife = mother; child = child;};
                mother = {name="Ayşe"; lastname=father.lastname; age = father.age - 3; husband = father; child = child;};
                child = {name="Ali"; lastname=mother.lastname; age = mother.age - 30; mother = mother; father = father;};
                };

in
{
   inherit family;
}

Örneğin babanın çocuğunun annesinin adını almak için alttaki komutu çalıştırabiliriz. Öncelikle repl

:l ./rec.nix

# result Added 1 variables.

family.father.child.mother.name

# result "Ayşe"

Bunu yapmamızı sağlayan Nix dilinin laziness olmasıdır. Bu sayede zaten dil içindeki bir değişkenin değeri hesaplanmadan başka bir değişkende kullanılabilir. Bu aynı zamanda Nix dilinin çok kuvvetli declarative bir dil olmasını sağlıyor.

Bu arada bu durum modüller import edilmesinde de geçerlidir. Yani aynı anda bir modül başka bir modülü import ederken import edilen modül de import edeni import edebilir. Böyle yazınca karmaşık geliyor ama aslında çok basit. Mesela "A.nix" dosyası "B.nix" doyasını import ederken "B.nix" dosyası da "A.nix" doyasını import edebilir.

Nix Dilinde Loop ve Recursive Fonksiyon Kullanımı

Nix dilinde bildiğimiz anlamda diğer dillerdeki gibi bir loop kavramı yoktur. Yani örneğin for, while, foreach gibi döngüleri kullanamayız. Ancak recursive fonksiyonlar kullanarak döngü mantığını oluşturabiliriz. Altta recursive fonksiyon yazılımını göreceğiz ancak bu tabii ki örneğin bir listedeki elemanları dolaşmamızı sağlamaz. Bunu için bir sonraki makalemizde paketlerin kullanımı ile birlikte loop kurgusunun da nasıl çalıştığını görmüş olacağız.

Bunun için basit bir örnek yapacağız. Amacımız verilen pozitif bir sayıya kadar bütün rakamların toplamını almak.

Bu sefer bir fonksiyon olarak tanımlayacağız.

{num? {num = 0;}}:
let
    sumAll = num1: if num1 < 1 then num1 else if  num1 > 1 then num1 + sumAll (num1 - 1) else 1;
in
{
result = sumAll num.num;
}

Test etmek için repl içinden alttaki komutları çalıştırabiliriz.

recursive = import ./recursive.nix
result = recursive(3)      
# result { result = 6; }

Formatter

Formetter'lar programlama dillerindeki ifadeleri düzenlemek ve biçimlendirmek için kullanılır. Bu, kodumuzun okunabilirliğini artırır ve genellikle kod tabanında tutarlılık sağlamak için kullanılır.

Nix ifadeleri, paketlerin ve sistem yapılandırmasının açıklanmasında kullanılan bir dil olduğundan, kodun anlaşılması ve yönetilmesi için düzenli bir biçimde tutulması önemlidir. Formatter, bu tür ifadeleri otomatik olarak biçimlendirerek, kod tabanının bakımını kolaylaştırır ve farklı geliştiriciler arasında tutarlılık sağlar. Bu, karmaşık sistem yapılandırmalarının veya paket setlerinin yönetilmesinde faydalıdır.

Formetter'lar daha çok community desteğiyle yürüyor. Resmi Github sayfası için şu linki ziyaret edebilirsiniz.

En çok kullanılan formatter ve en güncel tutulanı gördüğüm kadarıyla Alejandra görünüyor. Bir çok ide için plugin'leri bulunuyor. Nasıl kullanılacağına dair bilgiler sayfasında mevcut zaten. İkinci olarak da nixpkgs-fmt'a da bakabilirsiniz. ikissinin de VS Code için plugin'leri bulunuyor.

Tam olarak nasıl çalıştığını online görmek için şu sayfayı ziyaret edebilirsiniz.

Şimdilik bu kadar. Bir sonraki yazımızda artık Nix'in modüllerine bakacağız. Builtin fonksiyonlarının kullanımlarını göreceğiz.

Umarım faydalı olmuştur.

Kaynaklar