تبدأ تعلم البرمجة بلغة واحدة، ثم تفتح أول مشروع في العمل فتجد الواجهة الأمامية بلغة TypeScript، والخدمة الخلفية بلغة Go، وسكربتات الأتمتة بلغة Python، وتقارير قاعدة البيانات بلغة SQL، وملفات البنية التحتية بلغة Terraform، ثم تقرأ في الإعلانات الوظيفية كلمات مثل «لغة وظيفية Functional» و«أنواع ثابتة Static Types» و«Null Safety» وكأن كل مطور يعرف معناها. وقد يتساءل البعض: لماذا كل هذه اللغات أصلاً؟ ولماذا لا يتفق المطورون على لغة واحدة ويرتاح الجميع؟ والإجابة أن كل لغة هي مجموعة قرارات صممها أصحابها لحل نوع معين من المشاكل، فاللغة التي تجعل كتابة سكربت سريع أمراً سهلاً هي نفسها التي تترك بعض الأخطاء حتى وقت التشغيل، واللغة التي تمسك أغلب الأخطاء قبل التشغيل تطلب منك في المقابل أن تكتب أكثر وتنتظر المترجم.
وهذه القرارات لها أسماء، وأهمها سؤالان: الأول هو نمط البرمجة Programming Paradigm، أي هل تكتب للحاسوب خطوات ينفذها واحدة بعد الأخرى، أم تصف له النتيجة التي تريدها وتتركه يقرر الطريقة. والثاني هو نظام الأنواع Type System، أي متى تكتشف اللغة أنك تجمع نصاً مع رقم، وماذا تفعل عندها. وسوف نناقش في هذا المقال ما يلي:
- لماذا توجد مئات اللغات، وما هي المقايضات Trade-offs التي تختلف فيها.
- أنماط البرمجة بمثال واحد يتكرر في كل نمط، وهو حساب إجمالي طلب في متجر: الإجرائي Procedural بلغة Go، والكائني Object-Oriented بلغة Java، والوظيفي Functional بلغة JavaScript وPython وHaskell، والتصريحي Declarative بلغة SQL وTerraform، والمنطقي Logic بلغة Prolog، والقائم على الأحداث Event-Driven.
- أنظمة الأنواع: الثابتة Static والديناميكية Dynamic، والقوية Strong والضعيفة Weak، واستنتاج الأنواع Type Inference، والأنواع التدريجية Gradual Typing مع TypeScript وmypy، والنظام الاسمي Nominal والبنيوي Structural، والحماية من Null، والأنواع العامة Generics.
- جدول مقارنة لأشهر اللغات من حيث الأنماط والأنواع وطريقة التنفيذ وإدارة الذاكرة، ثم كيف تستفيد من هذا كله عند اختيار لغة لمشروعك.
وكل مثال في المقال شغلناه فعلاً داخل Container، والمخرجات والأخطاء المنسوخة هنا هي ما ظهر على الشاشة كما هو.
لماذا توجد كل هذه اللغات؟
لنفرض أنك تريد لغة واحدة تناسب كل شيء، فسوف تطلب منها أن تكون سريعة جداً مثل C لأنك تكتب بها نظام تشغيل، وأن تكون سهلة ومرنة مثل Python لأنك تكتب بها سكربت لمرة واحدة، وأن تمنع أخطاء الذاكرة مثل Rust لأنك تكتب بها برنامجاً يتعامل مع بيانات الناس، وأن تعمل داخل المتصفح مثل JavaScript. والمشكلة أن هذه الطلبات تتعارض، فالسرعة القصوى تحتاج تحكماً مباشراً في الذاكرة، والتحكم المباشر يفتح باب الأخطاء، ومنع الأخطاء قبل التشغيل يحتاج قواعد صارمة يفحصها المترجم Compiler، والقواعد الصارمة تبطئ كتابة السكربت السريع. لذلك فكل لغة تختار نقطة على هذه المقايضات، وأهمها ما يلي:
- سرعة التنفيذ Performance مقابل سرعة الكتابة Productivity: لغة مثل C تعطيك أقصى سرعة وتترك لك كل التفاصيل، ولغة مثل Python تكتب بها في سطر ما يحتاج عشرة أسطر في C، ولكنها أبطأ في التنفيذ.
- الأمان Safety مقابل الحرية: كلما زادت القواعد التي يفحصها المترجم قلت الأخطاء التي تصل إلى المستخدم، وزاد الوقت الذي تقضيه في إرضاء المترجم.
- البيئة التي تعمل فيها: JavaScript موجودة لأنها اللغة التي تفهمها المتصفحات، وSwift وKotlin موجودتان لأن Apple وGoogle تدعمانهما في أنظمة الهواتف، وSQL موجودة لأن قواعد البيانات تفهمها.
- طريقة التفكير في المشكلة: بعض المشاكل تكتب بوضوح كخطوات، وبعضها كتحويل بيانات، وبعضها كقواعد منطقية، ولهذا ظهرت الأنماط التي سوف نشرحها.
وقصة كل لغة ومن صنعها ولماذا، تجدها في مقال قصص لغات البرمجة ومن صنعها، أما الفرق بين اللغة كمواصفات Specification وبين البرامج التي تنفذها مثل CPython وPyPy، ومن يقرر مستقبل كل لغة، فهو موضوع مقال مواصفات لغات البرمجة وتطبيقاتها وحوكمتها. وفي هذا المقال سوف نركز على الفكرتين اللتين تفرقان بين اللغات في الكود الذي تكتبه كل يوم: الأنماط والأنواع.
المختبر: كل اللغات في Containers
لن تحتاج إلى تثبيت أي لغة على جهازك، فكل مثال يعمل داخل Container من Docker Image رسمية، يحذف بعد انتهاء الأمر بسبب الخيار --rm، ويقرأ الملفات من المجلد الحالي عبر -v "$PWD":/w. وإذا لم يكن Docker مثبتاً لديك فقد شرحنا تثبيته في دليل تثبيت Docker على Ubuntu. والجدول التالي يبين ال Images والإصدارات التي استخدمناها:
| اللغة أو الأداة | Docker Image | الإصدار الذي ظهر في المختبر |
|---|---|---|
| Go | golang:1.27.1-alpine3.24 | go1.27.1 |
| Java | eclipse-temurin:25-jdk-alpine | OpenJDK 25.0.4.1 |
| JavaScript وTypeScript | node:22-alpine | Node.js v22.23.3 وTypeScript 7.0.2 |
| Python وmypy | python:3.13-slim | Python 3.13.16 وmypy 2.4.0 |
| Rust وC | rust:1-alpine | rustc 1.99.0 وgcc 15.2.0 |
| Haskell | haskell:9-slim | GHC 9.14.1 |
| Prolog | swipl:latest | SWI-Prolog 10.1.16 |
| SQL | postgres:17.11-alpine | PostgreSQL 17.11 |
| Terraform | hashicorp/terraform:latest | Terraform v1.16.5 |
ولاحظ أن كل Container يأخذ اسماً يبدأ ب lab-lang، والسبب أنه إذا بقي أحدها يعمل لأي سبب فسوف تجده بسهولة وتحذفه وحده دون أن تلمس Containers المشاريع الأخرى على جهازك.
أنماط البرمجة Programming Paradigms: الخطوات أم النتيجة؟
نمط البرمجة Programming Paradigm هو طريقة التفكير التي تكتب بها الحل، وليس صياغة Syntax لغة معينة، فيمكنك أن تكتب بأكثر من نمط في نفس اللغة. والتقسيم الأشهر يبدأ بعائلتين: العائلة الأمرية Imperative، وفيها تقول للحاسوب كيف يصل إلى النتيجة خطوة بعد خطوة، وتغير قيم المتغيرات في الطريق، ومنها الإجرائية Procedural والكائنية Object-Oriented. والعائلة التصريحية Declarative، وفيها تصف النتيجة التي تريدها وتترك للغة أو للنظام تحديد الخطوات، ومنها الوظيفية Functional والمنطقية Logic ولغات الاستعلام والوصف مثل SQL وHTML. والصورة التالية تبين هذه العائلة:

