Skip to content

Monad'lar: Zincirlenebilir İş Akışlarını Güvenli Kurmak

Published: at 11:22 PMSuggest an edit

Monad konusu çoğu zaman gereğinden soyut anlatılıyor. Oysa pratik tarafta mesele daha basit: değeri bir bağlamın içinde taşımak ve zincirlenen adımları her aşamada aynı kuralla ilerletmek. Bu yazıda monad fikrini teorik tanımdan çok kullanım biçimi üzerinden ele alacağım.

Monad Nedir?

Basitçe söylemek gerekirse, monad bir şeyleri wrap eden ve o wrapper üzerinde güvenli işlemler yapmamızı sağlayan desendir.

Düşünün ki elimizde bir kutu var. Bu kutunun içinde bir değer olabilir, olmayabilir, hata olabilir, gelecekte bir değer olabilir… Monad, bu kutuyla nasıl çalışacağımızı tanımlayan kurallar bütünüdür.

// Bu basit bir "Maybe" monad'ı
class Maybe {
  constructor(value) {
    this.value = value;
  }

  static of(value) {
    return new Maybe(value);
  }

  isNothing() {
    return this.value === null || this.value === undefined;
  }

  map(fn) {
    return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this.value));
  }

  flatMap(fn) {
    return this.isNothing() ? Maybe.of(null) : fn(this.value);
  }
}

// Kullanım
const safeNumber = Maybe.of(42)
  .map(x => x * 2)
  .map(x => x + 10);

console.log(safeNumber.value); // 94

const unsafeNumber = Maybe.of(null)
  .map(x => x * 2) // Hata vermez!
  .map(x => x + 10); // Hata vermez!

console.log(unsafeNumber.value); // null

Monad Laws: Kurallara Uyalım

Bir şeyin monad olabilmesi için üç temel kurala uyması gerekir:

1. Left Identity Law

// M.of(value).flatMap(fn) === fn(value)

const value = 42;
const fn = x => Maybe.of(x * 2);

const left = Maybe.of(value).flatMap(fn);
const right = fn(value);

// left.value === right.value // true

2. Right Identity Law

// m.flatMap(M.of) === m

const m = Maybe.of(42);
const result = m.flatMap(Maybe.of);

// result.value === m.value // true

3. Associativity Law

// m.flatMap(fn1).flatMap(fn2) === m.flatMap(x => fn1(x).flatMap(fn2))

const m = Maybe.of(42);
const fn1 = x => Maybe.of(x * 2);
const fn2 = x => Maybe.of(x + 10);

const left = m.flatMap(fn1).flatMap(fn2);
const right = m.flatMap(x => fn1(x).flatMap(fn2));

// left.value === right.value // true

Popüler Monad Türleri

1. Maybe/Optional Monad

Null/undefined değerlerle güvenli çalışmak için:

class Maybe {
  constructor(value) {
    this.value = value;
  }

  static of(value) {
    return new Maybe(value);
  }

  static nothing() {
    return new Maybe(null);
  }

  isNothing() {
    return this.value === null || this.value === undefined;
  }

  map(fn) {
    return this.isNothing() ? Maybe.nothing() : Maybe.of(fn(this.value));
  }

  flatMap(fn) {
    return this.isNothing() ? Maybe.nothing() : fn(this.value);
  }

  getOrElse(defaultValue) {
    return this.isNothing() ? defaultValue : this.value;
  }
}

// Güvenli property access
function safeGet(obj, path) {
  return path.split(".").reduce((current, key) => {
    return current.flatMap(val =>
      val && val[key] !== undefined ? Maybe.of(val[key]) : Maybe.nothing()
    );
  }, Maybe.of(obj));
}

const user = {
  profile: {
    address: {
      city: "Istanbul",
    },
  },
};

const city = safeGet(user, "profile.address.city");
console.log(city.getOrElse("Unknown")); // "Istanbul"

const country = safeGet(user, "profile.address.country");
console.log(country.getOrElse("Unknown")); // "Unknown"

2. Either Monad

Error handling için:

class Either {
  constructor(value, isLeft = false) {
    this.value = value;
    this.isLeft = isLeft;
  }

  static left(value) {
    return new Either(value, true);
  }

  static right(value) {
    return new Either(value, false);
  }

  map(fn) {
    return this.isLeft ? this : Either.right(fn(this.value));
  }

  flatMap(fn) {
    return this.isLeft ? this : fn(this.value);
  }

  fold(leftFn, rightFn) {
    return this.isLeft ? leftFn(this.value) : rightFn(this.value);
  }
}

// Safe division
function safeDivide(x, y) {
  return y === 0 ? Either.left("Division by zero") : Either.right(x / y);
}

const result = safeDivide(10, 2)
  .flatMap(x => safeDivide(x, 2))
  .map(x => x * 3);

