Modules

تساعد الوحدات في جوليا على تنظيم الشيفرة في وحدات متماسكة. يتم تحديدها نحويًا داخل module NameOfModule ... end، ولها الميزات التالية:

  1. تعتبر الوحدات مساحات أسماء منفصلة، حيث تقدم كل منها نطاقًا عالميًا جديدًا. هذا مفيد، لأنه يسمح باستخدام نفس الاسم لوظائف مختلفة أو متغيرات عالمية دون تعارض، طالما أنها في وحدات منفصلة.
  2. تحتوي الوحدات على مرافق لإدارة المساحات الاسمية بالتفصيل: كل وحدة تحدد مجموعة من الأسماء التي تقوم بتصديرها وتحددها على أنها عامة، ويمكنها استيراد أسماء من وحدات أخرى باستخدام using و import (سنشرح هذه أدناه).
  3. يمكن تجميع الوحدات مسبقًا لتحميل أسرع، وقد تحتوي على كود لتهيئة وقت التشغيل.

عادةً، في حزم جوليا الأكبر، سترى كود الوحدة منظمًا في ملفات، مثل

module SomeModule

# export, public, using, import statements are usually here; we discuss these below

include("file1.jl")
include("file2.jl")

end

الملفات وأسماء الملفات غالبًا ما تكون غير مرتبطة بالوحدات؛ الوحدات مرتبطة فقط بتعبيرات الوحدة. يمكن أن يحتوي كل وحدة على ملفات متعددة، ويمكن أن تحتوي كل ملف على وحدات متعددة. include يتصرف كما لو أن محتويات ملف المصدر تم تقييمها في النطاق العالمي للوحدة المضمنة. في هذا الفصل، نستخدم أمثلة قصيرة ومبسطة، لذا لن نستخدم include.

الأسلوب الموصى به هو عدم إزاحة جسم الوحدة، حيث أن ذلك قد يؤدي عادةً إلى إزاحة الملفات بالكامل. كما أنه من الشائع استخدام UpperCamelCase لأسماء الوحدات (تمامًا مثل الأنواع)، واستخدام الشكل الجمع إذا كان ذلك مناسبًا، خاصةً إذا كانت الوحدة تحتوي على معرف يحمل اسمًا مشابهًا، لتجنب تعارض الأسماء. على سبيل المثال،

module FastThings

struct FastThing
    ...
end

end

Namespace management

إدارة المساحات الاسمية تشير إلى المرافق التي يقدمها اللغة لجعل الأسماء في وحدة متاحة في وحدات أخرى. نناقش المفاهيم والوظائف ذات الصلة أدناه بالتفصيل.

Qualified names

ت belong to a module, called the parent module, which can be found interactively with parentmodule، على سبيل المثال

julia> parentmodule(UnitRange)
Base

يمكن أيضًا الإشارة إلى هذه الأسماء خارج وحدتها الأصلية عن طريق تمييزها بوحدتها، مثل Base.UnitRange. يُطلق على هذا اسم اسم مؤهل. قد تكون الوحدة الأصلية متاحة باستخدام سلسلة من الوحدات الفرعية مثل Base.Math.sin، حيث تُسمى Base.Math مسار الوحدة. بسبب الغموض النحوي، يتطلب تأهيل اسم يحتوي فقط على رموز، مثل عامل، إدراج نقطتين، مثل Base.:+. عدد قليل من العوامل يتطلب أيضًا أقواسًا، مثل Base.:(==).

إذا كان الاسم مؤهلاً، فإنه يكون دائماً قابلاً للوصول، وفي حالة الدالة، يمكن أيضاً إضافة طرق إليها باستخدام الاسم المؤهل كاسم للدالة.

داخل الوحدة، يمكن "حجز" اسم متغير دون تعيينه عن طريق إعلانه كـ global x. هذا يمنع تعارض الأسماء للمتغيرات العالمية التي تم تهيئتها بعد وقت التحميل. الصيغة M.x = y لا تعمل لتعيين متغير عالمي في وحدة أخرى؛ فالتعيين العالمي يكون دائمًا محليًا للوحدة.

Export lists

يمكن إضافة الأسماء (التي تشير إلى الدوال، الأنواع، المتغيرات العالمية، والثوابت) إلى قائمة التصدير لوحدة باستخدام export: هذه هي الرموز التي يتم استيرادها عند استخدام الوحدة. عادةً، تكون في أعلى أو بالقرب من تعريف الوحدة حتى يتمكن قراء الشيفرة المصدرية من العثور عليها بسهولة، كما في

julia> module NiceStuff
       export nice, DOG
       struct Dog end      # singleton type, not exported
       const DOG = Dog()   # named instance, exported
       nice(x) = "nice $x" # function, exported
       end;

