مثال بنية طراز - عرض - تحديث في F #

شخص ما لم يعجبه Redux في React بسبب تنفيذه على JS؟


لم يعجبني حالة التبديل الخرقاء في المخفضات ، فهناك لغات بها مطابقة نمط أكثر ملاءمة ، وأنواع أحداث ونماذج أفضل للطراز. على سبيل المثال ، F #.
هذه المقالة هي شرح لجهاز المراسلة في Elmish .


سأقدم مثالًا على تطبيق وحدة التحكم المكتوب على هذه البنية ، وسوف يكون واضحًا في مثاله كيفية استخدام هذا النهج ، وبعد ذلك سوف نفهم بنية Elmish.


كتبت تطبيق وحدة تحكم بسيط لقراءة القصائد ، في الأساس هناك العديد من القصائد ، واحدة لكل مؤلف ، والتي يتم عرضها على وحدة التحكم.


تحتوي النافذة على 4 سطور فقط من النص ، بالضغط على الزرين "Up" و "Down" ، يمكنك التمرير خلال القصيدة ، والأزرار الرقمية تغير لون النص ، والأزرار الموجودة على اليسار واليمين تتيح لك التنقل عبر تاريخ الإجراءات ، على سبيل المثال ، يقرأ المستخدم قصيدة Pushkin ، ويتم تبديل قصيدة Yesenin ، لون النص ، ثم ظن أن اللون لم يكن جيدًا جدًا ولم يعجبه Esenin ، ثم نقر مرتين على السهم الأيسر ثم عاد إلى المكان الذي أنهى فيه قراءة Pushkin.


تبدو هذه المعجزة كما يلي:



النظر في التنفيذ.


إذا كنت تفكر في جميع الخيارات ، فمن الواضح أن كل ما يمكن للمستخدم القيام به هو الضغط على الزر ، بالضغط عليه ، يمكنك تحديد ما يريده المستخدم ، ويمكنه:


  1. تغيير المؤلف
  2. تغيير اللون
  3. التمرير (أعلى / أسفل)
  4. انتقل إلى الإصدار السابق / التالي

نظرًا لأن المستخدم يجب أن يكون قادرًا على الرجوع إلى الإصدار مرة أخرى ، فأنت بحاجة إلى إصلاح تصرفاته وتذكر النموذج ، ونتيجة لذلك ، يتم وصف جميع الرسائل الممكنة على النحو التالي:


type Msg = | ConsoleEvent of ConsoleKey | ChangeAuthor of Author | ChangeColor of ConsoleColor | ChangePosition of ChangePosition | ChangeVersion of ChangeVersion | RememberModel | WaitUserAction | Exit type ChangeVersion = | Back | Forward type ChangePosition = | Up | Down type Author = | Pushkin | Lermontov | Blok | Esenin type Poem = Poem of string 

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


 type Model = { viewTextInfo: ViewTextInfo countVersionBack: int history: ViewTextInfo list } type ViewTextInfo = { text: string; formatText: string; countLines: int; positionY: int; color: ConsoleColor } 

العمارة Elmish - نموذج عرض التحديث ، وقد تم بالفعل النظر في النموذج ، دعنا ننتقل لعرض:


 let SnowAndUserActionView (model: Model) (dispatch: Msg -> unit) = let { formatText = ft; color = clr } = model.viewTextInfo; clearConsoleAndPrintTextWithColor ft clr let key = Console.ReadKey().Key; Msg.ConsoleEvent key |> dispatch let clearConsoleAndPrintTextWithColor (text: string) (color: ConsoleColor) = Console.Clear(); Console.WriteLine() Console.ForegroundColor <- color Console.WriteLine(text) 

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


تحديث :


 let update (msg: Msg) (model: Model) = match msg with | ConsoleEvent key -> model, updateConsoleEvent key | ChangeAuthor author -> updateChangeAuthor model author | ChangeColor color -> updateChangeColor model color | ChangePosition position -> updateChangePosition model position | ChangeVersion version -> updateChangeVersion model version | RememberModel -> updateAddEvent model | WaitUserAction -> model, [] 

اعتمادًا على نوع msg ، تحدد الوظيفة التي ستقوم بمعالجة الرسالة.


هذا هو تحديث لإجراء المستخدم ، ورسم الزر للرسالة ، والحالة الأخيرة - إرجاع حدث WaitUserAction - تجاهل النقر والانتظار لمزيد من إجراءات المستخدم.


 let updateConsoleEvent (key: ConsoleKey) = let msg = match key with | ConsoleKey.D1 -> ChangeColor ConsoleColor.Red | ConsoleKey.D2 -> ChangeColor ConsoleColor.Green | ConsoleKey.D3 -> ChangeColor ConsoleColor.Blue | ConsoleKey.D4 -> ChangeColor ConsoleColor.Black | ConsoleKey.D5 -> ChangeColor ConsoleColor.Cyan | ConsoleKey.LeftArrow -> ChangeVersion Back | ConsoleKey.RightArrow -> ChangeVersion Forward | ConsoleKey.P -> ChangeAuthor Author.Pushkin | ConsoleKey.E -> ChangeAuthor Author.Esenin | ConsoleKey.B -> ChangeAuthor Author.Blok | ConsoleKey.L -> ChangeAuthor Author.Lermontov | ConsoleKey.UpArrow -> ChangePosition Up | ConsoleKey.DownArrow -> ChangePosition Down | ConsoleKey.X -> Exit | _ -> WaitUserAction msg |> Cmd.ofMsg 

نقوم بتغيير المؤلف ، لاحظ أن countVersionBack تتم إعادة تعيينه على الفور إلى 0 ، مما يعني أنه إذا تراجع المستخدم عن قصته ثم أراد تغيير اللون ، فسيتم التعامل مع هذا الإجراء كإجراء جديد وسيتم إضافته إلى السجل .


 let updateChangeAuthor (model: Model) (author: Author) = let (Poem updatedText) = seed.[author] let updatedFormatText = getlines updatedText 0 3 let updatedCountLines = (splitStr updatedText).Length let updatedViewTextInfo = {model.viewTextInfo with text = updatedText; formatText = updatedFormatText; countLines = updatedCountLines } { model with viewTextInfo = updatedViewTextInfo; countVersionBack = 0 }, Cmd.ofMsg RememberModel 

نرسل أيضًا رسالة RememberModel ، يقوم معالجها بتحديث السجل ، بإضافة النموذج الحالي.


 let updateModelHistory model = { model with history = model.history @ [ model.viewTextInfo ] }, Cmd.ofMsg WaitUserAction 

يمكن رؤية بقية التحديثات هنا ، فهي مشابهة لتلك التي تم النظر فيها.


لاختبار أداء البرنامج ، سأقدم اختبارات للعديد من السيناريوهات:


اختبارات

تأخذ طريقة التشغيل الهيكل الذي يتم فيه تخزين قائمة الرسائل وإرجاع النموذج بعد معالجتها.


 [<Property(Verbose=true)>] let ``    `` (authors: Author list) = let state = (createProgram (authors |> List.map ChangeAuthor) |> run) match (authors |> List.tryLast) with | Some s -> let (Poem text) = seed.[s] state.viewTextInfo.text = text | None -> true [<Property(Verbose=true)>] let ``    `` changeColorMsg = let state = (createProgram (changeColorMsg|>List.map ChangeColor)|> run) match (changeColorMsg |> List.tryLast) with | Some s -> state.viewTextInfo.color = s | None -> true [<Property(Verbose=true,Arbitrary=[|typeof<ChangeColorAuthorPosition>|])>] let ``        `` msgs = let tryLastSomeList list = list |> List.filter (Option.isSome) |> List.map (Option.get) |> List.tryLast let lastAuthor = msgs |> List.map (fun x -> match x with | ChangeAuthor a -> Some a | _ -> None) |> tryLastSomeList let lastColor = msgs |> List.map (fun x -> match x with | ChangeColor a -> Some a | _ -> None) |> tryLastSomeList let state = (createProgram msgs |> run) let colorTest = match lastColor with | Some s -> state.viewTextInfo.color = s | None -> true let authorTest = match lastAuthor with | Some s -> let (Poem t) = seed.[s]; state.viewTextInfo.text = t | None -> true authorTest && colorTest 

لهذا ، يتم استخدام مكتبة FsCheck ، والتي توفر القدرة على توليد البيانات.


الآن لننظر إلى جوهر البرنامج ، لقد تم كتابة الكود باللغة الميش لجميع المناسبات ، لقد قمت بتبسيطه ( الكود الأصلي) :


 type Dispatch<'msg> = 'msg -> unit type Sub<'msg> = Dispatch<'msg> -> unit type Cmd<'msg> = Sub<'msg> list type Program<'model, 'msg, 'view> = { init: unit ->'model * Cmd<'msg> update: 'msg -> 'model -> ('model * Cmd<'msg>) setState: 'model -> 'msg -> Dispatch<'msg> -> unit } let runWith<'arg, 'model, 'msg, 'view> (program: Program<'model, 'msg, 'view>) = let (initModel, initCmd) = program.init() //1 let mutable state = initModel //2 let mutable reentered = false //3 let buffer = RingBuffer 10 //4 let rec dispatch msg = let mutable nextMsg = Some msg; //5 if reentered //6 then buffer.Push msg //7 else while Option.isSome nextMsg do // 8 reentered <- true // 9 let (model, cmd) = program.update nextMsg.Value state // 9 program.setState model nextMsg.Value dispatch // 10 Cmd.exec dispatch cmd |> ignore //11 state <- model; // 12 nextMsg <- buffer.Pop() // 13 reentered <- false; // 14 Cmd.exec dispatch initCmd |> ignore // 15 state //16 let run program = runWith program 

نوع Dispath <'msg> هو بالضبط الإرسال المستخدم في العرض ، ويستغرق وضع الرسائل والإرجاع
Sub <'msg> - وظيفة المشترك ، يقبل وحدة الإرسال والإرجاع ، نحن نفرز القائمة الفرعية عندما نستخدم ofMsg :


 let ofMsg<'msg> (msg: 'msg): Cmd<'msg> = [ fun (dispatch: Dispatch<'msg>) -> dispatch msg ] 