result.fold(
  error => console.log(`Error: ${error}`),
  value => console.log(`Result: ${value}`) // "Result: 7.5"
);

3. IO Monad

Side effects’i kontrol etmek için:

class IO {
  constructor(effect) {
    this.effect = effect;
  }

  static of(value) {
    return new IO(() => value);
  }

  map(fn) {
    return new IO(() => fn(this.effect()));
  }

  flatMap(fn) {
    return new IO(() => fn(this.effect()).effect());
  }

  run() {
    return this.effect();
  }
}

// Side effect'leri defer etmek
function readFile(filename) {
  return new IO(() => {
    console.log(`Reading file: ${filename}`);
    return `Content of ${filename}`;
  });
}

function writeFile(filename, content) {
  return new IO(() => {
    console.log(`Writing to file: ${filename}`);
    return `Written: ${content}`;
  });
}

// Composition without executing
const fileOperation = readFile("input.txt").flatMap(content =>
  writeFile("output.txt", content.toUpperCase())
);

// Execute when ready
console.log("Setting up operations...");
const result = fileOperation.run(); // Actual side effects happen here

4. State Monad

State’i functional şekilde yönetmek için:

class State {
  constructor(runState) {
    this.runState = runState;
  }

  static of(value) {
    return new State(state => [value, state]);
  }

  static get() {
    return new State(state => [state, state]);
  }

  static put(newState) {
    return new State(state => [null, newState]);
  }

  map(fn) {
    return new State(state => {
      const [value, newState] = this.runState(state);
      return [fn(value), newState];
    });
  }

  flatMap(fn) {
    return new State(state => {
      const [value, newState] = this.runState(state);
      return fn(value).runState(newState);
    });
  }

  run(initialState) {
    return this.runState(initialState);
  }
}

// Stack operations
function push(item) {
  return State.get().flatMap(stack => State.put([item, ...stack]));
}

function pop() {
  return State.get().flatMap(stack => {
    if (stack.length === 0) {
      return State.of(null);
    }
    const [head, ...tail] = stack;
    return State.put(tail).map(() => head);
  });
}

// Stack operations composition
const stackOperations = push(1)
  .flatMap(() => push(2))
  .flatMap(() => push(3))
  .flatMap(() => pop())
  .flatMap(() => pop());

const [result, finalStack] = stackOperations.run([]);
console.log("Popped value:", result); // 2
console.log("Final stack:", finalStack); // [1]

Real-World Examples

Promise’ler (Future Monad)

JavaScript’te zaten kullandığımız monad’ların en yaygını:

// Promise is a monad!
const fetchUser = id => Promise.resolve({ id, name: `User ${id}` });
const fetchPosts = userId =>
  Promise.resolve([
    { id: 1, title: "Post 1", userId },
    { id: 2, title: "Post 2", userId },
  ]);

// Monadic composition
fetchUser(1)
  .then(user => fetchPosts(user.id)) // flatMap equivalent
  .then(posts => posts.map(post => post.title)) // map
  .then(titles => console.log(titles));

// With async/await (syntactic sugar for monad)
async function getUserPosts(id) {
  const user = await fetchUser(id);
  const posts = await fetchPosts(user.id);
  return posts.map(post => post.title);
}

Array’ler (List Monad)

Array’ler de aslında monad:

// Array as a monad for non-deterministic computation
const numbers = [1, 2, 3];

// map (functor operation)
const doubled = numbers.map(x => x * 2);

// flatMap (monadic bind)
const pairs = numbers.flatMap(x => numbers.map(y => [x, y]));

console.log(pairs);
// [[1,1], [1,2], [1,3], [2,1], [2,2], [2,3], [3,1], [3,2], [3,3]]

// Non-deterministic computation
function nonDeterministicAdd(x) {
  return [x + 1, x + 2, x + 3];
}

const result = [5].flatMap(nonDeterministicAdd).flatMap(nonDeterministicAdd);

console.log(result); // [7, 8, 9, 8, 9, 10, 9, 10, 11]

Monad Transformers

Monad’ları compose etmek için:

// MaybeT (Maybe transformer)
class MaybeT {
  constructor(computation) {
    this.computation = computation;
  }

  static lift(monad) {
    return new MaybeT(monad.map(Maybe.of));
  }

  static of(value) {
    return new MaybeT(Promise.resolve(Maybe.of(value)));
  }

  map(fn) {
    return new MaybeT(this.computation.then(maybe => maybe.map(fn)));
  }

  flatMap(fn) {
    return new MaybeT(
      this.computation.then(maybe =>
        maybe.isNothing()
          ? Promise.resolve(Maybe.nothing())
          : fn(maybe.value).computation
      )
    );
  }

  run() {
    return this.computation;
  }
}