لكن هذا مجرد اقتراح أسلوب — يمكن أن يحتوي الوحدة على عدة عبارات export في مواقع عشوائية.

من الشائع تصدير الأسماء التي تشكل جزءًا من واجهة برمجة التطبيقات (API). في الكود أعلاه، تشير قائمة التصدير إلى أن المستخدمين يجب أن يستخدموا nice و DOG. ومع ذلك، نظرًا لأن الأسماء المؤهلة تجعل المعرفات متاحة دائمًا، فإن هذه مجرد خيار لتنظيم واجهات برمجة التطبيقات: على عكس اللغات الأخرى، لا تحتوي جوليا على تسهيلات لإخفاء تفاصيل الوحدة حقًا.

أيضًا، بعض الوحدات لا تصدر أسماء على الإطلاق. يتم ذلك عادةً إذا كانت تستخدم كلمات شائعة، مثل derivative، في واجهة برمجتها، مما قد يتسبب في تعارض مع قوائم التصدير لوحدات أخرى. سنرى كيف ندير تعارض الأسماء أدناه.

لتحديد اسم كعام دون تصديره إلى مساحة أسماء الأشخاص الذين يستخدمون using NiceStuff، يمكن استخدام public بدلاً من export. هذا يحدد الاسماء العامة كجزء من واجهة برمجة التطبيقات العامة، ولكنه لا يؤثر على أي مساحة أسماء. الكلمة الرئيسية public متاحة فقط في جوليا 1.11 وما فوق. للحفاظ على التوافق مع جوليا 1.10 وما دون، استخدم الماكرو @compat من حزمة Compat.

Standalone using and import

للاستخدام التفاعلي، فإن الطريقة الأكثر شيوعًا لتحميل وحدة هي using ModuleName. هذا loads الكود المرتبط بـ ModuleName، ويجلب

  1. اسم الوحدة
  2. وعناصر قائمة التصدير إلى مساحة الاسم العالمية المحيطة.

تقنيًا، تعني العبارة using ModuleName أن وحدة تسمى ModuleName ستكون متاحة لحل الأسماء حسب الحاجة. عندما يتم مواجهة متغير عالمي ليس له تعريف في الوحدة الحالية، سيبحث النظام عنه بين المتغيرات المصدرة بواسطة ModuleName ويستخدمه إذا تم العثور عليه هناك. هذا يعني أن جميع استخدامات ذلك المتغير العالمي داخل الوحدة الحالية ستتوافق مع تعريف ذلك المتغير في ModuleName.

لتحميل وحدة من حزمة، يمكن استخدام العبارة using ModuleName. لتحميل وحدة من وحدة معرفة محليًا، يجب إضافة نقطة قبل اسم الوحدة مثل using .ModuleName.

للمتابعة مع مثالنا،

julia> using .NiceStuff

سيتم تحميل الكود أعلاه، مما يجعل NiceStuff (اسم الوحدة)، و DOG و nice متاحة. Dog ليست على قائمة التصدير، ولكن يمكن الوصول إليها إذا تم تأهيل الاسم مع مسار الوحدة (الذي هنا هو فقط اسم الوحدة) كـ NiceStuff.Dog.

من المهم أن using ModuleName هو الشكل الوحيد الذي تهم فيه قوائم التصدير على الإطلاق.

في المقابل،

julia> import .NiceStuff

يجلب فقط اسم الوحدة إلى النطاق. سيحتاج المستخدمون إلى استخدام NiceStuff.DOG و NiceStuff.Dog و NiceStuff.nice للوصول إلى محتوياتها. عادةً ما يتم استخدام import ModuleName في السياقات عندما يريد المستخدم الحفاظ على نظافة مساحة الأسماء. كما سنرى في القسم التالي، فإن import .NiceStuff يعادل using .NiceStuff: NiceStuff.

يمكنك دمج عدة عبارات using و import من نفس النوع في تعبير مفصول بفواصل، على سبيل المثال

julia> using LinearAlgebra, Random

using and import with specific identifiers, and adding methods

عندما يتبع using ModuleName: أو import ModuleName: قائمة مفصولة بفواصل من الأسماء، يتم تحميل الوحدة، ولكن فقط تلك الأسماء المحددة يتم جلبها إلى مساحة الأسماء بواسطة العبارة. على سبيل المثال،

julia> using .NiceStuff: nice, DOG

سوف نستورد الأسماء nice و DOG.

من المهم أن اسم الوحدة NiceStuff لن يكون في مساحة الأسماء. إذا كنت تريد جعله متاحًا، يجب عليك ذكره صراحة، كما هو الحال في

