» » Пример Model-View-Update архитектуры на F#

 

Пример Model-View-Update архитектуры на F#

Автор: admin от 14-07-2019, 02:35, посмотрело: 18

Кому-то не нравился Redux в React из-за его имплементации на JS?



Мне он не нравился корявыми switch-case в reducer'ах, есть языки с более удобным pattern matching, и типы лучше моделирующие события и модель. Например, F#.

Эта статья — разъяснение устройства обмена сообщениями в Elmish.

тут, они похожи на рассмотренные.



Чтобы проверить работоспособность программы, я приведу тесты на несколько сценариев:





Для этого используется библиотека 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 именно тот dispatch который используется во view, он принимает Message и возвращает unit

Sub — функция подписчик, принимает dispatch и возвращает unit, мы порождаем список Sub, когда используем ofMsg:



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


После вызова ofMsg, как, например Cmd.ofMsg RememberModel в конце метода updateChangeAuthor, через некоторое время вызовется подписчик и сообщение попадет в метод update

Cmd — Лист Sub



Перейдем к типу Program, это generic тип, принимает тип модели, сообщения и view, в консольном приложении нет нужны что-то возвращать из view, но в Elmish.React view возвращает F# структуру DOM дерева.



Поле init — вызывается на старте elmish, эта функция возвращает начальную модель и первое сообщение, в моем случае я возвращаю Cmd.ofMsg RememberModel

Update — главная функция update, вы с ней уже знакомы.



SetState — в стандартном Elmish принимает только модель и dispatch и вызывает view, но мне нужно передавать msg, чтобы подменять view в зависимости от сообщения, я покажу ее реализацию после того, как мы рассмотрим обмен сообщениями.



Функция runWith, получает конфигурацию, далее вызывает init, возвращаются модель и первое сообщение, на строчках 2,3 объявляются два изменяемых объекта, первый — в котором будет храниться state, второй нужен функции dispatch.



На 4 строке объявляется buffer — можно воспринимать его как очередь, первый зашел — первый вышел(на самом деле реализация RingBuffer, очень интересна, я взял ее из библиотеки, советую ознакомиться на github)



Далее идет сама рекурсивная функция dispatch, та же самая, что вызывается во view, при первом вызове мы минуем if на строчке 6 и сразу попадаем в цикл, ставим reented значение true, чтобы последующие рекурсивные вызовы, не заходили снова в этот цикл, а добавляли новое сообщение в buffer.



На строчке 9 выполняем метод update, из которого забираем измененную модель и новое сообщение(в первый раз это сообщение RememberModel)

На строчке 10 отрисовывается модель, метод SetState выглядит так:



Пример Model-View-Update архитектуры на F#

Как вы видите, разные сообщения вызывают разные view

Это необходимая мера, чтобы не блокировать поток, потому что вызов Console.ReadLine блокирует поток программы, и такие события как RememberModel,ChangeColor (которые инициируются внутри программы, а не пользователем) будут каждый раз ждать пока пользователь нажмет на кнопку, хотя просто должны изменить цвет.



В первый раз будет вызвана функция OnlyShowView, которая просто отрисует модель.

Eсли бы вместо RememberModel в метод пришло сообщение WaitUserAction, то вызвалась бы функция ShowAndUserActionView, которая отрисует модель и заблокирует поток, ожидая нажатия кнопки, как только кнопка будет нажата снова вызовется метод dispatch, и сообщение будет запушено в buffer(потому что reenvited= false)



Далее нужно обработать все сообщения, пришедшие из метода update, иначе мы их потеряем, рекурсивные вызовы попадут в цикл только если reented станет false. 11 строчка выглядит сложно, но на самом деле это просто push всех сообщения в buffer:



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


Для всех подписчиков, возвращенных методом update, будет вызван dispatch, тем самым эти сообщения будут добавлены в buffer.



На 12 строке обновляем модель, достаем новое сообщение и возвращаем значение reented на false, когда buffer не пустой это не нужно, но если там не осталось элементов и dispatch может быть вызван только из view, это имеет смысл. Опять же в нашем случае, когда все синхронно, это не имеет смысла, так как мы ожидаем синхронный вызов dispatch на 10 строчке, но если в коде есть асинхронные вызовы, возможен вызов dispatch из callback'a и нужно иметь возможность продолжить выполнение программы.



Ну вот и все описание функции dispatch, на 15 строке она вызывается и на 16 возвращается state.



В консольном приложении выход происходит, когда buffer становится пустым. В оригинальной версии runWith ничего не возвращает, но без этого тестирование невозможно.



Program для тестирования отличается, функция createProgram принимает список сообщений, которые бы инициировал пользователь и в SetState они подменяют обычное нажатие:

Пример Model-View-Update архитектуры на F#

Еще одно отличие моей измененной версии от оригинальной — сначала вызывается функция update, а потом только setState, в оригинальной версии наоборот, сначала происходит отрисовка, а потом обработка сообщений, я вынужден был на это пойти из-за блокирующего вызова Console.ReadKey (необходимости менять view)



Я надеюсь, мне удалось объяснить как устроен Elmish и подобные системы, за бортом осталось довольно много функционала Elmish, если вас заинтересовала это тема, советую заглянуть на их сайт.



Спасибо за внимание!



Источник: Хабр / Интересные публикации

Категория: Программирование

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *