F#中的示例模型-视图-更新体系结构

有人不喜欢React中的Redux,因为它在JS上实现了吗?


我不喜欢减速器笨拙的开关盒,有些语言具有更方便的模式匹配,并且可以键入更好的模型事件和模型。 例如,F#。
本文是Elmish中消息传递设备的说明


我将给出一个用这种架构编写的控制台应用程序的示例,在其示例中将清楚如何使用这种方法,然后我们将理解Elmish架构。


我写了一个简单的控制台应用程序来阅读诗歌,种子中有几首诗歌,每位作者一首,显示在控制台上。


该窗口仅包含4行文字,通过按下“上”和“下”按钮,您可以滚动浏览这首诗,数字按钮可更改文本的颜色,而左右按钮则可让您浏览动作的历史记录,例如,用户阅读普希金的诗,切换为耶塞宁的诗,颜色,然后以为颜色不是很好,叶塞宁不喜欢它,双击左箭头,回到他读完普希金的地方。


这个奇迹看起来像这样:



考虑实施。


如果您仔细考虑所有选项,那么很明显,用户可以做的就是按下按钮,通过按下按钮,您可以确定用户想要什么,并且他希望:


  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体系结构-model-view-update,已经考虑了模型,让我们继续查看:


 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绘制的,等待用户的反应,然后将此消息发送给update函数。
稍后,我们将详细研究调用dispatch时究竟发生了什么以及它具有什么样的功能。


更新


 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 

其余的更新可以在这里看到,它们与所考虑的相似。


为了测试程序的性能,我将针对以下几种情况进行测试:


测验

run 方法 采用存储消息列表的结构,并在处理消息后返回模型。


 [<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库,该库提供了生成数据的功能。


现在考虑程序的核心,Elmish中的代码是为所有场合编写的,我对其进行了简化( 原始代码)


 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>类型正是视图中使用的调度,它接受Message并返回单位
Sub <'msg> -订阅者函数,接受调度并返回unit ,我们在使用ofMsg生成 Sub列表:


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

调用ofMsg之后 (例如,在updateChangeAuthor方法末尾的Cmd.ofMsg RememberModel) ,一段时间后调用了订阅服务器,消息进入了update方法
Cmd <'msg> -表格Sub <'msg>


让我们继续前进到Program类型,它是一个泛型类型,它接受模型,消息和视图的类型,在控制台应用程序中不需要从视图中返回某些内容,但是在Elmish.React视图中它返回DOM树的F#结构。


在elmish的开头调用init字段,此函数返回初始模型和第一条消息,在我的情况下,我返回Cmd.ofMsg RememberModel
更新update的主要功能,您已经熟悉它。


SetState-在标准Elmish中,它仅接受模型并调度并调用view ,但是我需要根据消息传递msg来替换视图 ,在考虑消息传递之后,我将展示其实现。


runWith函数接收配置,然后调用init ,返回模型和第一条消息,在第2,3行上声明了两个可变对象,第一个-将存储状态 ,第二个是分派函数需要。


在第四行,声明了缓冲区 -您可以将其作为队列,第一个进来-第一个进来(实际上, RingBuffer的实现非常有趣,我从库中获取了它,建议您在github上阅读它)


接下来是递归调度函数本身,即在view中被调用的那个函数,在第一次调用时,我们绕过第6行的if行,立即进入循环,将reented设置为true,以便后续的递归调用不会返回此循环,而是添加缓冲区中有新消息。


在第9行,我们执行update方法,从中我们获取更改的模型和新消息(这是第一次是RememberModel消息)
10行绘制模型, SetState方法如下所示:



如您所见,不同的帖子导致不同的观点
这是一种不阻止流程的必要措施,因为调用Console.ReadLine会阻止程序流程,并且每次用户单击按钮时, RememberModel,ChangeColor (由程序内部触发,而不是由用户触发)之类的事件都会等待,尽管它们只是需要更改颜色。


第一次将调用OnlyShowView函数,该函数将简单地绘制模型。
如果WaitUserAction消息而不是RememberModel到达该方法,则将调用ShowAndUserActionView函数,该函数将绘制模型并阻塞流,等待按钮被按下,一旦按下按钮,将再次调用dispatch方法,并将消息发送到缓冲区 (因为reenvited = false )。


接下来,您需要处理所有来自update方法的消息,否则我们将丢失它们,仅当reented变为false时,递归调用才会进入循环。 第11行看起来很复杂,但是实际上这只是缓冲区中所有消息的推送:


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

对于由update方法返回的所有订阅者,将调用dispatch ,从而将这些消息添加到buffer


在第12行,我们更新了模型,获得了一条新消息,并在缓冲区不为空时将其返回为false,这是没有必要的,但是如果没有剩余元素并且只能从view调用dispatch ,这是有道理的。 同样,在我们的情况下,当一切都同步时,这没有意义,因为我们希望在第10行进行同步调度调用,但是如果代码中存在异步调用,则可以从回调中调用调度 ,并且您需要能够继续执行程序。


好了,这就是调度功能的全部描述,在第15行调用它,并在第16行返回状态


在控制台应用程序中,当缓冲区为空时退出。 在原始版本中, runWith不返回任何内容,但如果没有此功能,则无法进行测试。


用于测试的程序有所不同, createProgram函数接受用户将启动的消息列表,并在SetState中替换通常的单击:


我更改后的版本与原始版本之间的另一个区别是,首先调用了update函数,然后仅调用了setState函数 ,相反,在原始版本中,先呈现然后是消息处理,由于阻塞了对Console.ReadKey的调用,我不得不这样做(需要更改查看


希望我能解释Elmish和类似系统的工作原理,但仍然有很多Elmish功能无法使用,如果您对此主题感兴趣,建议您访问他们的网站


感谢您的关注!

Source: https://habr.com/ru/post/zh-CN459870/


All Articles