julia> using .NiceStuff: nice, DOG, NiceStuff

عندما تقوم حزمتان أو أكثر بتصدير اسم ولا يشير هذا الاسم إلى نفس الشيء في كل من الحزم، ويتم تحميل الحزم عبر using دون قائمة صريحة من الأسماء، فإنه من الخطأ الإشارة إلى ذلك الاسم دون تأهيل. لذلك يُوصى بأن يقوم الكود الذي يُقصد به أن يكون متوافقًا مع الإصدارات المستقبلية من تبعياته ومن جوليا، مثل الكود في الحزم المصدرة، بإدراج الأسماء التي يستخدمها من كل حزمة محملة، مثل using Foo: Foo, f بدلاً من using Foo.

تحتوي جوليا على شكلين لنفس الشيء ظاهريًا لأن import ModuleName: f يسمح بإضافة طرق إلى f بدون مسار وحدة. بمعنى آخر، المثال التالي سيعطي خطأ:

julia> using .NiceStuff: nice

julia> struct Cat end

julia> nice(::Cat) = "nice 😸"
ERROR: invalid method definition in Main: function NiceStuff.nice must be explicitly imported to be extended
Stacktrace:
 [1] top-level scope
   @ none:0
 [2] top-level scope
   @ none:1

يمنع هذا الخطأ إضافة طرق عن غير قصد إلى الدوال في وحدات أخرى كنت تنوي استخدامها فقط.

هناك طريقتان للتعامل مع هذا. يمكنك دائمًا تأهيل أسماء الدوال بمسار الوحدة:

julia> using .NiceStuff

julia> struct Cat end

julia> NiceStuff.nice(::Cat) = "nice 😸"

بدلاً من ذلك، يمكنك import اسم الدالة المحددة:

julia> import .NiceStuff: nice

julia> struct Cat end

julia> nice(::Cat) = "nice 😸"
nice (generic function with 2 methods)

أي واحدة تختارها هي مسألة أسلوب. الشكل الأول يوضح أنك تضيف طريقة إلى دالة في وحدة أخرى (تذكر أن الاستيرادات وتعريف الطريقة قد تكون في ملفات منفصلة)، بينما الشكل الثاني أقصر، وهو مريح بشكل خاص إذا كنت تعرف طرقًا متعددة.

بمجرد أن يتم جعل المتغير مرئيًا عبر using أو import، لا يمكن للوحدة إنشاء متغير خاص بها بنفس الاسم. المتغيرات المستوردة هي للقراءة فقط؛ فإن تعيين قيمة لمتغير عالمي يؤثر دائمًا على متغير مملوك للوحدة الحالية، أو يثير خطأ.

Renaming with as

يمكن إعادة تسمية معرف تم إدخاله في النطاق بواسطة import أو using باستخدام الكلمة الرئيسية as. هذا مفيد لتجاوز تعارضات الأسماء وكذلك لتقصير الأسماء. على سبيل المثال، يقوم Base بتصدير اسم الدالة read، ولكن حزمة CSV.jl توفر أيضًا CSV.read. إذا كنا سنستدعي قراءة CSV عدة مرات، سيكون من الملائم إسقاط المؤهل CSV.. ولكن بعد ذلك يصبح من الغامض ما إذا كنا نشير إلى Base.read أو CSV.read:

julia> read;

julia> import CSV: read
WARNING: ignoring conflicting import of CSV.read into Main

إعادة التسمية توفر حلاً:

julia> import CSV: read as rd

يمكن أيضًا إعادة تسمية الحزم المستوردة:

import BenchmarkTools as BT

as يعمل مع using فقط عندما يتم جلب معرف واحد إلى النطاق. على سبيل المثال، using CSV: read as rd يعمل، لكن using CSV as C لا يعمل، لأنه يعمل على جميع الأسماء المصدرة في CSV.

Mixing multiple using and import statements

عند استخدام عدة عبارات using أو import من أي من الأشكال المذكورة أعلاه، يتم دمج تأثيرها بالترتيب الذي تظهر به. على سبيل المثال،

julia> using .NiceStuff         # exported names and the module name

julia> import .NiceStuff: nice  # allows adding methods to unqualified functions

سيجلب جميع الأسماء المصدرة من NiceStuff واسم الوحدة نفسها إلى النطاق، وسيسمح أيضًا بإضافة طرق إلى nice دون الحاجة إلى تمييزها باسم الوحدة.

Handling name conflicts

اعتبر الحالة التي تصدر فيها حزمتان (أو أكثر) نفس الاسم، كما في

julia> module A
       export f
       f() = 1
       end
A
julia> module B
       export f
       f() = 2
       end
B

العبارة using .A, .B تعمل، ولكن عندما تحاول استدعاء f، تحصل على خطأ مع تلميح

julia> using .A, .B

julia> f
ERROR: UndefVarError: `f` not defined in `Main`
Hint: It looks like two or more modules export different bindings with this name, resulting in ambiguity. Try explicitly importing it from a particular module, or qualifying the name with the module it should come from.

هنا، لا تستطيع جوليا أن تقرر أي f تشير إليه، لذا عليك أن تتخذ قرارًا. الحلول التالية هي الأكثر استخدامًا:

  1. ببساطة تابع باستخدام الأسماء المؤهلة مثل A.f و B.f. هذا يجعل السياق واضحًا للقارئ في كودك، خاصة إذا كان f يتزامن بالصدفة ولكن له معاني مختلفة في حزم مختلفة. على سبيل المثال، يستخدم مصطلح degree بطرق مختلفة في الرياضيات، والعلوم الطبيعية، وفي الحياة اليومية، ويجب الحفاظ على هذه المعاني منفصلة.

  2. استخدم الكلمة الرئيسية as أعلاه لإعادة تسمية واحد أو كلا المعرفين، على سبيل المثال

    ```jldoctest module_manual julia> using .A: f as f

    julia> using .B: f as g

    ```

    سوف يجعل B.f متاحًا كـ g. هنا، نفترض أنك لم تستخدم using A من قبل، مما كان سيجلب f إلى مساحة الأسماء.

  3. عندما تتشارك الأسماء المعنية معنى، من الشائع أن يقوم أحد الوحدات باستيرادها من وحدة أخرى، أو أن يكون لديها حزمة "أساسية" خفيفة الوزن تهدف فقط إلى تعريف واجهة مثل هذه، والتي يمكن استخدامها من قبل حزم أخرى. من المعتاد أن تنتهي أسماء هذه الحزم بـ ...Base (وهو ما لا علاقة له بوحدة Base في جوليا).

Default top-level definitions and bare modules

تحتوي الوحدات تلقائيًا على using Core، using Base، وتعريفات الدوال eval و include، التي تقوم بتقييم التعبيرات/الملفات ضمن النطاق العالمي لتلك الوحدة.

إذا لم تكن هذه التعريفات الافتراضية مرغوبة، يمكن تعريف الوحدات باستخدام الكلمة الرئيسية baremodule بدلاً من ذلك (ملاحظة: لا يزال يتم استيراد Core). من حيث baremodule، تبدو الوحدة القياسية module هكذا:

baremodule Mod

using Base

eval(x) = Core.eval(Mod, x)
include(p) = Base.include(Mod, p)

...

end

إذا لم يكن Core مرغوبًا حتى، يمكن تعريف وحدة تستورد لا شيء ولا تعرف أي أسماء على الإطلاق باستخدام Module(:YourNameHere, false, false) ويمكن تقييم الشيفرة فيها باستخدام @eval أو Core.eval:

julia> arithmetic = Module(:arithmetic, false, false)
Main.arithmetic

julia> @eval arithmetic add(x, y) = $(+)(x, y)
add (generic function with 1 method)

julia> arithmetic.add(12, 13)
25

Standard modules

هناك ثلاثة وحدات قياسية مهمة:

  • Core يحتوي على جميع الوظائف "المضمنة" في اللغة.
  • Base يحتوي على وظائف أساسية مفيدة في معظم الحالات.
  • Main هو الوحدة العليا والوحدة الحالية، عندما يتم بدء تشغيل جوليا.
Standard library modules

بشكل افتراضي، تأتي جوليا مع بعض وحدات المكتبة القياسية. هذه الوحدات تعمل مثل حزم جوليا العادية باستثناء أنك لا تحتاج إلى تثبيتها بشكل صريح. على سبيل المثال، إذا كنت ترغب في إجراء بعض اختبارات الوحدة، يمكنك تحميل مكتبة Test القياسية كما يلي:

using Test

Submodules and relative paths

يمكن أن تحتوي الوحدات على وحدات فرعية، مع استخدام نفس بناء الجملة module ... end. يمكن استخدامها لتقديم مساحات أسماء منفصلة، مما يمكن أن يكون مفيدًا لتنظيم قواعد الشفرات المعقدة. لاحظ أن كل module يقدم خاصيته الخاصة scope، لذا فإن الوحدات الفرعية لا "ترث" الأسماء تلقائيًا من الوالدين.

يوصى بأن تشير الوحدات الفرعية إلى وحدات أخرى داخل الوحدة الأم المحيطة (بما في ذلك الأخيرة) باستخدام مؤهلات الوحدة النسبية في عبارات using و import. يبدأ مؤهل الوحدة النسبي بنقطة (.)، والتي تتوافق مع الوحدة الحالية، وكل نقطة متتالية . تقود إلى الوالد للوحدة الحالية. يجب أن يتبع ذلك الوحدات إذا لزم الأمر، وفي النهاية الاسم الفعلي للوصول إليه، جميعها مفصولة بنقاط ..

اعتبر المثال التالي، حيث تعرف الوحدة الفرعية SubA دالة، والتي يتم توسيعها بعد ذلك في وحدتها "الأخت":

julia> module ParentModule
       module SubA
       export add_D  # exported interface
       const D = 3
       add_D(x) = x + D
       end
       using .SubA  # brings `add_D` into the namespace
       export add_D # export it from ParentModule too
       module SubB
       import ..SubA: add_D # relative path for a “sibling” module
       struct Infinity end
       add_D(x::Infinity) = x
       end
       end;

يمكنك رؤية الشيفرة في الحزم، والتي، في حالة مشابهة، تستخدم

julia> import .ParentModule.SubA: add_D

ومع ذلك، يعمل هذا من خلال code loading، وبالتالي فإنه يعمل فقط إذا كان ParentModule في حزمة. من الأفضل استخدام المسارات النسبية.

لاحظ أن ترتيب التعريفات مهم أيضًا إذا كنت تقوم بتقييم القيم. اعتبر

module TestPackage

export x, y

x = 0

module Sub
using ..TestPackage
z = y # ERROR: UndefVarError: `y` not defined in `Main`
end

y = 1

end

حيث أن Sub تحاول استخدام TestPackage.y قبل أن يتم تعريفه، لذا فإنه لا يحتوي على قيمة.

لأسباب مماثلة، لا يمكنك استخدام ترتيب دوري:

module A

module B
using ..C # ERROR: UndefVarError: `C` not defined in `Main.A`
end

module C
using ..B
end

end

Module initialization and precompilation

يمكن أن تستغرق الوحدات الكبيرة عدة ثوانٍ لتحميلها لأن تنفيذ جميع التعليمات في وحدة ما غالبًا ما يتطلب تجميع كمية كبيرة من الشيفرة. تقوم جوليا بإنشاء ذاكرات مؤقتة مسبقة التجميع للوحدة لتقليل هذا الوقت.

تُنشأ وتُستخدم ملفات الوحدات المجمعة (المعروفة أحيانًا باسم "ملفات التخزين المؤقت") تلقائيًا عند تحميل وحدة باستخدام import أو using. إذا لم تكن ملفات التخزين المؤقت موجودة بعد، فسيتم تجميع الوحدة وحفظها لإعادة استخدامها في المستقبل. يمكنك أيضًا استدعاء Base.compilecache(Base.identify_package("modulename")) يدويًا لإنشاء هذه الملفات دون تحميل الوحدة. ستُخزن ملفات التخزين المؤقت الناتجة في المجلد الفرعي compiled من DEPOT_PATH[1]. إذا لم يتغير أي شيء في نظامك، فسيتم استخدام مثل هذه الملفات عند تحميل الوحدة باستخدام import أو using.

تخزن ملفات ذاكرة التخزين المؤقت السابقة تعريفات الوحدات والأنواع والأساليب والثوابت. قد تخزن أيضًا تخصصات الأساليب والشفرة المولدة لها، ولكن هذا يتطلب عادةً من المطور إضافة توجيهات صريحة precompile أو تنفيذ أحمال عمل تجبر على الترجمة أثناء بناء الحزمة.

ومع ذلك، إذا قمت بتحديث تبعيات الوحدة أو تغيير شفرتها المصدرية، يتم إعادة تجميع الوحدة تلقائيًا عند using أو import. التبعيات هي الوحدات التي تستوردها، بناء جوليا، الملفات التي تتضمنها، أو التبعيات الصريحة المعلنة بواسطة include_dependency(path) في ملف(ملفات) الوحدة.

بالنسبة للاعتماديات الملفات المحملة بواسطة include، يتم تحديد التغيير من خلال فحص ما إذا كان حجم الملف (fsize) أو المحتوى (المكثف في تجزئة) غير متغير. بالنسبة للاعتماديات الملفات المحملة بواسطة include_dependency، يتم تحديد التغيير من خلال فحص ما إذا كان وقت التعديل (mtime) غير متغير، أو يساوي وقت التعديل المقطوع إلى أقرب ثانية (لتلبية أنظمة لا يمكنها نسخ mtime بدقة أقل من الثانية). كما يأخذ في الاعتبار ما إذا كان المسار إلى الملف الذي اختاره منطق البحث في require يتطابق مع المسار الذي أنشأ ملف ما قبل التجميع. كما يأخذ في الاعتبار مجموعة الاعتماديات التي تم تحميلها بالفعل في العملية الحالية ولن يعيد تجميع تلك الوحدات، حتى لو تغيرت ملفاتها أو اختفت، لتجنب إنشاء عدم توافق بين النظام الجاري وذاكرة التخزين المؤقت للتجميع المسبق. أخيرًا، يأخذ في الاعتبار التغييرات في أي compile-time preferences.

