در ساخت زبان های برنامه نویسی یک شیوه ساختن مفسر ایجاد ساختار های درخت نحو انتزاعی (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 جدید به وجود نمیاد