بعد استدعاء ofMsg ، مثل Cmd.ofMsg RememberModel في نهاية طريقة updateChangeAuthor ، بعد فترة من الوقت يتم استدعاء المشترك وتدخل الرسالة في طريقة التحديث
كمد <'msg> - ورقة فرعية <' msg>


دعنا ننتقل إلى نوع البرنامج ، هذا نوع عام ، ويستغرق نوع النموذج والرسالة وطريقة العرض ، في تطبيق وحدة التحكم ليست هناك حاجة لإرجاع شيء من العرض ، لكن في Elmish.React ، تقوم بإرجاع بنية F # لشجرة DOM.


يتم استدعاء حقل init في بداية elmish ، تقوم هذه الوظيفة بإرجاع النموذج الأولي والرسالة الأولى ، وفي حالتي أعود Cmd.ofMsg RememberModel
التحديث هو الوظيفة الرئيسية للتحديث ، فأنت على دراية به بالفعل.


SetState - في Elmish القياسي ، لا يقبل إلا طريقة العرض وطريقة إرسال المكالمات والمكالمات ، لكنني أحتاج إلى تمرير msg لاستبدال العرض اعتمادًا على الرسالة ، وسأظهر تنفيذه بعد النظر في المراسلة.


تستقبل وظيفة runWith التكوين ، ثم تستدعي init ، يتم إرجاع النموذج والرسالة الأولى ، على الأسطر 2.3 يتم الإعلان عن كائنين قابلين للتغيير ، الأول - حيث سيتم تخزين الحالة ، تكون الثانية مطلوبة من قبل وظيفة الإرسال .


في السطر الرابع ، يتم الإعلان عن المخزن المؤقت - يمكنك أن تأخذه كقائمة انتظار ، جاء الأول - خرج الأول (في الواقع ، تطبيق RingBuffer ممتع للغاية ، أخذته من المكتبة ، أنصحك بقراءته على جيثب )


بعد ذلك ، تأتي وظيفة الإرسال العودية نفسها ، وهي نفس الوظيفة التي تُدعى في العرض ، في المكالمة الأولى ، ونحن نتجاوز خط if على السطر 6 ونذهب فورًا إلى الحلقة ، ونعيد ضبطها على true حتى لا تعود المكالمات المتكررة لاحقًا إلى هذه الحلقة ، ولكننا نضيف رسالة جديدة في المخزن المؤقت .


على السطر 9 ، ننفذ طريقة التحديث ، التي ننتقي منها النموذج الذي تم تغييره ورسالة جديدة (لأول مرة هذه هي رسالة RememberModel )
السطر 10 يرسم النموذج ، تبدو طريقة SetState كما يلي:



كما ترون ، النشرات المختلفة تسبب وجهات نظر مختلفة
يعد هذا إجراءً ضروريًا لعدم حظر التدفق ، لأن استدعاء Console.ReadLine يحظر تدفق البرنامج ، وستنتظر أحداث مثل RememberModel و ChangeColor (التي يتم تشغيلها داخل البرنامج ، وليس بواسطة المستخدم) في كل مرة ينقر فيها المستخدم على الزر ، على الرغم من أنه يحتاج فقط إلى التغيير اللون.


لأول مرة ، سيتم استدعاء وظيفة OnlyShowView ، والتي سوف ترسم النموذج ببساطة.
إذا ظهرت رسالة WaitUserAction على الطريقة بدلاً من RememberModel ، فسيتم استدعاء وظيفة ShowAndUserActionView ، والتي من شأنها رسم النموذج وحظر الدفق ، في انتظار الضغط على الزر ، بمجرد الضغط على الزر ، سيتم استدعاء طريقة الإرسال مرة أخرى وسيتم إرسال الرسالة إلى المخزن المؤقت (بسبب إعادة التنشيط = خطأ )


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


 let exec<'msg> (dispatch: Dispatch<'msg>) (cmd: Cmd<'msg>) = cmd |> List.map (fun sub -> sub dispatch) 

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


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


حسنًا ، هذا هو الوصف الكامل لوظيفة الإرسال ، في السطر 15 ، يتم استدعاؤها ويتم إرجاع الحالة في السطر 16.


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


يختلف برنامج الاختبار ، وتقبل وظيفة createProgram قائمة بالرسائل التي سيبدأها المستخدم ، وفي SetState يستبدلون النقرات المعتادة:


هناك اختلاف آخر بين إصداري الذي تم تغييره والإصدار الأصلي وهو أن وظيفة التحديث تسمى أولاً ، ثم وظيفة setState فقط ، على العكس من ذلك ، في الإصدار الأصلي ، مما يؤدي إلى معالجة الرسالة أولاً ثم ، اضطررت إلى القيام بذلك بسبب استدعاء حظر إلى Console.ReadKey (تحتاج إلى تغيير عرض )


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


شكرا لاهتمامكم!

Source: https://habr.com/ru/post/ar459870/


All Articles