إذا كنت تعلم أن الوحدة ليست آمنة للتجميع المسبق (على سبيل المثال، لأحد الأسباب الموضحة أدناه)، يجب عليك وضع __precompile__(false) في ملف الوحدة (عادة ما يتم وضعه في الأعلى). سيؤدي ذلك إلى جعل Base.compilecache يرمي خطأ، وسيتسبب في تحميل using / import لها مباشرة في العملية الحالية وتجاوز التجميع المسبق والتخزين المؤقت. وهذا يمنع أيضًا استيراد الوحدة من قبل أي وحدة تم تجميعها مسبقًا.

قد تحتاج إلى أن تكون على دراية ببعض السلوكيات المتأصلة في إنشاء المكتبات المشتركة التزايدية والتي قد تتطلب العناية عند كتابة وحدتك. على سبيل المثال، لا يتم الحفاظ على الحالة الخارجية. لاستيعاب ذلك، يجب فصل أي خطوات تهيئة يجب أن تحدث في وقت التشغيل عن الخطوات التي يمكن أن تحدث في وقت الترجمة. لهذا الغرض، يسمح لك جوليا بتعريف دالة __init__() في وحدتك التي تنفذ أي خطوات تهيئة يجب أن تحدث في وقت التشغيل. لن يتم استدعاء هذه الدالة أثناء الترجمة (--output-*). بشكل فعال، يمكنك أن تفترض أنها ستُشغل مرة واحدة فقط في عمر الكود. يمكنك، بالطبع، استدعاؤها يدويًا إذا لزم الأمر، لكن الافتراضي هو افتراض أن هذه الدالة تتعامل مع حساب الحالة للجهاز المحلي، والذي لا يحتاج إلى أن يتم – أو حتى يجب ألا يتم – التقاطه في الصورة المترجمة. سيتم استدعاؤها بعد تحميل الوحدة في عملية، بما في ذلك إذا كانت تُحمّل في ترجمة تزايدية (--output-incremental=yes)، ولكن ليس إذا كانت تُحمّل في عملية ترجمة كاملة.

بشكل خاص، إذا قمت بتعريف دالة function __init__() في وحدة، فإن جوليا ستقوم باستدعاء __init__() على الفور بعد تحميل الوحدة (مثل import، using، أو require) في وقت التشغيل للمرة الأولى (أي، يتم استدعاء __init__ مرة واحدة فقط، وفقط بعد تنفيذ جميع العبارات في الوحدة). نظرًا لأنه يتم استدعاؤه بعد استيراد الوحدة بالكامل، فإن أي وحدات فرعية أو وحدات مستوردة أخرى يتم استدعاء دوال __init__ الخاصة بها قبل __init__ للوحدة المحيطة.

استخدامان نموذجيان لـ __init__ هما استدعاء دوال التهيئة في وقت التشغيل لمكتبات C الخارجية وتهيئة الثوابت العالمية التي تتضمن مؤشرات تم إرجاعها بواسطة المكتبات الخارجية. على سبيل المثال، افترض أننا نستدعي مكتبة C libfoo التي تتطلب منا استدعاء دالة التهيئة foo_init() في وقت التشغيل. افترض أيضًا أننا نريد تعريف ثابت عالمي foo_data_ptr الذي يحتفظ بقيمة الإرجاع لدالة void *foo_data() المعرفة بواسطة libfoo – يجب تهيئة هذا الثابت في وقت التشغيل (وليس في وقت الترجمة) لأن عنوان المؤشر سيتغير من تشغيل إلى آخر. يمكنك تحقيق ذلك من خلال تعريف دالة __init__ التالية في وحدتك:

const foo_data_ptr = Ref{Ptr{Cvoid}}(0)
function __init__()
    ccall((:foo_init, :libfoo), Cvoid, ())
    foo_data_ptr[] = ccall((:foo_data, :libfoo), Ptr{Cvoid}, ())
    nothing
end

