1. مقدمه

Expr یک راه‌حل پیکربندی پویا است که برای زبان Go طراحی شده است و به دلیل سینتکس ساده و ویژگی‌های عملکرد قدرتمند خود شناخته می‌شود. مغزه اصلی موتور عبارت Expr بر روی ایمنی، سرعت و آسانی است و این امر آن را برای سناریوهایی مانند کنترل دسترسی، فیلترینگ داده و مدیریت منابع مناسب می‌کند. هنگامی که برای Go استفاده می‌شود، Expr بسیار توانمندی برنامه‌ها را در رفتار با قوانین پویا بهبود می‌دهد. برخلاف تفسیرها یا موتورهای اسکریپت در زبان‌های دیگر، Expr از چک‌کردن نوع استاتیک استفاده می‌کند و بایت‌کد برای اجرا تولید می‌کند که هم عملکرد و هم امنیت را تضمین می‌کند.

2. نصب Expr

شما می‌توانید موتور عبارت Expr را با استفاده از ابزار مدیریت بسته زبان Go به نام go get نصب کنید:

go get github.com/expr-lang/expr

این دستور فایل‌های کتابخانه Expr را دانلود کرده و آن‌ها را در پروژه Go خود نصب می‌کند تا بتوانید Expr را در کد Go خود وارد و استفاده کنید.

3. شروع سریع

3.1 کامپایل و اجرای عبارات پایه

بیایید با یک مثال ابتدایی شروع کنیم: نوشتن یک عبارت ساده، کامپایل آن و سپس اجرای آن برای به دست آوردن نتیجه.

package main

import (
	"fmt"
	"github.com/expr-lang/expr"
)

func main() {
	// کامپایل یک عبارت جمع ابتدایی
	program, err := expr.Compile(`2 + 2`)
	if err != nil {
		panic(err)
	}

	// اجرای عبارت کامپایل شده بدون گذراندن محیط، زیرا اینجا نیازی به متغیر‌ها نیست
	output, err := expr.Run(program, nil)
	if err != nil {
		panic(err)
	}

	// چاپ نتیجه
	fmt.Println(output)  // مقدار 4 را چاپ می‌کند
}

در این مثال، عبارت 2 + 2 به بایت‌کد قابل اجرا تبدیل می‌شود و سپس اجرا می‌شود تا نتیجه تولید شود.

3.2 استفاده از عبارات متغیر

در مرحله بعد، ما یک محیط ایجاد می‌کنیم که شامل متغیرهایی است و یک عبارت می‌نویسیم که از این متغیرها استفاده می‌کند، آن را کامپایل و اجرا می‌کنیم.

package main

import (
	"fmt"
	"github.com/expr-lang/expr"
)

func main() {
	// ایجاد یک محیط با متغیرها
	env := map[string]interface{}{
		"foo": 100,
		"bar": 200,
	}

	// کامپایل یک عبارت که از متغیرهای محیط استفاده می‌کند
	program, err := expr.Compile(`foo + bar`, expr.Env(env))
	if err != nil {
		panic(err)
	}

	// اجرای عبارت
	output, err := expr.Run(program, env)
	if err != nil {
		panic(err)
	}

	// چاپ نتیجه
	fmt.Println(output)  // مقدار 300 را چاپ می‌کند
}

در این مثال، محیط env شامل متغیرهای foo و bar است. عبارت foo + bar انواع foo و bar را در مرحله کامپایل از محیط استنتاج می‌کند و مقادیر این متغیرها را در زمان اجرا برای ارزیابی نتیجه عبارت استفاده می‌کند.

4. نحو Expr به صورت دقیق

4.1 متغیرها و نویسه‌ها

موتور عبارت Expr می‌تواند انواع رایج داده‌ها را پردازش کند، از جمله اعداد، رشته‌ها و مقادیر بولی. نویسه‌ها مقادیر داده‌ای دارند که به صورت مستقیم در کد نوشته می‌شوند، مانند 42، "hello" و true.

اعداد

در Expr، می‌توانید عدد صحیح و اعشاری را به صورت مستقیم نوشت:

42      // نمایانگر عدد صحیح 42 است
3.14    // نمایانگر عدد اعشاری 3.14 است

رشته‌ها

نویسه‌های رشته در دو نوع دابل کوتیشن " و یا بک‌تیک `` قرار می‌گیرند. به عنوان مثال:

"سلام، دنیا" // رشته درون دابل کوتیشن، حمایت از کاراکترهای فرار دارد
`سلام، دنیا` // رشته درون بک‌تیک، قالب رشته را بدون حمایت از کاراکترهای فرار حفظ می‌کند

مقادیر بولی

فقط دو مقدار بولی منطقی وجود دارد، true و false، که نمایانگر درست و غلط هستند.

true   // مقدار منطقی درست
false  // مقدار منطقی غلط

متغیرها

Expr همچنین امکان تعریف متغیرها در محیط را فراهم می‌کند و سپس از این متغیرها در عبارت استفاده می‌کند. به عنوان مثال:

env := map[string]interface{}{
    "age": 25,
    "name": "Alice",
}

سپس در عبارت، می‌توانید به age و name ارجاع دهید:

age > 18  // بررسی می‌کند که آیا سن بیشتر از 18 است یا خیر
name == "Alice"  // تعیین می‌کند آیا نام برابر "Alice" است یا خیر

4.2 عملگرها

موتور عبارت Expr از انواع عملگرها پشتیبانی می‌کند، از جمله عملگرهای حسابی، منطقی، مقایسه‌ای و مجموعه، و غیره.

اپراتور‌های حسابی و منطقی

اپراتورهای حسابی شامل جمع (+), تفریق (-), ضرب (*), تقسیم (/), و باقی‌مانده (%) می‌شوند. اپراتورهای منطقی شامل و منطقی (&&), یا منطقی (||), و منفی منطقی (!) می‌باشند، به عنوان مثال:

2 + 2 // نتیجه ۴ است
7 % 3 // نتیجه ۱ است
!true // نتیجه false است
age >= 18 && name == "Alice" // بررسی می‌کند که سن کمتر از ۱۸ نباشد و نام برابر با "Alice" باشد

اپراتورهای مقایسه

اپراتورهای مقایسه شامل مساوی (==), نامساوی (!=), کمتر از (<), کمتر یا مساوی (<=), بزرگتر از (>), و بزرگتر یا مساوی (>=) استفاده می‌شود تا دو مقدار را مقایسه کند:

age == 25 // بررسی می‌کند که سن برابر با ۲۵ باشد
age != 18 // بررسی می‌کند که سن نامساوی ۱۸ باشد
age > 20  // بررسی می‌کند که سن بزرگتر از ۲۰ باشد

اپراتورهای مجموعه

Expr همچنین برای کار با مجموعه‌ها، اپراتورهایی ارائه می‌دهد، مانند in برای بررسی اینکه آیتمی در مجموعه وجود دارد. مجموعه‌ها می‌توانند آرایه‌ها، تیکه‌ها یا نگاشت‌ها باشند:

"user" in ["user", "admin"]  // true، چون "user" در آرایه وجود دارد
3 in {1: true, 2: false}     // false، چون ۳ کلیدی در نگاشت نیست

همچنین توابع پیشرفته‌ی عملیات مجموعه نیز وجود دارند، مانند all، any، one، و none، که نیازمند استفاده از توابع نامشخص (لامبدا) می‌باشند:

all(tweets, {.Len <= 240})  // بررسی می‌کند که فیلد Len در تمامی توییت‌ها از ۲۴۰ کمتر باشد
any(tweets, {.Len > 200})   // بررسی می‌کند که فیلد Len در توییت‌ها وجود دارد که بیشتر از ۲۰۰ باشد

اپراتور عضو

در زبان بیان Expr، اپراتور عضو به ما امکان می‌دهد تا به ویژگی‌های struct در زبان Go دسترسی داشته باشیم. این ویژگی امکان دسترسی مستقیم به ساختارهای داده پیچیده را فراهم می‌کند که باعث انعطاف‌پذیری و کاربردی بودن بسیاری می‌شود.

استفاده از اپراتور عضو بسیار ساده است، فقط کافی است از اپراتور . پس از نام ویژگی استفاده کنید. به عنوان مثال، اگر ما ساختار زیر را داشته باشیم:

type User struct {
    Name string
    Age  int
}

می‌توانید یک عبارت برای دسترسی به ویژگی Name ساختار User به این شکل بنویسید:

env := map[string]interface{}{
    "user": User{Name: "Alice", Age: 25},
}

code := `user.Name`

program, err := expr.Compile(code, expr.Env(env))
if err != nil {
    panic(err)
}

output, err := expr.Run(program, env)
if err != nil {
    panic(err)
}

fmt.Println(output) // خروجی: Alice

برخورد با مقادیر nil

در هنگام دسترسی به ویژگی‌ها، ممکن است با مواجهه با موقعیت‌هایی که شیء nil باشد روبرو شوید. Expr امکان دسترسی ایمن به ویژگی‌ها را فراهم می‌کند، بنابراین حتی اگر ساختمان یا ویژگی تو در تو nil باشد، به اشتباهات زمان اجرای جلوگیری می‌نماید.

استفاده از اپراتور ?. برای ارجاع به ویژگی‌ها است. اگر شیء nil باشد، به جای پرتاب خطا، به جای آن nil را برمی‌گرداند.

author.User?.Name

عبارت معادل

author.User != nil ? author.User.Name : nil

استفاده از اپراتور ?? اصولاً برای بازگرداندن مقادیر پیش‌فرض مورد استفاده قرار می‌گیرد:

author.User?.Name ?? "Anonymous"

عبارت معادل

author.User != nil ? author.User.Name : "Anonymous"

عملگر لوله

عملگر لوله (|) در Expr برای انتقال نتیجه یک عبارت به عنوان پارامتر به یک عبارت دیگر استفاده می‌شود. این مانند عملیات لوله کشی در شل Unix است و به چندین ماژول کاربردی امکان زنجیره‌سازی برای ایجاد خط لوله پردازش فراهم می‌کند. در Expr، استفاده از آن می‌تواند عبارات روشن‌تر و مختصرتری ایجاد کند.

به عنوان مثال، اگر یک تابع برای گرفتن نام کاربر و یک الگو برای یک پیام خوش‌آمدگویی داشته باشیم:

env := map[string]interface{}{
    "user":      User{Name: "Bob", Age: 30},
    "get_name":  func(u User) string { return u.Name },
    "greet_msg": "Hello, %s!",
}

code := `get_name(user) | sprintf(greet_msg)`

program, err := expr.Compile(code, expr.Env(env))
if err != nil {
    panic(err)
}

output, err := expr.Run(program, env)
if err != nil {
    panic(err)
}

fmt.Println(output) // خروجی: Hello, Bob!

در این مثال، ابتدا نام کاربر را از طریق get_name(user) دریافت می‌کنیم، سپس نام را به تابع sprintf با استفاده از عملگر لوله | منعکس کرده و پیام خوش‌آمدگویی نهایی را ایجاد می‌کنیم.

استفاده از عملگر لوله می‌تواند کد را ماژولار کند، قابلیت استفاده مجدد کد را افزایش دهد و عبارات را خواناتر کند.

4.3 توابع

Expr از توابع تعبیه‌شده و توابع سفارشی پشتیبانی می‌کند که باعث می‌شود عبارات قدرتمندتر و انعطاف‌پذیرتر شوند.

استفاده از توابع تعبیه‌شده

توابع تعبیه‌شده مانند len، all، none، any و غیره را می‌توان به طور مستقیم در عبارت استفاده کرد.

// مثال استفاده از تابع تعبیه‌شده
program, err := expr.Compile(`all(users, {.Age >= 18})`, expr.Env(env))
if err != nil {
    panic(err)
}

// توجه: در اینجا env باید شامل متغیرهای users باشد و هر کاربر باید ویژگی Age را داشته باشد
output, err := expr.Run(program, env)
fmt.Print(output) // اگر همه‌ی کاربران در env 18 سال یا بیشتر باشند، درستی برمی‌گردد

تعریف و استفاده از توابع سفارشی

