الفكرة في جملة واحدة
تُنفِّذ برامج Zero الإدخال/الإخراج بأن تُمنَح الإذن لذلك، لا بالوصول إلى متغيِّر عمومي محيط.
ذلك الإذن قيمة من النوع World. يُنشئ زمن التشغيل واحدة قبل استدعاء main، ويُمرّرها برنامجك (أو قطعًا منها) أينما يحتاج التفاعل مع العالم الخارجي.
لماذا لا متغيِّرات عمومية؟
معظم اللغات تسمح لأي دالة، في أي مكان، بالكتابة إلى stdout أو فتح ملف. JavaScript لديها console.log. Python لديها print. C لديها printf. الراحة حقيقية، وكذلك التكلفة: لا تستطيع من توقيع الدالة معرفة ما إذا كانت قد تُنفّذ إدخال/إخراج. لمعرفة ذلك، يجب قراءة الجسم — تكراريًا.
تتّخذ Zero موقفًا مختلفًا. لا توجد دالة print عمومية. لا يوجد os.Stdout محيط. إن نفّذت دالتك إدخال/إخراج، فهذه الحقيقة يجب أن تظهر في توقيعها، لأن الطريقة الوحيدة لتنفيذ إدخال/إخراج هي أن تكون قد سُلِّمت قدرة.
تظهر الفوائد في ثلاثة مواضع:
- قراءة التوقيع تُخبرك بما تستطيع الدالة فعله. دالة لا تذكر
Worldلا تستطيع الكتابة إلى stdout، ولا فتح مقبس، ولا قراءة ملف. نظام الأنواع يجعل ذلك ضمانًا صارمًا. - اختبار الشيفرة النقية تافه. الدوال النقية لا تحتاج إلى دالة
printمزيّفة أو نظام ملفات وهمي — هي ليس لديها وصول إليها أصلًا. - يستطيع الوكلاء الاستدلال محليًا. وكيل ذكاء اصطناعي يُولّد أو يُصلح شيفرة Zero يستطيع المعرفة — دون قراءة قاعدة الشيفرة كاملةً — ما إذا كانت دالة ينظر إليها ذات تأثيرات."
الاستخدام التقليدي
رأيت بالفعل الشكل الأساسي من hello-world:
ثلاثة أمور تجري:
- تُعلن
mainمعاملها بـworld: World. يُسلّم زمن التشغيل البرنامج قيمةWorldويربطها هنا. world.outهو تيار الإخراج القياسي، مكشوفًا كحقل على قدرةWorld.world.out.write(...)يكتب نصًا. يُعيد قيمة قابلة للفشل (الكتابة قد تفشل)، التي تنشرهاcheck.
تستطيع أيضًا إعادة تسمية المعامل — w: World أو io: World — لكن world هو الاتّفاق ويستحقّ الإبقاء عليه للاتّساق مع منظومة Zero الأوسع.
ماذا يعيش على World
تكشف قدرة World عن الواجهات التي قد يحتاجها البرنامج. الشكل الدقيق يعتمد على ما يدعمه زمن التشغيل، لكن تستطيع توقّع مدخلات لـ:
world.out— الإخراج القياسي.world.err— الخطأ القياسي.world.in— الإدخال القياسي.- طريقة لفتح الملفات، وقراءة متغيِّرات البيئة، والاتّصال عبر الشبكة.
راجع توثيق المكتبة القياسية الحالي لـ Zero للقائمة الموثوقة بالحقول. بعض الواجهات (الشبكة، نظام الملفات) قد تعيش خلف أنواع قدرات أضيق متاحة عبر World بدلًا من المستوى الأعلى.
تمرير القدرات عبر الشيفرة
المقابل — الثمن الذي تدفعه للتأثيرات الصريحة — هو أن أي دالة تحتاج تنفيذ إدخال/إخراج يجب أن تستقبل القدرة. لا تستطيع الوصول إليها ضمنيًا.
fun log(world: World, message: String) -> Void raises {
check world.out.write(message)
}
pub fun main(world: World) -> Void raises {
log(world, "starting\n")
log(world, "done\n")
}
log تأخذ world لتستطيع الكتابة عبره. لو لم تأخذ log معامل world، لما استطاع الجسم استدعاء world.out.write — الربط ببساطة لن يكون موجودًا.
هذا تمديد معاملات أكثر من لغة بإدخال/إخراج محيط. المقابل أن رسم استدعاءات main بالكامل مرئي الآن من التوقيعات وحدها:
mainتأخذworld، لذا قد تُنفّذ إدخال/إخراج.logتأخذworld، لذا قد تُنفّذ إدخال/إخراج.- أي دالة بدون
worldفي توقيعها لا تستطيع ذلك.
قدرات أضيق
تمرير World كاملةً لكل دالة أسلوب فظّ — يشبه إعطاء صلاحيات الجذر. النمط الذي تُشجّع عليه Zero هو أخذ شريحة World التي تحتاجها فعلًا فقط:
fun log(out: Stream, message: String) -> Void raises {
check out.write(message)
}
pub fun main(world: World) -> Void raises {
log(world.out, "starting\n")
log(world.out, "done\n")
}
الآن log تحصل فقط على وصول إلى Stream (نفس النوع الذي يكشفه world.out). تستطيع الكتابة عبره لكن لا تستطيع فتح ملف أو القراءة من الشبكة. المستدعي اختار ما تستطيع log فعله.
أسماء الأنواع الدقيقة التي ستراها في شيفرة Zero الحقيقية (Stream، Writer، شرائح القدرات) ستتتبّع مفردات المكتبة القياسية في إصدار سلسلة أدواتك. النمط — مرّر الحدّ الأدنى، لا الحدّ الأقصى — عالمي.
الدوال النقية
الدوال التي لا تحتاج World يجب ألّا تأخذ World. هذا تفضيل أسلوبي جزئيًا ومفروض جزئيًا من نظام الأنواع: لا توجد طريقة لتنفيذ إدخال/إخراج بدون قدرة.
fun sum(point: Point) -> i32 {
return point.x + point.y
}
sum نقية بالنسبة للعالم الخارجي. مستدعٍ ينظر إلى التوقيع يعلم يقينًا أن هذه الدالة لن تطبع شيئًا، ولن تفتح ملفًا، ولن تطرق خادمًا. هذه خاصية يستطيع المحلّل الساكن (أو الوكيل) الاعتماد عليها دون قراءة الجسم.
القدرات وraises
تقريبًا كل عملية على قدرة قابلة للفشل. world.out.write قد تفشل لأن التيار مغلق. فتح ملف قد يفشل لأن الملف غير موجود. سطح واجهة القدرات مقترن بـ raises وcheck — العمليات القابلة للفشل تُعلن أنماط فشلها في توقيعاتها، والمستدعون يُقرّون بها بـ check.
التركيب هو قلب قصّة التأثيرات في Zero:
- ماذا يمكن أن يحدث →
raises { ... }. - من خلال ماذا →
World(أو شريحة منه). - أين → حيثما تكون القدرة و
raisesمرئيتَين.
هذه معلومات كافية للاستدلال بدقّة على تأثيرات الدالة من توقيعها وحده.
ملاحظة عن الاختبار
الإدخال/الإخراج القائم على القدرات يجعل الاختبار بسيطًا بحكم البناء. تريد التقاط مخرجات دالة تحت الاختبار؟ مرّر إليها قدرة out مزيّفة تُسجّل ما طُلب منها كتابته. تريد اختبار دالة يُفترض أن تكون نقية؟ لا تُمرّر إليها أي قدرة — توقيع نوعها لن يسمح لها بمسّ العالم الخارجي.
تستطيع المكتبة القياسية توفير أُطر اختبار تبني قدرات مزيّفة أو في الذاكرة لهذا الغرض بالضبط. ستتطوّر الواجهة الدقيقة مع اللغة؛ المبدأ (القدرات قيم تستطيع استبدالها) هو الرافعة.
التالي: Raises و Check
World نصف قصّة التأثيرات — الواجهات التي تستطيع الدالة مسّها. النصف الآخر هو الفشل: عندما يسوء شيء، كيف ينتشر؟ ذلك مشمول تاليًا في Raises و Check.
الأسئلة الشائعة
ما هي World في Zero؟
World هو كائن القدرة الذي يوفّره زمن التشغيل ويمنح برنامج Zero الوصول إلى العالم الخارجي: stdout، وstdin، والملفات، والشبكة، ومتغيِّرات البيئة وغيرها. يُنشئ زمن التشغيل قيمة World ويُمرّرها إلى main. الدوال التي تحتاج تنفيذ إدخال/إخراج يجب تمرير World (أو شريحة أضيق منه) إليها — لا يوجد منفذ هروب عمومي.
لماذا تأخذ main معامل World؟
Zero ليس فيها متغيِّرات عمومية محيطة. لا يوجد ما يكافئ printf أو console.log أو os.Stdout التي تستطيع أي دالة استدعاءها دون إذن. يُسلّم زمن التشغيل main قدرة World وتستطيع main (وأي دالة تستدعيها) تنفيذ إدخال/إخراج فقط من خلال تلك القيمة. هذا يجعل كل تأثير مرئيًا في توقيع الدالة.
كيف يختلف الإدخال/الإخراج القائم على القدرات عن الإدخال/الإخراج العادي؟
في معظم اللغات، الإدخال/الإخراج ضمني — أي دالة تستطيع الكتابة إلى stdout أو القراءة من نظام الملفات في أي وقت. الإدخال/الإخراج القائم على القدرات يجعل إذن تنفيذ الإدخال/الإخراج قيمةً: يجب أن تُسلَّم World (أو شريحة منه) لاستخدامه. دوال الحساب النقي لا تأخذ World ولذلك لا تستطيع حرفيًا تنفيذ إدخال/إخراج، ويفرض نظام الأنواع ذلك.
هل يمكنني الحصول على World ضمنيًا في مكان عميق من مكدّس الاستدعاءات؟
لا، بحكم التصميم. إن احتاج مساعد عميق في مكدّس الاستدعاءات للكتابة إلى stdout، يجب تمرير World — أو قدرة أضيق — إليه صراحة. هذا تمديد معاملات أكثر من لغة بإدخال/إخراج محيط، لكنّه ثمن القدرة على قراءة توقيع دالة ومعرفة ما إذا كانت قد تُنفّذ إدخال/إخراج.
ماذا يفعل world.out.write؟
world.out.write("text\n") يكتب النصّ المُعطى إلى تيار الإخراج القياسي للبرنامج عبر القدرة التي وفّرها زمن التشغيل. يُعيد قيمة قابلة للفشل — قد تفشل الكتابة — لذا تُغلّف الاستدعاء بـ check لنشر الخطأ إلى الأعلى.