لاحظ أنه من الممكن تمامًا تعريف متغير عالمي داخل دالة مثل __init__؛ هذه واحدة من مزايا استخدام لغة ديناميكية. ولكن من خلال جعله ثابتًا في النطاق العالمي، يمكننا التأكد من أن النوع معروف للمترجم والسماح له بتوليد كود محسن بشكل أفضل. من الواضح أن أي متغيرات عالمية أخرى في وحدتك تعتمد على foo_data_ptr يجب أيضًا أن يتم تهيئتها في __init__.

الثوابت التي تتعلق بمعظم كائنات جوليا التي لم يتم إنتاجها بواسطة ccall لا تحتاج إلى أن توضع في __init__: يمكن تجميع تعريفاتها مسبقًا وتحميلها من صورة الوحدة المخزنة. يشمل ذلك الكائنات المعقدة المخصصة في الذاكرة مثل المصفوفات. ومع ذلك، يجب استدعاء أي روتين يعيد قيمة مؤشر خام في وقت التشغيل لكي تعمل التجميع المسبق (Ptr ستتحول إلى مؤشرات فارغة ما لم تكن مخفية داخل كائن isbits). يشمل ذلك قيم الإرجاع من دوال جوليا @cfunction و pointer.

أنواع القواميس والمجموعات، أو بشكل عام أي شيء يعتمد على ناتج دالة hash(key)، هي حالة أكثر تعقيدًا. في الحالة الشائعة حيث تكون المفاتيح أرقامًا، أو سلاسل نصية، أو رموزًا، أو نطاقات، أو Expr، أو تركيبات من هذه الأنواع (عبر المصفوفات، أو الأزواج، أو المجموعات، أو الأزواج، إلخ) فهي آمنة للتجميع المسبق. ومع ذلك، بالنسبة لبعض أنواع المفاتيح الأخرى، مثل Function أو DataType وأنواع المستخدم المعرفة بشكل عام حيث لم تقم بتعريف دالة hash، تعتمد دالة hash الاحتياطية على عنوان الذاكرة للكائن (عبر objectid الخاص به) وبالتالي قد تتغير من تشغيل إلى آخر. إذا كان لديك أحد هذه الأنواع من المفاتيح، أو إذا لم تكن متأكدًا، لتكون في أمان يمكنك تهيئة هذه القاموس من داخل دالة __init__ الخاصة بك. بدلاً من ذلك، يمكنك استخدام نوع القاموس IdDict، الذي يتم التعامل معه بشكل خاص من قبل التجميع المسبق بحيث يكون آمنًا للتهيئة في وقت التجميع.

عند استخدام ما قبل التجميع، من المهم الحفاظ على فهم واضح للتمييز بين مرحلة التجميع ومرحلة التنفيذ. في هذا الوضع، سيكون من الواضح غالبًا أن جوليا هي مترجم يسمح بتنفيذ كود جوليا عشوائي، وليس مفسرًا مستقلًا يقوم أيضًا بإنشاء كود مجمع.

تشمل السيناريوهات المحتملة الأخرى للفشل المعروفة:

  1. عدادات عالمية (على سبيل المثال، لمحاولة تحديد الكائنات بشكل فريد). اعتبر مقتطف الشيفرة التالي:

    julia mutable struct UniquedById myid::Int let counter = 0 UniquedById() = new(counter += 1) end end

    بينما كانت نية هذا الكود هي إعطاء كل مثيل معرفًا فريدًا، يتم تسجيل قيمة العداد في نهاية التجميع. ستبدأ جميع الاستخدامات اللاحقة لهذه الوحدة المجمعة بشكل تدريجي من نفس قيمة العداد تلك.

    لاحظ أن objectid (الذي يعمل عن طريق تجزئة مؤشر الذاكرة) لديه مشاكل مشابهة (انظر الملاحظات حول استخدام Dict أدناه).

    بديل واحد هو استخدام ماكرو لالتقاط @__MODULE__ وتخزينه بمفرده مع قيمة counter الحالية، ومع ذلك، قد يكون من الأفضل إعادة تصميم الكود لعدم الاعتماد على هذه الحالة العالمية.

  2. تحتاج المجموعات الترابطية (مثل Dict و Set) إلى إعادة التجزئة في __init__. (في المستقبل، قد يتم توفير آلية لتسجيل دالة تهيئة.)

  3. اعتمادًا على الآثار الجانبية في وقت الترجمة التي تستمر خلال وقت التحميل. تشمل الأمثلة: تعديل المصفوفات أو المتغيرات الأخرى في وحدات جوليا الأخرى؛ الحفاظ على مقبضات للملفات أو الأجهزة المفتوحة؛ تخزين مؤشرات لموارد النظام الأخرى (بما في ذلك الذاكرة)؛

  4. إنشاء "نسخ" غير مقصودة من الحالة العالمية من وحدة أخرى، من خلال الإشارة إليها مباشرة بدلاً من عبر مسار البحث الخاص بها. على سبيل المثال، (في النطاق العالمي):

    ```julia #mystdout = Base.stdout #= will not work correctly, since this will copy Base.stdout into this module =#

    instead use accessor functions:

    getstdout() = Base.stdout #= best option =#

    or move the assignment into the runtime:

    init() = global mystdout = Base.stdout #= also works =# ```