در Expr، می‌توانید با انتقال تعریف‌های تابع در نگاشت محیطی، توابع سفارشی ایجاد کنید.

// مثال تابع سفارشی
env := map[string]interface{}{
    "greet": func(name string) string {
        return fmt.Sprintf("Hello, %s!", name)
    },
}

program, err := expr.Compile(`greet("World")`, expr.Env(env))
if err != nil {
    panic(err)
}

output, err := expr.Run(program, env)
fmt.Print(output) // خروجی: Hello, World!

در استفاده از توابع در Expr، شما می‌توانید کد را ماژولار کنید و منطق پیچیده را در عبارات گنجانید. با ترکیب متغیرها، عملگرها و توابع، Expr یک ابزار قدرتمند و آسان برای استفاده می‌شود. همیشه به تأمین ایمنی نوع هنگام ساخت محیط Expr و اجرای عبارات توجه کنید.

5. مستند توابع تعبیه‌شده

موتور عبارت Expr برای توسعه‌دهندگان مجموعه‌ای غنی از توابع تعبیه‌شده را برای رفع و تنظیم انواع پیچیده ارائه می‌دهد. در زیر، این توابع تعبیه‌شده و استفاده‌شان به تفصیل توضیح داده شده است.

همه

تابع همه برای بررسی اینکه آیا همه عناصر یک مجموعه شرط داده شده را ارضا می‌کنند یا خیر، استفاده می‌شود. این دو پارامتر را می‌پذیرد: مجموعه و عبارت شرطی.

// بررسی اینکه آیا همه توییت‌ها طول محتوای کمتر از ۲۴۰ را دارند یا خیر
code := `all(tweets, len(.Content) < 240)`

هر

مانندهمه، تابع هر برای بررسی اینکه آیا هر عنصر در یک مجموعه شرطی را برآورده می‌کند یا خیر، استفاده می‌شود.

// بررسی اینکه آیا هر توییت دارای طول محتوای بیشتر از ۲۴۰ است یا خیر
code := `any(tweets, len(.Content) > 240)`

هیچ

تابع هیچ برای بررسی اینکه هیچ عنصری در یک مجموعه شرطی را برآورده می‌کند یا خیر، استفاده می‌شود.

// اطمینان حاصل کردن از عدم تکرار توییت‌ها
code := `none(tweets, .IsRepeated)`

یکی

تابع یکی برای تایید کردن اینکه فقط یک عنصر در یک مجموعه شرطی را برآورده می‌کند یا خیر، استفاده می‌شود.

// بررسی اینکه آیا تنها یک توییت حاوی یک کلمه کلیدی خاص است
code := `one(tweets, contains(.Content, "keyword"))`

فیلتر

تابع فیلتر می‌تواند عناصر مجموعه را که یک شرط داده شده را برآورده می‌کنند، فیلتر کند.

// فیلتر کردن همه توییت‌های مشخص شده به عنوان اولویت‌دار
code := `filter(tweets, .IsPriority)`

نقشه

تابع نقشه برای تبدیل عناصر مجموعه براساس یک متد مشخص، استفاده می‌شود.

// قالب‌بندی زمان انتشار همه توییت‌ها
code := `map(tweets, {.PublishTime: Format(.Date)})`

len

تابع len برای بازگشت طول یک مجموعه یا رشته استفاده می‌شود.

// گرفتن طول نام کاربری
code := `len(username)`

contains

تابع contains برای بررسی اینکه آیا یک رشته شامل یک زیررشته است یا آیا یک مجموعه حاوی یک عنصر خاص استفاده می‌شود.

// بررسی اینکه آیا نام کاربری شامل کاراکترهای غیرقانونی است
code := `contains(username, "illegal characters")`

موارد فوق تنها بخشی از توابع تعبیه‌شده ارائه شده توسط موتور اصطلاح Expr هستند. با این توابع قدرتمند، می‌توانید با داده و منطق، به صورت انعطاف‌پذیرتر و کارآمدتر برخورد کنید. برای لیست اصلاحی تر دقیق توابع و دستورالعمل‌های استفاده، لطفا به مستندات رسمی اصطلاح مراجعه کنید.