ولكي تكون المقارنة عادلة سوف نحل نفس المشكلة في كل نمط، وهي حساب إجمالي طلب في متجر فيه أربعة أسطر: قهوة بسعر 12 والكمية 2، وكعكة بسعر 18 والكمية 1، وعصير بسعر 9 والكمية 0 لأن العميل ألغاه، وماء بسعر 3 والكمية 4. والإجمالي الصحيح هو 24 + 18 + 12 = 54، وسطر العصير يجب أن يتجاهل.
البرمجة الإجرائية Procedural: خطوات تغير متغيراً
البرمجة الإجرائية Procedural Programming هي أقرب الأنماط لطريقة عمل المعالج نفسه: متغيرات في الذاكرة، وأوامر تنفذ بالترتيب، وحلقات Loops وشروط، ودوال Procedures تجمع الخطوات المتكررة. وهذا هو الحل بلغة Go في الملف main.go:
package main
import "fmt"
type Line struct {
Name string
Price int
Qty int
}
func main() {
order := []Line{
{"coffee", 12, 2},
{"cake", 18, 1},
{"juice", 9, 0},
{"water", 3, 4},
}
total := 0
for i := 0; i < len(order); i++ {
if order[i].Qty > 0 {
total = total + order[i].Price*order[i].Qty
fmt.Println("after", order[i].Name, "total =", total)
}
}
fmt.Println("order total:", total)
}ونشغله بالأمر التالي:
docker run --rm --name lab-lang-go -v "$PWD":/w -w /w golang:1.27.1-alpine3.24 go run main.goوالمخرج سوف يكون كما يلي:
after coffee total = 24
after cake total = 42
after water total = 54
order total: 54في الكود أعلاه لاحظ التالي:
- المتغير
totalيبدأ من صفر ثم تتغير قيمته في كل دورة من الحلقة، وهذا ما يسمى الحالة القابلة للتغيير Mutable State، وهي قلب البرمجة الأمرية. - أنت من يحدد كل شيء: من أين تبدأ الحلقة، ومتى تتوقف، وكيف يتخطى الكود سطر العصير، ولهذا السبب فالكود واضح لمن يقرؤه خطوة خطوة، ولكنه أيضاً يفتح الباب لأخطاء مثل أن تبدأ العداد من 1 بدلاً من 0 فيضيع أول سطر.
- الأسطر المطبوعة تبين الحالة بعد كل خطوة: 24 ثم 42 ثم 54، ولا يوجد سطر للعصير لأن الشرط
Qty > 0تخطاه.
ولغة C وPascal وGo هي أمثلة واضحة على هذا النمط، وإن كانت Go تضيف الواجهات Interfaces التي سوف نراها لاحقاً، ولا تحتوي على الوراثة Inheritance كما تقول الأسئلة الشائعة للغة Go.
البرمجة الكائنية OOP: بيانات وسلوك في مكان واحد
عندما يكبر البرنامج تظهر مشكلة في الأسلوب الإجرائي، وهي أن البيانات (أسطر الطلب) في مكان، والدوال التي تعدلها في مكان آخر، وأي جزء من الكود يستطيع أن يضع كمية سالبة في الطلب دون أن يمنعه أحد. والبرمجة الكائنية Object-Oriented Programming واختصاراً OOP تحل هذا بأن تجمع البيانات والدوال التي تعمل عليها في كلاس Class واحد، وتخفي البيانات بحيث لا تتغير إلا عبر دوال الكلاس. والمثال التالي بلغة Java في الملف Main.java:
import java.util.ArrayList;
import java.util.List;
interface Discount {
int apply(int total);
}
class NoDiscount implements Discount {
public int apply(int total) { return total; }
}
class PercentOff implements Discount {
private final int percent;
PercentOff(int percent) { this.percent = percent; }
public int apply(int total) { return total - total * percent / 100; }
}
class Order {
private final List<int[]> lines = new ArrayList<>();
private final Discount discount;
Order(Discount discount) { this.discount = discount; }
void add(int price, int qty) {
if (qty < 0) throw new IllegalArgumentException("qty must be >= 0");
lines.add(new int[] {price, qty});
}
int total() {
int sum = 0;
for (int[] l : lines) sum += l[0] * l[1];
return discount.apply(sum);
}
}
public class Main {
public static void main(String[] args) {
for (Discount d : List.of(new NoDiscount(), new PercentOff(10))) {
Order order = new Order(d);
order.add(12, 2);
order.add(18, 1);
order.add(9, 0);
order.add(3, 4);
System.out.println(d.getClass().getSimpleName() + " -> " + order.total());
}
new Order(new NoDiscount()).add(5, -1);
}
}ونشغله بالأمر التالي، ولاحظ أن Java منذ الإصدار 11 تستطيع تشغيل ملف المصدر مباشرة دون خطوة javac منفصلة كما في JEP 330:
docker run --rm --name lab-lang-java -v "$PWD":/w -w /w eclipse-temurin:25-jdk-alpine java Main.javaوالمخرج سوف يكون كما يلي:
NoDiscount -> 54
PercentOff -> 49
Exception in thread "main" java.lang.IllegalArgumentException: qty must be >= 0
at Order.add(Main.java:25)
at Main.main(Main.java:46)في الكود أعلاه لاحظ التالي:
- التغليف Encapsulation: القائمة
linesمعرفةprivate، فلا يستطيع أي كود خارج الكلاسOrderأن يضيف إليها مباشرة، والطريق الوحيد هو الدالةaddالتي ترفض الكمية السالبة، ولهذا ظهر الاستثناءIllegalArgumentExceptionفي آخر سطر من المخرج عندما حاولنا إضافة كمية-1. - تعدد الأشكال Polymorphism: الكلاس
Orderلا يعرف نوع الخصم، فهو يتعامل مع الواجهةDiscountفقط، ونفس الاستدعاءdiscount.apply(sum)أعطى 54 معNoDiscountو49 معPercentOff(10)، والسبب في 49 وليس 48.6 أن القسمة هنا على أعداد صحيحة Integers فيقطع الكسر. - التركيب Composition مقابل الوراثة Inheritance: الطلب «لديه» خصم Has-a، ولم نجعل
DiscountedOrderكلاساً يرث منOrder. والسبب أن الوراثة تربط الكلاس الابن بتفاصيل الأب، فإذا احتجت طلباً بخصم وبشحن مجاني وبضريبة فسوف تنتهي بشجرة كلاسات لكل تركيبة، أما التركيب فتمرر فيه الكائنات التي تريدها. والوراثة لها مكانها عندما تكون العلاقة «هو نوع من» Is-a فعلاً، كما يشرح درس الوراثة في توثيق Oracle.
ولاحظ أن الكائنية ما زالت أمرية في داخلها، فالدالة total() فيها حلقة تغير المتغير sum تماماً كما في Go، والفرق هو تنظيم الكود حول الكائنات وحماية حالتها.
البرمجة الوظيفية Functional: دوال تحول البيانات
البرمجة الوظيفية Functional Programming تنظر إلى الحساب على أنه تطبيق دوال على بيانات لإنتاج Production بيانات جديدة، بدلاً من تغيير متغيرات. وفكرتها الأساسية ثلاث أدوات: filter تختار العناصر التي تحقق شرطاً، وmap تحول كل عنصر إلى قيمة جديدة، وreduce تجمع القائمة في قيمة واحدة. والمثال التالي بلغة JavaScript في الملف order.js:
const order = [
{ name: "coffee", price: 12, qty: 2 },
{ name: "cake", price: 18, qty: 1 },
{ name: "juice", price: 9, qty: 0 },
{ name: "water", price: 3, qty: 4 },
];
const lineTotal = (line) => line.price * line.qty; // pure function
const inStock = (line) => line.qty > 0;
const total = order
.filter(inStock)
.map(lineTotal)
.reduce((sum, x) => sum + x, 0);
console.log("order total:", total);
console.log("order unchanged:", order.length, "lines");
const applyTwice = (f, x) => f(f(x)); // higher-order function
console.log(applyTwice((n) => n * 2, 5));
const frozen = Object.freeze([1, 2, 3]);
try { frozen.push(4); } catch (e) { console.log(e.constructor.name + ":", e.message); }ونشغله بالأمر التالي:
docker run --rm --name lab-lang-node -v "$PWD":/w -w /w node:22-alpine node order.jsوالمخرج سوف يكون كما يلي:
order total: 54
order unchanged: 4 lines
20
TypeError: Cannot add property 3, object is not extensibleفي الكود أعلاه لاحظ التالي:
- الدوال النقية Pure Functions: الدالة
lineTotalتعطي نفس النتيجة دائماً لنفس المدخل، ولا تغير شيئاً خارجها، ولهذا يسهل اختبارها وحدها، ويسهل تشغيلها على عدة أنوية دون أقفال، وهو موضوع شرحناه في مقال المعالج والخيوط والتزامن. - عدم التغيير Immutability: بعد كل الحساب ما زال الطلب الأصلي فيه 4 أسطر، لأن
filterوmapتنتجان قوائم جديدة ولا تعدلان القائمة الأصلية. والسطر الأخير يبين أنObject.freezeيمنع التعديل فعلاً، فمحاولةpushعلى المصفوفة المجمدة رمتTypeError، كما يوضح توثيق MDN. - الدوال عالية الرتبة Higher-Order Functions:
applyTwiceتستقبل دالة كمعامل وتستدعيها مرتين، فأعطت 5 × 2 × 2 = 20. وهذا ممكن لأن الدوال في JavaScript قيم عادية من الدرجة الأولى First-Class Functions كما في MDN، وfilterوmapوreduceنفسها دوال عالية الرتبة.
ونفس الفكرة في Python، مرة بالدوال map وfilter وreduce، ومرة بالطريقة التي يفضلها مطورو Python وهي التعبير المولد Generator Expression، في الملف order.py:
from functools import reduce
order = (("coffee", 12, 2), ("cake", 18, 1), ("juice", 9, 0), ("water", 3, 4))
total = reduce(lambda s, x: s + x,
map(lambda l: l[1] * l[2],
filter(lambda l: l[2] > 0, order)), 0)
print("reduce:", total)
print("sum + generator:", sum(p * q for _, p, q in order if q > 0))docker run --rm --name lab-lang-py -v "$PWD":/w -w /w python:3.13-slim python order.pyوالمخرج سوف يكون كما يلي:
reduce: 54
sum + generator: 54أما Haskell فهي لغة وظيفية نقية Purely Functional كما تصف نفسها في موقعها الرسمي، أي أن الدوال فيها لا تغير شيئاً، ولا توجد فيها حلقة for أو متغير يتغير أصلاً، والمسألة كلها سطر واحد يقرأ تقريباً مثل تعريف رياضي: «مجموع السعر × الكمية لكل سطر كميته أكبر من صفر». وهذا هو الملف order.hs:
order :: [(String, Int, Int)]
order = [("coffee", 12, 2), ("cake", 18, 1), ("juice", 9, 0), ("water", 3, 4)]
main :: IO ()
main = print (sum [p * q | (_, p, q) <- order, q > 0])docker run --rm --name lab-lang-hs -v "$PWD":/w -w /w haskell:9-slim runghc order.hsوالمخرج سوف يكون كما يلي:
54والصورة التالية تضع الحل الأمري والحل الوظيفي جنباً إلى جنب، لاحظ أن الأول يغير خانة واحدة في الذاكرة أربع مرات، والثاني ينتج قائمة جديدة في كل مرحلة:

وقد يتساءل البعض: هل الأسلوب الوظيفي أبطأ لأنه ينشئ قوائم جديدة؟ والإجابة أنه قد ينشئ قوائم وسيطة فعلاً كما في JavaScript أعلاه، وهذا لا يهم في طلب من أربعة أسطر، ولكنه يهم في ملايين العناصر، ولهذا تستخدم لغات مثل Python المولدات Generators التي تمرر العنصر تلو الآخر دون قائمة وسيطة، وتستخدم Haskell التقييم الكسول Lazy Evaluation. والقاعدة العملية أن تكتب الكود الأوضح أولاً، ثم تقيس قبل أن تغيره من أجل السرعة.
البرمجة التصريحية Declarative: صف النتيجة واترك الطريقة
في الأنماط السابقة كتبت بنفسك كيف يحسب الإجمالي، حتى في الأسلوب الوظيفي اخترت ترتيب filter ثم map ثم reduce. أما في البرمجة التصريحية Declarative Programming فأنت تصف ما تريده فقط، والنظام يختار الطريقة. وأشهر مثال تستخدمه كل يوم هو SQL، وهذا الملف order.sql:
CREATE TABLE order_lines (name text, price int, qty int);
INSERT INTO order_lines VALUES ('coffee', 12, 2), ('cake', 18, 1), ('juice', 9, 0), ('water', 3, 4);
SELECT sum(price * qty) AS order_total
FROM order_lines
WHERE qty > 0;
EXPLAIN SELECT sum(price * qty) FROM order_lines WHERE qty > 0;ونشغله على PostgreSQL في Container مؤقت، ثم نحذفه:
docker run -d --name lab-lang-pg -e POSTGRES_HOST_AUTH_METHOD=trust -v "$PWD":/w postgres:17.11-alpine
docker exec lab-lang-pg pg_isready -U postgres
docker exec lab-lang-pg psql -U postgres -f /w/order.sql
docker rm -f lab-lang-pgوالمخرج سوف يكون كما يلي:
CREATE TABLE
INSERT 0 4
order_total
-------------
54
(1 row)
QUERY PLAN
--------------------------------------------------------------------
Aggregate (cost=27.00..27.01 rows=1 width=8)
-> Seq Scan on order_lines (cost=0.00..25.00 rows=400 width=8)
Filter: (qty > 0)
(3 rows)في الإعداد أعلاه لاحظ التالي:
- الاستعلام لا يحتوي على حلقة ولا على متغير، وإنما يقول: «أعطني مجموع السعر × الكمية من الأسطر التي كميتها أكبر من صفر».
- الأمر
EXPLAINيكشف الخطة Query Plan التي اختارها مخطط الاستعلامات Planner في PostgreSQL: قراءة الجدول كله Seq Scan مع فلتر، ثم تجميع Aggregate. ولو كان على العمود فهرس Index وكان الجدول كبيراً فقد يختار المخطط طريقة أخرى دون أن تغير حرفاً في استعلامك، وهذا هو جوهر التصريحية كما يشرح توثيق PostgreSQL.
ونفس الفكرة في ملفات البنية التحتية، فتوثيق Terraform يقول صراحة إن لغة Terraform تصريحية، تصف الهدف المطلوب وليس الخطوات للوصول إليه، وترتيب الكتل في الملفات غير مهم. وهذا الملف main.tf يحسب نفس الإجمالي بلغة HCL دون أي مزود Provider، فقط لنرى الأسلوب:
locals {
order = [
{ name = "coffee", price = 12, qty = 2 },
{ name = "cake", price = 18, qty = 1 },
{ name = "juice", price = 9, qty = 0 },
{ name = "water", price = 3, qty = 4 },
]
total = sum([for l in local.order : l.price * l.qty if l.qty > 0])
}
output "order_total" {
value = local.total
}docker run --rm --name lab-lang-tf -v "$PWD":/w -w /w hashicorp/terraform:latest init
docker run --rm --name lab-lang-tf -v "$PWD":/w -w /w hashicorp/terraform:latest planوالمخرج من الأمر plan سوف يكون كما يلي:
Changes to Outputs:
+ order_total = 54
You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.ولاحظ أن local.total يستخدم local.order المعرف قبله، ولو نقلت تعريف total إلى أعلى الملف فلن يتغير شيء، لأن Terraform يبني ترتيب التنفيذ من العلاقات بين القيم وليس من ترتيب الأسطر. وفي الحياة الحقيقية تكتب بنفس الطريقة «أريد سيرفراً بهذه المواصفات وسجل DNS يشير إليه»، وTerraform يقارن الوصف بالواقع ويقرر ما الذي يجب إنشاؤه أو تعديله.
أما HTML وCSS فهما تصريحيتان بنفس المعنى: تكتب <button> ولا تشرح للمتصفح كيف يرسم الزر بكسلاً بكسلاً، وتكتب display: grid ولا تحسب مواقع العناصر بنفسك، والمتصفح يقرر الطريقة.
البرمجة المنطقية Logic Programming: حقائق وقواعد والسؤال عليك
البرمجة المنطقية Logic Programming تأخذ التصريحية إلى أبعد نقطة: تكتب حقائق Facts وقواعد Rules، ثم تسأل سؤالاً، ومحرك اللغة يبحث بنفسه عن الإجابات التي تحقق القواعد. وأشهر لغاتها Prolog، وهذا الملف order.pl بلغة SWI-Prolog:
line(coffee, 12, 2).
line(cake, 18, 1).
line(juice, 9, 0).
line(water, 3, 4).
line_total(Name, T) :- line(Name, P, Q), Q > 0, T is P * Q.
order_total(Total) :- findall(T, line_total(_, T), Ts), sum_list(Ts, Total).ونسأل المحرك عن الإجمالي، ثم عن كل سطر له إجمالي:
docker run --rm --name lab-lang-pl -v "$PWD":/w -w /w swipl:latest swipl -q \
-g "order_total(T), format('order total: ~w~n', [T]), forall(line_total(N, X), format('~w ~w~n', [N, X]))" \
-t halt order.plوالمخرج سوف يكون كما يلي:
order total: 54
coffee 24
cake 18
water 12في الكود أعلاه لاحظ أن القاعدة line_total تقرأ «إجمالي السطر Name هو T إذا كان هناك سطر سعره P وكميته Q، وكانت Q أكبر من صفر، وكانت T تساوي P × Q»، ولم نكتب حلقة تمر على الأسطر، وإنما المحرك هو الذي جرب كل الحقائق ووجد ثلاثة تحقق القاعدة، وتجاهل العصير لأن شرط Q > 0 فشل فيه. وهذا النمط قليل في تطبيقات الويب، ولكن أفكاره حاضرة في أنظمة القواعد ومحركات السياسات، وفي لغة Datalog التي تستخدمها بعض أدوات تحليل الكود.
البرمجة القائمة على الأحداث Event-Driven: الكود ينتظر ما يحدث
هناك أسلوب آخر لا يدخل في الشجرة السابقة لأنه يعمل فوق أي نمط، وهو البرمجة القائمة على الأحداث Event-Driven Programming. فبدلاً من أن يحدد البرنامج ترتيب التنفيذ من البداية، يسجل دوالاً تستجيب لأحداث Events: ضغطة زر في الواجهة، أو طلب HTTP وصل إلى السيرفر، أو منتج أضيف إلى السلة. ويقول توثيق Node.js إن أغلب واجهات Node.js مبنية على هذا الأسلوب. وهذا المثال في الملف order.js داخل مجلد آخر:
const { EventEmitter } = require("node:events");
const shop = new EventEmitter();
let total = 0;
shop.on("item-added", ({ name, price, qty }) => {
total += price * qty;
console.log(`event item-added: ${name} -> total ${total}`);
});
shop.on("checkout", () => console.log("event checkout: pay", total));
shop.emit("item-added", { name: "coffee", price: 12, qty: 2 });
shop.emit("item-added", { name: "cake", price: 18, qty: 1 });
shop.emit("item-added", { name: "water", price: 3, qty: 4 });
shop.emit("checkout");docker run --rm --name lab-lang-ev -v "$PWD":/w -w /w node:22-alpine node order.jsوالمخرج سوف يكون كما يلي:
event item-added: coffee -> total 24
event item-added: cake -> total 42
event item-added: water -> total 54
event checkout: pay 54ولاحظ أن الكود الذي يحسب الإجمالي لا يعرف متى سوف يضاف المنتج القادم، وإنما يستجيب عندما يحدث الحدث. وهذا هو الأساس الذي تقوم عليه حلقة الأحداث Event Loop في Node.js والمتصفح، وقد شرحنا حدودها في مقال المعالج والتزامن. والبرمجة التفاعلية Reactive Programming تبني على نفس الفكرة، ولكنها تتعامل مع الأحداث كتدفق Stream من القيم، وتطبق عليه map وfilter كما في البرمجة الوظيفية، ومن مكتباتها المعروفة ReactiveX مثل RxJS وRxJava.
أغلب اللغات الحديثة متعددة الأنماط Multi-Paradigm
بعد كل هذا قد يبدو أن كل لغة تتبع نمطاً واحداً، ولكن الواقع أن أغلب اللغات التي تستخدمها اليوم متعددة الأنماط Multi-Paradigm. فتوثيق Python نفسه في دليل البرمجة الوظيفية يقول إن Python وC++ وLisp لغات متعددة الأنماط، يمكنك أن تكتب بها برامج إجرائية أو كائنية أو وظيفية. وقد رأيت ذلك فعلاً في هذا المقال: Python كتبنا بها حلاً وظيفياً، وJavaScript كتبنا بها حلاً وظيفياً وحلاً قائماً على الأحداث، وJava منذ الإصدار 8 تدعم الدوال Lambdas وStreams. لذلك فالسؤال العملي ليس «ما هو نمط هذه اللغة؟» وإنما «ما هو النمط الأنسب لهذا الجزء من الكود؟»، فحساب إجمالي من قائمة يناسبه الأسلوب الوظيفي، ونموذج طلب له قواعد وحالات يناسبه الكلاس، والاستعلام عن البيانات مكانه SQL.
مترجمة Compiled أم مفسرة Interpreted؟
قبل الدخول في الأنواع بقي سؤال يخلط بينه وبين الأنواع كثير من المبتدئين، وهو هل اللغة مترجمة Compiled أم مفسرة Interpreted. وقد شرحنا ذلك بالتفصيل في مقال ماذا يحدث عندما تكتب برنامج Hello World وتشغله: الترجمة المسبقة Ahead-of-Time تحول الكود إلى تعليمات آلة قبل التشغيل كما في C وGo وRust، والمفسر يقرأ تعليمات وسيطة Bytecode وينفذها كما في CPython، والترجمة أثناء التشغيل JIT تترجم الأجزاء المتكررة وهي تعمل كما في Java وV8. والذي يهمنا هنا أن طريقة التنفيذ وصفة لتطبيق Implementation معين وليست للغة نفسها، فلغة Python لها مفسر CPython ولها PyPy الذي يستخدم JIT، وأن طريقة التنفيذ لا تحدد نظام الأنواع، فاللغة المترجمة قد تفحص الأنواع قبل التشغيل وقد لا تفحصها، كما سوف ترى في الأقسام التالية.
أنظمة الأنواع Type Systems: متى تكتشف اللغة أخطاءك؟
النوع Type هو الذي يحدد ما هي القيم المسموحة لمتغير، وما هي العمليات التي يمكن إجراؤها عليها: فالرقم يجمع ويضرب، والنص يقطع ويدمج، وجمع رقم مع نص سؤال يجيب عنه نظام الأنواع Type System في كل لغة بطريقته. وهناك سؤالان منفصلان يخلط بينهما كثيرون: متى تفحص اللغة الأنواع، وكم تتسامح عندما يختلف النوعان.
الأنواع الثابتة Static والديناميكية Dynamic: قبل التشغيل أم أثناءه؟
في الأنواع الثابتة Static Typing يعرف المترجم نوع كل متغير وكل تعبير قبل التشغيل، ويرفض البرنامج إذا وجد عملية لا تناسب الأنواع، وتوثيق Java يقول ذلك صراحة في مواصفات اللغة: كل متغير وكل تعبير له نوع معروف وقت الترجمة Compile Time. أما في الأنواع الديناميكية Dynamic Typing فالنوع يرتبط بالقيمة نفسها أثناء التشغيل Runtime، والمتغير يمكن أن يحمل رقماً ثم نصاً، والفحص يحدث فقط عندما ينفذ السطر. ولنرى الفرق بخطأ واحد في الحالتين، وهذا الملف report.py:
def total(order):
return sum(price * qty for price, qty in order)
def report(order, show_tax):
t = total(order)
if show_tax:
return "total with tax: " + t * 1.15
return f"total: {t}"
order = [(12, 2), (18, 1), (3, 4)]
print(report(order, show_tax=False))
print(report(order, show_tax=True))docker run --rm --name lab-lang-py -v "$PWD":/w -w /w python:3.13-slim python report.pyوالمخرج سوف يكون كما يلي:
total: 54
Traceback (most recent call last):
File "/w/report.py", line 12, in <module>
print(report(order, show_tax=True))
~~~~~~^^^^^^^^^^^^^^^^^^^^^^
File "/w/report.py", line 7, in report
return "total with tax: " + t * 1.15
~~~~~~~~~~~~~~~~~~~^~~~~~~~~~
TypeError: can only concatenate str (not "float") to strلاحظ أن البرنامج عمل وطبع السطر الأول بنجاح، والخطأ في السطر 7 كان موجوداً من البداية، ولكن Python لم تكتشفه إلا عندما نفذ الفرع show_tax=True. ولو كان هذا الفرع لا ينفذ إلا في نهاية الشهر عند إصدار الفواتير، فسوف يمر البرنامج من كل اختباراتك اليدوية ويصل إلى السيرفر، ويكتشف المستخدم الخطأ قبلك. والآن نفس البرنامج بلغة Go في الملف main.go:
package main
import "fmt"
func total(order [][2]int) int {
sum := 0
for _, l := range order {
sum += l[0] * l[1]
}
return sum
}
func report(order [][2]int, showTax bool) string {
t := total(order)
if showTax {
return "total with tax: " + float64(t)*1.15
}
return fmt.Sprintf("total: %d", t)
}
func main() {
order := [][2]int{{12, 2}, {18, 1}, {3, 4}}
fmt.Println(report(order, false))
}docker run --rm --name lab-lang-go -v "$PWD":/w -w /w golang:1.27.1-alpine3.24 go run main.goوالمخرج سوف يكون كما يلي:
# command-line-arguments
./main.go:16:10: invalid operation: "total with tax: " + float64(t) * 1.15 (mismatched types untyped string and float64)لاحظ أن الدالة main لا تستدعي الفرع الخاطئ أصلاً، فهي تمرر false، ومع ذلك رفض المترجم البرنامج كله ولم يطبع حتى السطر الأول، لأنه يفحص كل الكود وليس فقط الأسطر التي سوف تنفذ. والصورة التالية تبين أين يمسك كل نظام هذا الخطأ:

وقد يتساءل البعض: إذا كانت الأنواع الثابتة تمسك الأخطاء مبكراً فلماذا لا تستخدمها كل اللغات؟ والإجابة أن لها ثمناً: كود أكثر لتعريف الأنواع، وخطوة ترجمة تنتظرها، وبعض الأشياء التي تكتب في سطر واحد في Python مثل قراءة JSON بشكل غير معروف مسبقاً تحتاج في Java عملاً أكثر. والأنواع الديناميكية تعطيك سرعة في البداية ومرونة في السكربتات الصغيرة، ولكن كلما كبر الكود وزاد عدد المطورين زادت قيمة المترجم الذي يمسك الأخطاء عنك، ولهذا السبب ظهرت الأنواع التدريجية التي سوف نراها بعد قليل. ولاحظ أن الاختبارات Tests تعوض جزءاً من ذلك في اللغات الديناميكية، فالخطأ أعلاه كان سوف يظهر في اختبار يغطي الفرع show_tax=True، ولكنها لا تفحص إلا الحالات التي كتبت لها اختباراً.
الأنواع القوية Strong والضعيفة Weak: هل تحول اللغة الأنواع بصمت؟
السؤال الثاني مستقل عن الأول: عندما تجمع نصاً مع رقم، هل ترفض اللغة العملية، أم تحول أحدهما إلى نوع الآخر بصمت Implicit Conversion وتكمل؟ اللغة التي ترفض تسمى قوية الأنواع Strongly Typed، والتي تحول بصمت تسمى ضعيفة الأنواع Weakly Typed. وهذا المثال بلغة JavaScript في الملف weak.js:
console.log("1" + 1);
console.log("5" - 2);
console.log("5" * "2");
console.log(1 == "1", 1 === "1");
console.log([] + {});docker run --rm --name lab-lang-node -v "$PWD":/w -w /w node:22-alpine node weak.jsوالمخرج سوف يكون كما يلي:
11
3
10
true false
[object Object]لاحظ أن "1" + 1 أعطت النص 11 لأن + مع نص تعني الدمج، بينما "5" - 2 أعطت الرقم 3 لأن الطرح لا يعمل إلا على الأرقام فحولت النص إلى رقم، و1 == "1" صحيحة لأن == تحول الأنواع قبل المقارنة، و=== لا تحول فأعطت false. وتوثيق MDN يصف JavaScript بأنها لغة ضعيفة الأنواع تسمح بالتحويل الضمني بدلاً من رمي خطأ، ولهذا السبب ينصح دائماً باستخدام ===. والآن نفس العملية في Python، في الملف strong.py:
print("5" * 2)
print(int("1") + 1)
print("1" + 1)docker run --rm --name lab-lang-py -v "$PWD":/w -w /w python:3.13-slim python strong.pyوالمخرج سوف يكون كما يلي:
55
2
Traceback (most recent call last):
File "/w/strong.py", line 3, in <module>
print("1" + 1)
~~~~^~~
TypeError: can only concatenate str (not "int") to strلاحظ أن Python ديناميكية وقوية معاً: لم تفحص الأنواع قبل التشغيل، ولكنها عندما وصلت إلى "1" + 1 رفضت أن تخمن ما تريده ورمت TypeError، والتحويل لا يحدث إلا إذا طلبته صراحة ب int("1"). ولاحظ أيضاً أن "5" * 2 أعطت 55، وهذا ليس تحويلاً للأنواع، وإنما عملية معرفة لتكرار النص، فالقوة لا تعني أن كل عملية بين نوعين ممنوعة.
وقد يتفاجأ البعض بأن لغة ثابتة الأنواع مثل C ضعيفة في نفس الوقت، وهذا الملف weak.c يبين السبب:
#include <stdio.h>
int main(void) {
char c = 'A';
int n = c + 1;
double price = 12.99;
int cents = price;
printf("%d %d\n", n, cents);
if (-1 < sizeof(int))
printf("-1 is less than 4\n");
else
printf("-1 is NOT less than 4\n");
return 0;
}docker run --rm --name lab-lang-c -v "$PWD":/w -w /w rust:1-alpine sh -c 'gcc -o /tmp/weak weak.c && /tmp/weak && gcc -Wall -Wextra -o /tmp/weak weak.c'والمخرج سوف يكون كما يلي:
66 12
-1 is NOT less than 4
$ gcc -Wall -Wextra -o weak weak.c
weak.c: In function 'main':
weak.c:9:12: warning: comparison of integer expressions of different signedness: 'int' and 'long unsigned int' [-Wsign-compare]
9 | if (-1 < sizeof(int))
| ^في الكود أعلاه لاحظ التالي:
- الحرف
'A'أصبح الرقم 66 بعد الجمع لأن الحروف في C أرقام صغيرة، والسعر12.99أصبح 12 بصمت عند وضعه في متغيرint، دون أي خطأ. - والأخطر أن
-1 < sizeof(int)كانت خاطئة، فالبرنامج يقول إن -1 ليست أصغر من 4. والسبب أنsizeofتعيد نوعاً بدون إشارة Unsigned، فتحول C الرقم -1 إليه بقواعد التحويلات الحسابية المعتادة Usual Arithmetic Conversions كما في مرجع cppreference، فيصبح أكبر رقم ممكن. - الترجمة العادية لم تظهر أي تحذير، والتحذير
-Wsign-compareظهر فقط مع-Wall -Wextra، ولهذا لا تقم بترجمة كود C بدون هذه الخيارات، والسبب أنها الطريقة الوحيدة لتعرف أن المترجم حول شيئاً بصمت، كما يشرح توثيق GCC.
والصورة التالية تضع اللغات على السؤالين معاً:

ولاحظ أن «قوية» و«ضعيفة» ليس لهما تعريف رسمي متفق عليه مثل «ثابتة» و«ديناميكية»، وإنما هما طيف Spectrum: فC++ أقوى من C في بعض المواضع، وPHP تصبح أكثر صرامة مع declare(strict_types=1) في معاملات الدوال كما في توثيق PHP، وTypeScript تفحص الأنواع قبل التشغيل ثم يعمل الكود كJavaScript بكل تحويلاتها. لذلك فالمواقع في الصورة تقريبية، والمهم أن تعرف كيف تتصرف لغتك في الحالات التي تخصك.
استنتاج الأنواع Type Inference: ثابتة دون أن تكتب النوع
من أكثر ما ينفر المبتدئين من الأنواع الثابتة أنهم يتخيلون كتابة النوع بجانب كل متغير كما في Java القديمة. ولكن أغلب اللغات الثابتة الحديثة تستنتج النوع Type Inference من القيمة التي تضعها، فتكتب كأنك في لغة ديناميكية، ويبقى الفحص ثابتاً. وهذا مثال بلغة Go في الملف main.go:
package main
import "fmt"
func main() {
total := 54
price := 12.5
name := "coffee"
fmt.Printf("%T %T %T\n", total, price, name)
// total = "fifty four"
}docker run --rm --name lab-lang-go -v "$PWD":/w -w /w golang:1.27.1-alpine3.24 go run main.goوالمخرج سوف يكون كما يلي:
int float64 stringلم نكتب كلمة int أو string في أي مكان، والمعامل := في Go يعرف المتغير ويستنتج نوعه كما في مواصفات Go. والآن نزيل التعليق عن السطر الأخير لنضع نصاً في المتغير total:
# command-line-arguments
./main.go:10:10: cannot use "fifty four" (untyped string constant) as int value in assignmentلاحظ أن النوع استنتج مرة واحدة عند التعريف، ثم أصبح ثابتاً، فالمتغير total هو int إلى الأبد. ونفس الشيء في Rust مع let، وهذا الملف main.rs:
fn main() {
let prices = vec![12, 18, 3];
let mut total = 0;
for p in &prices {
total += p;
}
total = "thirty three";
println!("{total}");
}docker run --rm --name lab-lang-rs -v "$PWD":/w -w /w rust:1-alpine rustc main.rsوالمخرج سوف يكون كما يلي:
error[E0308]: mismatched types
--> main.rs:7:13
|
3 | let mut total = 0;
| - expected due to this value
...
7 | total = "thirty three";
| ^^^^^^^^^^^^^^ expected integer, found `&str`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.لاحظ أن رسالة Rust تشير إلى السطر الذي استنتجت منه النوع expected due to this value، ثم إلى السطر الذي خالفه، وهذا ما يجعل الاستنتاج مريحاً: المترجم يشرح لك من أين جاء بالنوع. وتوثيق Rust يقول في فصل أنواع البيانات إن المترجم يستطيع غالباً استنتاج النوع من القيمة وطريقة استخدامها، ونفس الفكرة في TypeScript كما سوف ترى في القسم التالي.
الأنواع التدريجية Gradual Typing: TypeScript وPython مع mypy
لنفرض أن لديك مشروعاً كبيراً بلغة JavaScript أو Python، وبدأت الأخطاء من نوع «undefined is not a function» تصل إلى المستخدمين، فإعادة كتابة المشروع بلغة ثابتة الأنواع ليست خياراً واقعياً. وهنا جاءت الأنواع التدريجية Gradual Typing، وفكرتها التي تشرحها PEP 483 أن تضيف الأنواع إلى جزء من الكود، ويبقى الباقي ديناميكياً، ثم تزيد التغطية تدريجياً.
ولنبدأ ب TypeScript، وهي JavaScript مع أنواع، والملف order.ts التالي فيه خطآن مقصودان:
interface Line {
name: string;
price: number;
qty: number;
}
function total(order: Line[]): number {
return order.filter((l) => l.qty > 0).reduce((s, l) => s + l.price * l.qty, 0);
}
let count = 4; // inferred as number
count = "four";
const order: Line[] = [
{ name: "coffee", price: 12, qty: 2 },
{ name: "cake", price: "18", qty: 1 },
];
console.log(total(order));ونثبت المترجم tsc داخل Container ثم نفحص الملف ونشغل الناتج:
docker run --rm --name lab-lang-ts -v "$PWD":/w -w /w node:22-alpine sh -c \
'npm i -g [email protected] >/dev/null 2>&1; tsc --strict --target es2022 order.ts; node order.js'والمخرج سوف يكون كما يلي:
order.ts(12,1): error TS2322: Type 'string' is not assignable to type 'number'.
order.ts(16,19): error TS2322: Type 'string' is not assignable to type 'number'.
42في المخرج أعلاه لاحظ التالي:
- الخطأ الأول في السطر 12 جاء من استنتاج النوع:
countبدأ رقماً فأصبحnumber، والخطأ الثاني في السطر 16 لأن السعر"18"نص والواجهةLineتقولnumber. - ومع وجود الخطأين أنشأ
tscالملفorder.jsوعمل وطبع 42 بدلاً من 54، فقد ضربت JavaScript النص"18"في 1 فحولته إلى رقم بصمت. والسبب أن الخيار noEmitOnError معطل افتراضياً، ولهذا اجعلهtrueفي مشروعك، أو اجعل خط CI يفشل عندما يفشلtsc.
وإذا صححنا الخطأين، فهذا هو ملف JavaScript الذي ينتجه tsc من الملف ok.ts:
"use strict";
function total(order) {
return order.filter((l) => l.qty > 0).reduce((s, l) => s + l.price * l.qty, 0);
}
const order = [
{ name: "coffee", price: 12, qty: 2 },
{ name: "cake", price: 18, qty: 1 },
];
console.log(total(order));لاحظ أن الواجهة Line وكل : number اختفت تماماً، وهذا ما يسميه توثيق TypeScript الأنواع الممحوة Erased Types: الأنواع موجودة فقط وقت الفحص، والذي يعمل في المتصفح أو Node.js هو JavaScript عادية لا تعرف شيئاً عنها. لذلك فالبيانات التي تأتي من خارج برنامجك، مثل جسم طلب HTTP أو ملف JSON، لا يحميها TypeScript وحده، وتحتاج إلى تحقق Validation وقت التشغيل.
وفي Python جاءت تلميحات الأنواع Type Hints مع PEP 484، ولكن لاحظ الجملة الأولى في توثيق الوحدة typing: مفسر Python لا يفرض هذه التلميحات، وإنما تستخدمها أدوات خارجية مثل mypy. وهذا الملف app.py:
from dataclasses import dataclass
@dataclass
class Line:
name: str
price: int
qty: int
def total(order: list[Line]) -> int:
return sum(l.price * l.qty for l in order if l.qty > 0)
def receipt(order: list[Line]) -> str:
return "total: " + total(order)
def legacy(order):
return "total: " + total(order)
order = [Line("coffee", 12, 2), Line("cake", "18", 1)]
print(order[1])
print(total(order))نشغله أولاً بـ Python مباشرة:
docker run --rm --name lab-lang-py -v "$PWD":/w -w /w python:3.13-slim python app.pyوالمخرج سوف يكون كما يلي:
Line(name='cake', price='18', qty=1)
Traceback (most recent call last):
File "/w/app.py", line 25, in <module>
print(total(order))
~~~~~^^^^^^^
File "/w/app.py", line 12, in total
return sum(l.price * l.qty for l in order if l.qty > 0)
TypeError: unsupported operand type(s) for +: 'int' and 'str'لاحظ أن ال Dataclass قبلت السعر '18' كنص رغم أن التلميح يقول int، ولم يظهر الخطأ إلا عند الجمع داخل total. والآن نفحص نفس الملف ب mypy قبل التشغيل:
docker run --rm --name lab-lang-mypy -v "$PWD":/w -w /w python:3.13-slim sh -c \
'pip install -q mypy==2.4.0 && mypy app.py; mypy --check-untyped-defs app.py'والمخرج سوف يكون كما يلي:
app.py:16: error: Unsupported operand types for + ("str" and "int") [operator]
app.py:23: error: Argument 2 to "Line" has incompatible type "str"; expected "int" [arg-type]
Found 2 errors in 1 file (checked 1 source file)
$ mypy --check-untyped-defs app.py
app.py:16: error: Unsupported operand types for + ("str" and "int") [operator]
app.py:20: error: Unsupported operand types for + ("str" and "int") [operator]
app.py:23: error: Argument 2 to "Line" has incompatible type "str"; expected "int" [arg-type]
Found 3 errors in 1 file (checked 1 source file)في المخرج أعلاه لاحظ التالي:
- mypy وجد خطأ السعر في السطر 23 قبل أن يعمل البرنامج، ووجد أيضاً خطأ في السطر 16 داخل الدالة
receiptلم يظهر في التشغيل أصلاً لأن أحداً لم يستدعها، وهذه بالضبط فائدة الفحص الثابت. - الدالة
legacyفيها نفس الخطأ في السطر 20، ولكن mypy تجاهلها في الفحص الأول لأنها بدون تلميحات، فهذا هو الجزء الديناميكي من «التدريجي». ومع الخيار--check-untyped-defsفحص بداخلها أيضاً فظهرت ثلاثة أخطاء. لذلك لا تقم بإضافة mypy لمشروع قديم ثم تفترض أن كل الكود مفحوص، والسبب أن الدوال بدون تلميحات تبقى خارج الفحص حتى تضيف لها التلميحات أو تفعل هذا الخيار.
والصورة التالية تجمع الطريقين:

الأنواع الاسمية Nominal والبنيوية Structural: هل يهم اسم النوع أم شكله؟
عندما تكتب دالة تستقبل «أي شيء له دالة total()»، يبقى سؤال: كيف تقرر اللغة أن الكائن الذي مررته مقبول؟ في النظام الاسمي Nominal Typing يجب أن يعلن الكلاس صراحة أنه يطبق الواجهة، وفي النظام البنيوي Structural Typing يكفي أن يملك الكائن الدوال المطلوبة بنفس الأشكال. وهذا الملف Main.java، فيه كلاس Invoice له دالة total() ولكنه لا يذكر الواجهة:
interface HasTotal {
int total();
}
class Invoice {
public int total() { return 54; }
}
public class Main {
static void printTotal(HasTotal x) {
System.out.println("total: " + x.total());
}
public static void main(String[] args) {
printTotal(new Invoice());
}
}docker run --rm --name lab-lang-java -v "$PWD":/w -w /w eclipse-temurin:25-jdk-alpine java Main.javaوالمخرج سوف يكون كما يلي:
Main.java:15: error: method printTotal in class Main cannot be applied to given types;
printTotal(new Invoice());
^
required: HasTotal
found: Invoice
reason: argument mismatch; Invoice cannot be converted to HasTotal
1 error
error: compilation failedرفضت Java الكلاس رغم أن شكله مطابق تماماً، والحل الوحيد أن تكتب class Invoice implements HasTotal. والآن نفس الشيء بلغة Go في الملف main.go:
package main
import "fmt"
type HasTotal interface {
Total() int
}
type Invoice struct{}
func (Invoice) Total() int { return 54 }
func printTotal(x HasTotal) {
fmt.Println("total:", x.Total())
}
func main() {
printTotal(Invoice{})
}docker run --rm --name lab-lang-go -v "$PWD":/w -w /w golang:1.27.1-alpine3.24 go run main.goوالمخرج سوف يكون كما يلي:
total: 54قبلت Go النوع Invoice دون أن يذكر اسم HasTotal في أي مكان، لأن الأسئلة الشائعة للغة Go تقول إن النوع يطبق الواجهة بمجرد أن يطبق دوالها، ولا توجد كلمة implements في اللغة أصلاً. وTypeScript بنيوية أيضاً كما يقول توثيق التوافق بين الأنواع، وهذا الملف structural.ts:
interface HasTotal {
total(): number;
}
class Invoice {
total() { return 54; }
}
function printTotal(x: HasTotal) {
console.log("total:", x.total());
}
printTotal(new Invoice());
printTotal({ total: () => 42 });
printTotal({ sum: () => 42 });docker run --rm --name lab-lang-ts -v "$PWD":/w -w /w node:22-alpine sh -c \
'npm i -g [email protected] >/dev/null 2>&1; tsc --strict --target es2022 structural.ts; node structural.js'والمخرج سوف يكون كما يلي:
structural.ts(15,14): error TS2353: Object literal may only specify known properties, and 'sum' does not exist in type 'HasTotal'.
total: 54
total: 42
/w/structural.js:6
console.log("total:", x.total());
^
TypeError: x.total is not a functionلاحظ أن الكلاس Invoice والكائن الحرفي { total: () => 42 } قبلا لأن شكلهما مطابق، أما الكائن الذي فيه sum بدلاً من total فرفضه tsc، ولأن الملف أنشئ رغم الخطأ كما رأينا من قبل فقد وصل الخطأ نفسه إلى وقت التشغيل. والصورة التالية تلخص الفرق:

وقد يتساءل البعض: أيهما أفضل؟ والإجابة أن الاسمي يجعل العلاقة موثقة في الكود، فلا يطابق نوع واجهة بالصدفة لأن له دالة بنفس الاسم ومعنى مختلف، أما البنيوي فيسمح لك بتعريف واجهة لنوع من مكتبة لا تملك تعديلها، وهذا ما تذكره الأسئلة الشائعة للغة Go كسبب لاختيارها.
الحماية من Null أو Null Safety: الخطأ الذي سماه صاحبه خطأ المليار دولار
أشهر خطأ في تاريخ البرمجة هو أن تستخدم متغيراً قيمته null أو undefined وأنت تظن أن فيه قيمة. وهذا مثال بلغة Java في الملف Main.java، يبحث عن كوبون خصم غير موجود:
import java.util.Map;
public class Main {
static Map<String, String> coupons = Map.of("WELCOME", "10%");
static String findCoupon(String code) {
return coupons.get(code);
}
public static void main(String[] args) {
String coupon = findCoupon("SUMMER");
System.out.println(coupon.length());
}
}docker run --rm --name lab-lang-java -v "$PWD":/w -w /w eclipse-temurin:25-jdk-alpine \
sh -c 'javac -g Main.java && java Main'والمخرج سوف يكون كما يلي:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "coupon" is null
at Main.main(Main.java:12)ترجم البرنامج بدون أي تحذير لأن النوع String في Java يقبل null، وانفجر عند التشغيل. ولاحظ أن الرسالة تذكر اسم المتغير coupon، وهذه الرسائل المفصلة جاءت مع JEP 358، ولكن اسم المتغير يظهر فقط إذا ترجمت مع الخيار -g، وبدونه سوف تقرأ "<local1>" is null كما ظهر لنا عند التشغيل المباشر ب java Main.java.
والحل في اللغات الحديثة أن يصبح «قد لا توجد قيمة» جزءاً من النوع نفسه، فيمنعك المترجم من استخدام القيمة قبل أن تتعامل مع حالة غيابها. ففي TypeScript مع الخيار strictNullChecks، وهذا الملف nulls.ts:
const coupons: Record<string, number> = { WELCOME: 10 };
function findCoupon(code: string): number | undefined {
return coupons[code];
}
const pct = findCoupon("SUMMER");
console.log(pct.toFixed(1));docker run --rm --name lab-lang-ts -v "$PWD":/w -w /w node:22-alpine sh -c \
'npm i -g [email protected] >/dev/null 2>&1; tsc --strict --target es2022 nulls.ts;
tsc --strict --strictNullChecks false --target es2022 nulls.ts && node nulls.js'والمخرج سوف يكون كما يلي:
nulls.ts(8,13): error TS18048: 'pct' is possibly 'undefined'.
/w/nulls.js:7
console.log(pct.toFixed(1));
^
TypeError: Cannot read properties of undefined (reading 'toFixed')مع --strict رفض tsc الكود لأن النوع number | undefined يقول صراحة إن القيمة قد لا توجد، وعندما عطلنا strictNullChecks مر الفحص بنجاح وانفجر الكود عند التشغيل كما في Java تماماً. لذلك لا تقم بتعطيل strict في مشروع TypeScript جديد، والسبب أنك تعطل بذلك أهم فحص فيه.
أما Rust فلا يوجد فيها null أصلاً، والقيمة التي قد لا توجد نوعها Option، وهو إما Some(value) أو None. وهذا الملف main.rs يحاول استخدام النتيجة مباشرة:
use std::collections::HashMap;
fn find_coupon(coupons: &HashMap<&str, u32>, code: &str) -> Option<u32> {
coupons.get(code).copied()
}
fn main() {
let coupons = HashMap::from([("WELCOME", 10)]);
let pct: u32 = find_coupon(&coupons, "SUMMER");
println!("{pct}");
}docker run --rm --name lab-lang-rs -v "$PWD":/w -w /w rust:1-alpine rustc main.rsوالمخرج سوف يكون كما يلي:
error[E0308]: mismatched types
--> main.rs:9:20
|
9 | let pct: u32 = find_coupon(&coupons, "SUMMER");
| --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found `Option<u32>`
| |
| expected due to this
|
= note: expected type `u32`
found enum `Option<u32>`
help: consider using `Option::expect` to unwrap the `Option<u32>` value, panicking if the value is an `Option::None`
|
9 | let pct: u32 = find_coupon(&coupons, "SUMMER").expect("REASON");
| +++++++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.رفض المترجم أن تعامل Option<u32> كأنها u32، واقترح expect التي تنهي البرنامج إذا لم توجد قيمة، ولكن الطريقة الصحيحة أن تتعامل مع الحالتين ب match، كما في الملف ok.rs:
use std::collections::HashMap;
fn find_coupon(coupons: &HashMap<&str, u32>, code: &str) -> Option<u32> {
coupons.get(code).copied()
}
fn main() {
let coupons = HashMap::from([("WELCOME", 10)]);
for code in ["WELCOME", "SUMMER"] {
match find_coupon(&coupons, code) {
Some(pct) => println!("{code}: {pct}% off"),
None => println!("{code}: no such coupon"),
}
}
}docker run --rm --name lab-lang-rs -v "$PWD":/w -w /w rust:1-alpine sh -c 'rustc ok.rs -o /tmp/ok && /tmp/ok'والمخرج سوف يكون كما يلي:
WELCOME: 10% off
SUMMER: no such couponونفس الفكرة في Kotlin، فالنوع String لا يقبل null، والنوع String? يقبله ويجبرك المترجم على فحصه أو استخدام ?. قبل الوصول إليه، كما في توثيق Kotlin، وفي Swift بنفس الطريقة مع ال Optionals.
الأنواع العامة Generics: دالة واحدة لأكثر من نوع
آخر مفهوم في هذا القسم، لنفرض أنك تريد دالة Sum تجمع قائمة أرقام صحيحة، وأخرى تجمع أرقاماً عشرية، فبدون الأنواع العامة Generics سوف تكتب الدالة مرتين، أو تكتب دالة تقبل أي شيء فتخسر الفحص الثابت. والأنواع العامة تسمح لك بكتابة الدالة مرة واحدة مع معامل نوع Type Parameter، كما في هذا الملف بلغة Go:
package main
import "fmt"
func Sum[T int | float64](xs []T) T {
var s T
for _, x := range xs {
s += x
}
return s
}
func main() {
fmt.Println(Sum([]int{24, 18, 12}))
fmt.Println(Sum([]float64{12.5, 18.25}))
fmt.Println(Sum([]string{"a", "b"}))
}بدون السطر الأخير يعمل البرنامج ويكون المخرج كما يلي:
54
30.75ومع السطر الأخير الذي يمرر قائمة نصوص يرفض المترجم البرنامج:
# command-line-arguments
./main.go:16:17: string does not satisfy int | float64 (string missing in int | float64)لاحظ أن القيد int | float64 يحدد الأنواع المسموحة، فالدالة عامة ولكنها ليست مفتوحة لأي شيء، والمترجم يفحص كل استدعاء بنوعه. والأنواع العامة موجودة في Java وC# وRust وTypeScript وغيرها، ويشرحها دليل Go وتوثيق TypeScript.
مقارنة أشهر اللغات
الجدول التالي يجمع ما سبق لأشهر اللغات، وكل خلية فيه من التوثيق الرسمي للغة، وعمود «قوية أو ضعيفة» تصنيف تقريبي حسب السلوك كما شرحنا، وطريقة التنفيذ هي للتطبيق الرسمي أو الأشهر لكل لغة:
| اللغة | الأنماط Paradigms | الأنواع Types | التنفيذ Execution | إدارة الذاكرة Memory Management | الاستخدام الشائع |
|---|---|---|---|---|---|
| C | إجرائية | ثابتة، ضعيفة | ترجمة مسبقة AOT | يدوية malloc وfree | أنظمة التشغيل والأنظمة المدمجة Embedded |
| C++ | متعددة: إجرائية وكائنية وGeneric | ثابتة، ضعيفة نسبياً بسبب تحويلات C | AOT | يدوية مع RAII | الألعاب والمتصفحات وقواعد البيانات |
| Java | كائنية مع عناصر وظيفية | ثابتة، قوية كما في JLS | Bytecode على JVM مع JIT | GC | أنظمة المؤسسات وتطبيقات Android |
| C# | كائنية مع عناصر وظيفية | ثابتة، قوية | CIL على CLR مع JIT | GC | تطبيقات .NET والمؤسسات وألعاب Unity |
| Kotlin | كائنية ووظيفية | ثابتة، قوية، مع Null Safety | Bytecode على JVM، وأيضاً JavaScript وNative | GC على JVM | تطبيقات Android والخوادم |
| Go | إجرائية مع واجهات وبدون وراثة | ثابتة، قوية، واجهات بنيوية | AOT | GC | الخوادم وأدوات السحابة وسطر الأوامر |
| Rust | متعددة: إجرائية ووظيفية مع Traits | ثابتة، قوية، Option بدلاً من null | AOT | الملكية Ownership بدون GC | برمجة الأنظمة وأدوات سطر الأوامر وWebAssembly |
| Swift | متعددة: كائنية وProtocols ووظيفية | ثابتة، قوية، Optionals | AOT | ARC عد المراجع | تطبيقات أجهزة Apple |
| Python | متعددة | ديناميكية، قوية، مع تلميحات اختيارية | Bytecode مع مفسر CPython | عد المراجع مع جامع للدورات | تحليل البيانات والأتمتة والويب |
| JavaScript | متعددة: Prototypes ووظيفية وأحداث | ديناميكية، ضعيفة | محركات JIT مثل V8 | GC | الويب في المتصفح والخوادم مع Node.js |
| TypeScript | مثل JavaScript | ثابتة تدريجية بنيوية، تمحى عند الترجمة | تترجم إلى JavaScript | مثل JavaScript | تطبيقات الويب الكبيرة |
| PHP | إجرائية وكائنية | ديناميكية، ضعيفة افتراضياً مع strict_types اختياري | Bytecode مع OPcache، وJIT معطل افتراضياً منذ PHP 8.4 | عد المراجع مع جامع للدورات | مواقع وتطبيقات الويب |
| Ruby | كائنية وكل شيء فيها كائن، مع عناصر وظيفية | ديناميكية، قوية | Bytecode مع YJIT اختياري | GC | تطبيقات الويب مع Rails |
| Haskell | وظيفية نقية | ثابتة، قوية، استنتاج للأنواع | AOT عبر GHC | GC | المترجمات والبحث والتعليم |
| SQL | تصريحية | أعمدة لها أنواع يفحصها محرك قاعدة البيانات | المخطط يختار خطة التنفيذ | يديرها محرك قاعدة البيانات | الاستعلام عن البيانات |
ولاحظ في الجدول أن عمود إدارة الذاكرة له ثلاث طرق رئيسية: يدوية كما في C حيث تحجز الذاكرة وتحررها بنفسك فتأتي معها أخطاء مثل استخدام ذاكرة بعد تحريرها، وجامع النفايات Garbage Collector كما في Java وGo وPython حيث يبحث برنامج داخل التطبيق عن الذاكرة التي لم يعد يستخدمها أحد، والملكية Ownership في Rust حيث يفحص المترجم قبل التشغيل من يملك كل قيمة ومتى تحرر، فتحصل على الأمان بدون جامع نفايات، وثمنه قواعد يتعلمها المبتدئ ببطء.
ماذا يعني هذا عند اختيار لغة لمشروعك؟
بعد هذه الجولة لا توجد لغة «أفضل» بشكل مطلق، وإنما لغة أنسب لمشكلة وفريق معين. وهذه أسئلة عملية تساعدك:
- حجم الكود وعمره وعدد المطورين: سكربت لمرة واحدة يناسبه Python أو Bash، أما مشروع يعيش سنوات ويعمل عليه عدة مطورين فقيمة الأنواع الثابتة Static أو التدريجية Gradual فيه كبيرة، لأن المترجم يمسك ما ينساه المطورون عند تعديل كود لم يكتبوه.
- البيئة التي يعمل فيها: المتصفح يعني JavaScript أو TypeScript، وقاعدة البيانات تعني SQL، وتطبيق iPhone يعني Swift غالباً.
- كلفة الخطأ: برنامج يتعامل مع المال أو الذاكرة مباشرة يستحق لغة ترفض أكثر قبل التشغيل، والسبب أن الخطأ الذي يصل إلى المستخدم هناك مكلف جداً.
- المشروع القائم: إذا كان مشروعك بلغة ديناميكية فلا تقم بإعادة كتابته، وإنما أضف TypeScript أو mypy تدريجياً واجعل الفحص جزءاً من خط CI، وابدأ بالملفات التي تتغير كثيراً.
وتذكر أن تعلم لغة من نمط مختلف عن لغتك الأساسية يغير طريقة كتابتك حتى في لغتك، فالمطور الذي جرب Haskell أو SQL جيداً يكتب حلقات أقل ودوالاً نقية أكثر في Java وPython.
تدريب عملي
- أضف إلى مثال Java كلاساً ثالثاً للخصم اسمه
FixedOffيطرح مبلغاً ثابتاً، ثم شغل البرنامج ولاحظ أن الكلاسOrderلم يتغير فيه أي سطر. - غير في الملف
app.pyالدالةlegacyبإضافة تلميحات للمعامل وقيمة الإرجاع، ثم شغلmypy app.pyبدون--check-untyped-defsولاحظ ظهور الخطأ. - أضف
"noEmitOnError": trueإلى ملفtsconfig.jsonفي مشروع TypeScript، ثم تأكد أنorder.jsلا ينشأ عند وجود الخطأ.
وبعد التدريب تأكد أنه لم يبق أي Container من المختبر:
docker ps -a --filter name=lab-langالخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- توجد لغات كثيرة لأن كل لغة تختار نقطة مختلفة بين السرعة وسهولة الكتابة والأمان والبيئة التي تعمل فيها.
- الأمرية Imperative تصف الخطوات وتغير الحالة، ومنها الإجرائية Procedural والكائنية OOP، والتصريحية Declarative تصف النتيجة، ومنها الوظيفية Functional وSQL وTerraform والمنطقية Logic، وأغلب اللغات الحديثة تجمع أكثر من نمط.
- الأنواع الثابتة Static تمسك الخطأ قبل التشغيل حتى في الأسطر التي لا تنفذ، والديناميكية Dynamic تمسكه فقط عندما ينفذ السطر، وقد يكون ذلك عند المستخدم.
- القوة Strong والضعف Weak سؤال آخر: هل تحول اللغة الأنواع بصمت؟ وPython ديناميكية قوية Dynamic Strong، وC ثابتة ضعيفة Static Weak، وJavaScript ديناميكية ضعيفة Dynamic Weak.
- استنتاج الأنواع Type Inference يعطيك الفحص الثابت دون كتابة الأنواع في كل سطر، والأنواع التدريجية Gradual Typing مع TypeScript وmypy تضيف الفحص إلى مشروع قائم، والأنواع فيها تمحى أو تتجاهل وقت التشغيل.
- Java تقبل النوع باسمه المعلن Nominal، وGo وTypeScript تقبلانه بشكله Structural، وKotlin وRust وTypeScript مع strict تجبرك على التعامل مع غياب القيمة قبل استخدامها.
دليلماذا يحدث عندما تشغل برنامج Hello World؟نتتبع برنامج C صغيراً من المحرر إلى الشاشة، فترى كل طبقة يمر بها في الجهاز حتى تظهر الجملة أمامك.
دليلاللغة شيء والمترجم شيء آخر: من يقرر مستقبل لغات البرمجة؟لماذا لا تصح جمل مثل «Python بطيئة»، وما الفرق بين مواصفة اللغة والبرامج التي تطبقها، ومن يقرر مستقبلها.
دليلقصص لغات البرمجة ومن صنعها، من Fortran إلى Rustمن صنع اللغات التي نكتب بها كل يوم، وما المشكلة التي دفعته إلى ذلك، وكيف تشرح قصة كل لغة قراراتها.
دليلالتزامن Concurrency والخيوط: ماذا يعني لسيرفرك وتطبيقاتكلماذا يكون المعالج عند 100% والتطبيق بطيئاً، ولا تسرع الخيوط كود Python، وكيف تضبط عدد العمال وحدود ال Containers.
دليلإدارة النسخ وGit للمبتدئين: لماذا نحتاجها وكيف تعمللماذا تضيع الملفات بأسماء final_v2 ويمسح الزملاء عمل بعضهم، وكيف يحل Git ذلك على مشروع حقيقي من أول commit.سجل التحديثات
- أكتوبر 2026: إضافة المصطلح الإنجليزي بجانب المصطلحات العربية في العناوين والجدول والخلاصة.
- أكتوبر 2026: كتابة المقال، واختبار كل الأمثلة على Docker مع Go 1.27.1، وOpenJDK 25.0.4.1، وNode.js 22.23.3 مع TypeScript 7.0.2، وPython 3.13.16 مع mypy 2.4.0، وrustc 1.99.0، وgcc 15.2.0، وGHC 9.14.1، وSWI-Prolog 10.1.16، وPostgreSQL 17.11، وTerraform v1.16.5.