تم فرض العديد من القيود الإضافية على العمليات التي يمكن القيام بها أثناء تجميع الكود مسبقًا لمساعدة المستخدم في تجنب حالات السلوك الخاطئ الأخرى:

  1. استدعاء eval للتسبب في تأثير جانبي في وحدة أخرى. سيؤدي هذا أيضًا إلى إصدار تحذير عند تعيين علامة ما قبل التجميع التزايدي.
  2. global const بيانات من النطاق المحلي بعد بدء __init__() (انظر المشكلة #12010 لخطط إضافة خطأ لذلك)
  3. استبدال وحدة هو خطأ في وقت التشغيل أثناء القيام بعملية تجميع مسبق تدريجي.

بعض النقاط الأخرى التي يجب أن تكون على دراية بها:

  1. لا يتم إعادة تحميل الكود / إبطال ذاكرة التخزين المؤقت بعد إجراء تغييرات على ملفات المصدر نفسها، (بما في ذلك بواسطة Pkg.update)، ولا يتم إجراء أي تنظيف بعد Pkg.rm
  2. يتم تجاهل سلوك مشاركة الذاكرة لمصفوفة معاد تشكيلها بواسطة ما قبل التجميع (كل عرض يحصل على نسخته الخاصة)
  3. Expecting the filesystem to be unchanged between compile-time and runtime e.g. @__FILE__/source_path() to find resources at runtime, or the BinDeps @checked_lib macro. Sometimes this is unavoidable. However, when possible, it can be good practice to copy resources into the module at compile-time so they won't need to be found at runtime.
  4. أجسام WeakRef والمُنهِيات لا تُعالج حاليًا بشكل صحيح بواسطة المُسجِّل (سيتم إصلاح ذلك في إصدار قادم).
  5. من الأفضل عادةً تجنب التقاط الإشارات إلى حالات كائنات البيانات الوصفية الداخلية مثل Method و MethodInstance و MethodTable و TypeMapLevel و TypeMapEntry وحقول تلك الكائنات، حيث يمكن أن يؤدي ذلك إلى إرباك المُسلسل وقد لا يؤدي إلى النتيجة التي ترغب فيها. ليس من الضروري أن يكون هذا خطأ، ولكن عليك ببساطة أن تكون مستعدًا لأن النظام سيحاول نسخ بعض هذه الكائنات وإنشاء حالة فريدة واحدة من غيرها.

أحيانًا يكون من المفيد أثناء تطوير الوحدة إيقاف التشغيل التدريجي للتجميع المسبق. يتيح لك علم سطر الأوامر --compiled-modules={yes|no|existing} تبديل التجميع المسبق للوحدات تشغيلًا وإيقافًا. عندما يتم بدء تشغيل جوليا مع --compiled-modules=no، يتم تجاهل الوحدات المسلسلة في ذاكرة التخزين المؤقت للتجميع عند تحميل الوحدات واعتماديات الوحدات. في بعض الحالات، قد ترغب في تحميل وحدات مسبقة التجميع موجودة، ولكن دون إنشاء وحدات جديدة. يمكن القيام بذلك عن طريق بدء جوليا مع --compiled-modules=existing. يتوفر تحكم أكثر دقة مع --pkgimages={yes|no|existing}، والذي يؤثر فقط على تخزين الشيفرة الأصلية أثناء التجميع المسبق. لا يزال يمكن استدعاء Base.compilecache يدويًا. يتم تمرير حالة علم سطر الأوامر هذا إلى Pkg.build لتعطيل تشغيل التجميع المسبق التلقائي عند تثبيت أو تحديث أو بناء الحزم بشكل صريح.

يمكنك أيضًا تصحيح بعض أخطاء ما قبل التجميع باستخدام متغيرات البيئة. قد يساعد تعيين JULIA_VERBOSE_LINKING=true في حل مشكلات الربط لمكتبات المشاركة من التعليمات البرمجية الأصلية المجمعة. راجع قسم وثائق المطور في دليل جوليا، حيث ستجد مزيدًا من التفاصيل في القسم الذي يوثق البنية الداخلية لجوليا تحت "صور الحزمة".