Abstractions OOP/FP

OOP اگه خوب نوشته بشه با توجه به اصول SOLID و separation of concerns بسیار خوب میتونه abstraction ایجاد کنه در FP هم میشه کده بد نوشت فرقی که وجود داره اینکه در FP بخاطره یک سری قانون ها که خوده زبان اعمال میکنه کد با ساختار پیچیده سخت تر نوشته میشه
در FP
composition اول از همه بجای inheritance کار میکنه و به جای ساختن سلسه شی و پنهان کردن داده پشته شی برنامه به جابجا کردن داده از function به function انجام میشه

شما شاید بگی inheritance در oop زیاد بد نیست و اگه نباشه چطور FP abstraction بوجود میاره

با یک مثال شاید این فرق بهتر معلوم شه فرض کنید میخواهیم یک برنامه برای log کردن بنویسیم
راهه کلاسیک OOP ساختن interface و ارث بردن

trait Loggable {
  def logMessage:String
}

object Logger {
  def log(value: Loggable):String = {
    println("Logging:" value.logMessage)
  }
}

حالا هر class که میخواهیم log کنیم باید از این trait استفاده کنن و راجبه Logging بدونن این کار tight coupling بوجود میاره و کد به سختی عوض میشه بعد از این

case class User(name: String, age: Int) extends Loggable {
  override def logMessage = println(s"name: $name age: $age") 
}

و به علاوه شاید یک کلاس برای یک کتاب خانه باشه که ما اصلا بهش دسترسی نداریم چطوری ان وقت لاگ میکنیم ؟

میتونیم مثال بالا رو کمی بهتر کنیم و Loggable برای هر تایپ بصورت کلی بنویسیم و function log عوض کنیم که Logger خاص هر تایپ بگیره

trait Loggable[A] {
  def logMessage(a: A):String
}

object Logger {
  def log[A](value: A, loggable: Loggable[A]):String = {
    println("Logging:" loggable.logMessage(a))
  }
}

این بهتره والی هنوز مشکل کلاس هایی که برای کتابخانه های دیگه هستن حل نمیکنه برای اینکار از
یک خاصیت سکالا که هسکل هم شبیهش را انجام میده استفاده میکنیم با اضافه کردن کلمه implicit برای loggable
complier سعی میکنه یک Loggable برای تایپ که بهش داده شده پیدا کنه و اگه پیدا نکنه compile نمیشه

trait Loggable[A] {
  def logMessage(a: A):String
}

object Logger {
  def log[A](value: A)(implicit loggable: Loggable[A]):String = {
    println("Logging:" loggable.logMessage(a))

}
}

نوشتن Loggable برای یک تایپ

  implicit val intLoggable: Loggable[Int] = new Loggable[Int] {
    def logMessage(a: Int):String = a.toString
  }

این کار به typeclass پترن معروفه که به قدرت dynamic typing از نظره کلی بودن ولی کاملا type safe هستش و در compile time خطا آشکار میشه اگه یک تایپ با interface درست کار نکنه تا زمانی که هر کتابخانه برای interface ما یک instance داشته باشه یا اینکه خودمون بنویسیم همه چیز کار میکنه

6 پسندیده

بسیار عالی و مفهومی :rose::hibiscus:
یه چیز دیگه دیگه ک اینجا بحث شد تایپ ها بودن . ک خطاهارو کاهش دادن
یه مدتیه تو یه گروه فانکشنال . یه نفر زبان idris رو معرفی کرده … ک مثل اینکه زاده هسکل هست و روی تایپ ها خیلی تاکید داره
dris is a general-purpose purely functional programming language with dependent types
و مثل اینکه خیلی راحت هم قادره به چیزای دیگه مثل js تبدیل بشه …

یه سوالی سام عزیز. abstraction میتونه تاثیری توی ترانسپایل راحت زبان یا پارادایم ها به همدیگه داشته باشه ؟

3 پسندیده

بله تمامه زبان ها چه دینامیک مثله روبی و پایتان چه ستاتیک مثله جاوا یک لایه در parse دران که از type theory استفاده میکنن و سینتکس تبدیل به فرمان های ماشین تبدیل میکنن یکسری از بهترین زبان های خاص مثله LINQ توسط type theory و FP ساخته شدن
یکی از پترن های رایج ساختن زبان های کوچک برای برنامه هست که اینجا اشاره کردم


Idris از هسکل هم بهتره چون با تایپ میتونی منطق برنامه رو بیان کنی مثلا بگی این تایپه عدد بین ۱۰ تا ۱۲ هستش و این تایپ لیست با ۲ عضو به این میگن
Type Level Programming با Dependent Types
و idris میتونه برای برخی از function ها فقط با نوشتن تایپ function خودش کده function بنویسه
ترانسپیله idris یا elm به جاواسکریپت موجب میشه که برنامه تا حد ممکن درست باشه و وقتی compile میشه به احتمال خیلی زیاد درست کار میکنه
کسایی که elm کار میکنن میگن هیچ خطا runtime هیچ وقت نمیگیر و وقتی refactor هم میکنن وقتی compile میشه
کد درست کار میکنه
یک مثال از یک زبان

sealed trait Expr[A]
case class Value[A](a: A) extends Expr[A]
case class Multiply[A](a: Expr[A], b: Expr[A]) extends Expr[A]
case class Divide[A](a: Expr[A], b: Expr[A]) extends Expr[A]
case class Add[A](a: Expr[A], b: Expr[A]) extends Expr[A]
case class Sub[A](a: Expr[A], b: Expr[A]) extends Expr[A]

object Calculator {
  def run(expr: Expr[Int]): Int= {
    expr match  {
      case Value(a) => a
      case Multiply(a,b) => this.run(a) * this.run(b)
      case Divide(a,b) => this.run(a) / this.run(b)
      case Add(a,b) => this.run(a) + this.run(b)
      case Sub(a,b) => this.run(a) - this.run(b)
  
    }
  }
}

Calculator.run(
  Multiply(Value(6), Multiply(Value(6),Value(3)))
)
4 پسندیده