// Usage: combining Promise and Maybe
async function safeAsyncOperation(id) {
  const userMaybe = await MaybeT.of(id)
    .flatMap(id => MaybeT.lift(fetchUser(id)))
    .flatMap(user => MaybeT.lift(fetchUserProfile(user.id)))
    .map(profile => profile.email)
    .run();

  return userMaybe.getOrElse("No email found");
}

Do Notation (Syntactic Sugar)

Monad chain’lerini daha okunabilir hale getirmek için:

// Custom do notation implementation
function doM(generatorFn) {
  return function (...args) {
    const generator = generatorFn(...args);

    function step(value) {
      const { value: monad, done } = generator.next(value);

      if (done) {
        return monad;
      }

      return monad.flatMap(step);
    }

    return step();
  };
}

// Usage
const calculation = doM(function* () {
  const x = yield Maybe.of(10);
  const y = yield Maybe.of(20);
  const z = yield Maybe.of(30);

  return Maybe.of(x + y + z);
});

console.log(calculation().value); // 60

// With error handling
const safeCalculation = doM(function* () {
  const x = yield safeDivide(10, 2); // Right(5)
  const y = yield safeDivide(x, 0); // Left("Division by zero")
  const z = yield safeDivide(y, 2); // Won't execute

  return Either.right(z * 2);
});

safeCalculation().fold(
  error => console.log(`Error: ${error}`),
  value => console.log(`Result: ${value}`)
);

Performans İyileştirmeleri

1. Lazy Evaluation

class LazyMaybe {
  constructor(valueThunk) {
    this.valueThunk = valueThunk;
    this._computed = false;
    this._value = null;
  }

  static of(value) {
    return new LazyMaybe(() => value);
  }

  get value() {
    if (!this._computed) {
      this._value = this.valueThunk();
      this._computed = true;
    }
    return this._value;
  }

  map(fn) {
    return new LazyMaybe(() => {
      const val = this.value;
      return val === null || val === undefined ? null : fn(val);
    });
  }

  flatMap(fn) {
    return new LazyMaybe(() => {
      const val = this.value;
      return val === null || val === undefined ? null : fn(val).value;
    });
  }
}

2. Memoization

class MemoizedIO {
  constructor(effect) {
    this.effect = effect;
    this._memoized = false;
    this._result = null;
  }

  static of(value) {
    return new MemoizedIO(() => value);
  }

  map(fn) {
    return new MemoizedIO(() => fn(this.run()));
  }

  flatMap(fn) {
    return new MemoizedIO(() => fn(this.run()).run());
  }

  run() {
    if (!this._memoized) {
      this._result = this.effect();
      this._memoized = true;
    }
    return this._result;
  }
}

Testing Monad’lar

// Property-based testing for monad laws
function testMonadLaws(M, value, fn1, fn2) {
  // Left identity
  const leftIdentity = M.of(value).flatMap(fn1);
  const leftExpected = fn1(value);
  console.assert(
    JSON.stringify(leftIdentity) === JSON.stringify(leftExpected),
    "Left identity failed"
  );

  // Right identity
  const m = M.of(value);
  const rightIdentity = m.flatMap(M.of);
  console.assert(
    JSON.stringify(rightIdentity) === JSON.stringify(m),
    "Right identity failed"
  );

  // Associativity
  const assocLeft = m.flatMap(fn1).flatMap(fn2);
  const assocRight = m.flatMap(x => fn1(x).flatMap(fn2));
  console.assert(
    JSON.stringify(assocLeft) === JSON.stringify(assocRight),
    "Associativity failed"
  );
}

// Test Maybe monad
testMonadLaws(
  Maybe,
  42,
  x => Maybe.of(x * 2),
  x => Maybe.of(x + 10)
);

Son Sözler

Monad’lar başta karmaşık görünse de, aslında çok elegant bir abstraction. Bir kez anladığınızda, kodunuzun daha güvenli, daha composable ve daha okunabilir olduğunu göreceksiniz.

En önemli nokta: Monad’ları anlamak için Category Theory bilmenize gerek yok. Pratik kullanımlarını öğrenin, desenleri kavrayın, kuralları anlayın. Teorik arka plan daha sonra gelir.

JavaScript’te native olarak Promise ve Array zaten monad. TypeScript kullanıyorsanız fp-ts gibi kütüphaneler harika monad implementations sunar.

Günümüz functional programming dillerinde (Haskell, Scala, F#) monad’lar her yerde. Bu deseni öğrenmek sadece JavaScript’te değil, birçok dilde size avantaj sağlayacak.

Keep composing! 🎯🔗



Previous Post
SCP: Güvenli Dosya Transferinin Klasiği
Next Post
Systemd Nedir ve Service Nasıl Oluşturulur?