زبان خاص دامنه و مفسر توسط Free Monad

functional

#1

در ساخت زبان های برنامه نویسی یک شیوه ساختن مفسر ایجاد ساختار های درخت نحو انتزاعی (Abstract Syntax Tree (AST که عبارت ها به صورت درختی تفسیر میشوند

https://upload.wikimedia.org/wikipedia/commons/c/c7/Abstract_syntax_tree_for_Euclidean_algorithm.svg

در برنامه نویسی روز مره هم میشه از این روش برای ساختن سیستم های ترکیب پذیر استفاده کرد
برای هر قسمت و دامنه برنامه زبانه خاص تعریف کرد و مفسره مختص به کارایی برنامه.

ساختار داده که در اینکار در سکالا و هسکل خیلی خوبه Free Monad هستش

Free یک monad که داده جبری تشکیل شده از دو حالت Suspend که تفسیر متوقف میکنه و pure که خود یک Free Monad دیگه درست میکنه برای تفسیر و توسط این میشه
یک داده درختی AST درست کرد که به صورت recursive یا تفسیر میشه یا یک درخت دیگه برای تفسیر برمیگردونه

sealed abstract class Free[F[_], A]
case class Pure[F[_], A](a: A) extends Free[F, A]
case class Suspend[F[_], A](a: F[Free[F, A]]) extends Free[F, A]

تعریف زبان :

فرض کنید که ما یک سرویس داریم که اطلاعات کاربر تغییر میده در زبان که تعریف میکنیم سه عملکرد مجاز این زبان به صورت داده جبری مینویسیم
۱. گرفتن کاربر
۲. تغییر و ذخیره
۳. لاگ کردن

هر عبارت این زبان و خروجی به در مبنا Free تعریف میکنیم

  import cats._
  import cats.data._
  import cats.free._
  import cats.implicits._

  case class User(Id: Int, name: String)

  type Script[A] = Free[AppAction, A]

  sealed trait AppAction[A] {
    def lift: Script[A] = Free.liftF(this)
  }

  case class FetchById(userID: Int) extends AppAction[User]

  case class Save(user: User) extends AppAction[Unit]

  case class Log(message: String) extends AppAction[Unit]

نوشتن function ها که هرکدام با این زبان کار میکنن

  def fetchById(id: Int): Script[User] = FetchById(id).lift
  def save(user: User): Script[Unit] = Save(user).lift

  def log(message: String): Script[Unit] = Log(message).lift


  def updateUser(userID: Int, post: String): Script[User] = {
    for {
      user <- fetchById(userID)
      u1 = user.copy(name = name)
      _ <- log("Updating")
      _ <- save(u1)
    } yield u1
  }

در اینجا جبر و زبان تغییر کاربر و حتی ترتیب این کار تعریف شده ولی هیچ مفسری برای زبان نداریم
شاید بخواهیم یک مفسر جدا برای تست داشته باشیم که به database واقعن وصل نشه

مفسر تست :
برای نوشتن مفسر کافی یک عملکرد برای هر یک از سه عبارت زبان تعریف کنیم و برنامه رو با این مفسر اجرا کنیم

  object TestInterp extends (AppAction ~> Id) {
    def apply[A](fa: AppAction[A]): Id[A] = fa match {
      case FetchById(_) => User(1, "2000")
      case Save(_) => ()
      case Log(msg) => println(msg)
    }

    def run[A](script: Script[A]) = script.foldMap(this)
  }

در این مفسر با save کاری نمیکنیم چون نمیخواهیم به database وصل بشیم

چرا با این روش برنامه بنویسیم ؟
شاید برای شما سوال پیش اومد که این همه کار و برای چی داریم انجام میدیم ؟
فایده این کار بسیار زیاده چون کد بسیار ماژول های کوچک داره اضافه کردن یک کار جدید به کد یا با غنی تر کردن زبان انجام میشه یا با نوشتن یک مفسر جدید
و با این کار باگ به سیستم های قدیمی با تغییر و درست کردن feature جدید به وجود نمیاد


Abstractions OOP/FP
#2

ممنون سام عزیز. جسارتا من سه بار خوندم ولی هیچی متوجه نشدم . یعنی چی داریم مفسر جدید می سازیم ؟ شما دارید کد جدید می زنید روی کد قدیمی ؟

اگر امکان و وقت دارید به زبان ساده برای تازه واردین مثل من توضیح بدید بسیار ممنون می شم


#3

زبان روبی if داره while داره و … اینا همه عبارت های زبانه روبی به حساب میان
حالا اینجا من برای برنامه خودم اومدم ی زبان درست کردم
که سه عبارت داره
FetchById برای گرفتن کاربر
log برای نوشتن به لاگ
save برای ذخیره کاربر

تا اینجا رو متوجه شدی؟


#5

تا اینجا رو سام عزیز متوجه شدم


#6

خوب حالا که این زبان و عبارت هارو تعریف کردم یک ساختار وجود داره به نامه Free Monad که هر عبارت این زبان به عبارت درختی تبدیل میکنه AST مثل عکس بالا


#7

من متوجه صحبت شما الان به صورت کلی شدم و فهمیدم که کارش چییه . ولی یک سوال برام پیش اومده این فرقش با یک defmacro در الکسیر چی هست ؟

ما یک تمپلیت درست می کنیم که توش مثلا monad استفاده بشه مثل with ?

ممنون بخاطر توضیحت تشکر


#8

defmacro به صورت دینامیک زبان تعریف میکنه این ستاتیک و type safe
ولی از نظر کلی هدفشون یکیه

نکته مهمش اینکه اگه بتونی برای هر حوزه کاری زبانه خاص با قانون بنویسی برنامه ها بسیار قویتر و راحت تر تغییر میکنن

اگه فکر کنی زبان های برنامه نویسی هرچه گویا تر هستن راحت تر تغییر میکنن و شما هم میتونی یک زبان یا چند زبانه مجزا برای برنامه خودت تعریف کنی


#9

سام عزیز ممنون دیگه نمی دونم چطور تشکر کنم .

یک سوال آیا این مورد باعث پایین اومدن خوانایی کد نمی شه مخصوصا برای افرادی که تازه وارد پروژه شدند ؟

فکر کنم elixir قسمتی مثل type safe نداره درسته ؟ و آیا چنین موردی منظورم Free Monad داخل الکسیر هم وجود داره ؟ چون سرچ کردم مواردی برای Monad داره ولی این موردی که شما فرمودید خیر

من یکی از دلایلی که هیچ وقت از نوع داینامیک که شما فرمودید منظورم defmacro استفاده نکردم دلیلی این بود که خوانایی رو پایین می یاره . البته بیشتر داخل انجمن صحبت شده بود .

اینجوری که از این پست فهمیدم پس با ساخت قانون های کلی پروژه با ثبات تر و هدفمند تر جلو می ره , درسته صحبتم ؟


#10

اینکار کد ناخوانا نمیکنه ولی درکش برای برنامه نویسی که از مفاهیم پیچیده تر FP آگاهی نداره سخت تره
به همین دلیل انتظار از برنامه نویس بسیار بیشتره که همیشه این توصیه نمیشه

الیکسیر دینامیک هستش و در زبان های داینامیک macro ها روش عمومی این کاره در الیکسیر و زبان های دینامیک وجود Free در تئوری امکان پذیره اما در عمل اصلا به درد نمیخوره چون تو تایپ سیستم ستاتیک و قوی که خوب میشه ازشون استفاده کرد

من شخصا با این موافقم و طرفداره این نوع زبان ها هستم که درستی برنامه تا حد ممکن در زمان compile با type ها ثابت بشه ولی این یک از اون دعوا کلاسیک برنامه نویسی ستاتیک vs دینامیک که پایان نداره