vlambda博客
学习文章列表

函数式编程中的战斗机(五)--elm编写实例(薛定鄂的猫)



# 1.初始设置


今天编写一个elm应用实例。首先,做好初始设置。



Elm init



运行命令后,会在目录中创建一个elm.json配置文件,以及一个名为src的目录,我们在该目录中用编辑器。可以是vscode、atom等专门甚至最简单的记事本。在src目录中创建一个名为main.elm文件。该文件就是要编写应用代码的文件。


用编辑器打开文件,建议用vscode,最世界上最流行的程序代码编辑工具。


初次设置完成后,正式进入elm编程之旅。


# 2.薛定谔的猫


估计许多人都听说过薛定谔的猫的实验。在一个封闭的房间中放入一只活猫,房间中放置有毒药陷阱。因为房间是密封的。在房门关闭时,我们无法从外界观测到猫是死是活,因此猫处在一种非死非活的不确定性状态中;当我们打开房门时,可以观察到猫的状态,猫的死活也确定下来。薛定谔的猫阐述的是一种量子不确定现象。


我们的任务就是编写模拟薛定谔的猫实验的elm应用代码。
函数式编程中的战斗机(五)--elm编写实例(薛定鄂的猫)


# 3.定义规则


我们需要定义一个包含门、锁、毒药陷阱(拔掉保险丝时开始运转)三种事物的房间。


规则1:如果门开,那么拔掉保险丝。


规则2:如果保险丝被拔掉,它能够重新放上。


规则3:如果门开,重新放保险丝。


规则4:如果门开,能关门。


规则5:如果门关,能开门或上锁。


规则6:如果上锁,能解锁。


每一种事物都可以用多种可能的状态组合表示,并且明确可能状态之间的转换规则是什么。


上述规矩转成对象状态如下:


-- 可能的状态:


Door(门):
Locked(上锁)
Closed(关门)
Opened(开门)
Alarm(毒药陷阱):
Armed (触发)
Disarmed (没触发)
Triggered(保险丝)
--可能的状态组合有:
Locked + Armed(上锁+毒药陷阱触发)
Locked + Triggered(上锁+拔掉保险丝)
Locked + Disarmed (上锁+毒药陷阱没有触发)
Unlocked + Armed (开锁+毒药陷阱触发)
Unlocked + Triggered (开锁+拔掉保险丝)
Unlocked + Disarmed (开锁+毒药陷阱没有触发)
Opened + Triggered (开门+拔掉保险丝)
Opened + Disarmed(开门+毒药陷阱没有触发)
--互相转换的状态有:
Door(门):
Closed <-> Locked (关门 <-> 上锁)
Closed <-> Opened (关门 <-> 开门)
Alarm(毒药陷阱):
Armed -> Triggered (毒药陷阱触发 -> 拔掉保险丝)
Triggered -> Disarmed (拔掉保险丝 -> 毒药陷阱没有触发)
Armed <-> Disarmed (毒药陷阱触发 <-> 毒药陷阱没有触发)



# 4.建立模型


OK,规则确定好后,我们会发现,在监测开始的时间点,房间只能真正存在一种可能的状态组合。因此要定义一个显示房间状态的模型model,它包括了监测时间点时的状态等。这个模型要将失败的情景加上去,防止观测不到的错误产生,用自定义类型添加到mail.elm代码中去。


 type Model = DisplayingRoom DoorState AlarmState | Failure String type DoorState = Opened | Closed | Locked type AlarmState = Armed | Disarmed | Triggered


好了,我们薛定谔的猫模型建立起来,完成第一步代码。
函数式编程中的战斗机(五)--elm编写实例(薛定鄂的猫)


# 5.建立更新模型的逻辑


要对模型进行更新,必须要有信息通知到达才能开始更新。因此更新逻辑前需先定义消息:


type Msg = Open | Close | Lock | Unlock | Arm | Disarm




消息定义后,开始定义update更新函数,实现消息->模型->返回一个新模型,我们先从检查房间状态开始定义:



   update msg model =
      case model of
         DisplayingRoom doorState alarmState ->
            ...
      Failure errorMessage ->
            model


先建立函数框架,如果因为故障原因无法观测到房间状态,那么更新函数只能返回原来的模型model。


当可以正常地观测房间时,如果房门处于打开状态下,受到的限制最大:门开、锁解、毒药陷阱不能起作用。


**第一步,让我们添加房门打开时的更新代码,扩展原来基础框架:**



   update : Msg -> Model -> Model
   update msg model=
      case model of
         DisplayingRoom doorState alarmState ->
            case doorState of
               Opened ->
                  case msg of
               Close ->
                  DisplayingRoom Closed alarmState
               _ -> Failure "故障,观测不到!”
         Failure _ ->
               model


直到门关,毒药陷阱才可能被触发。


上述代码中,我们处理了,房间由开门转为关门时模型的更新代码:


  


 DisplayingRoom Closed alarmState


其中alarmState是包含了毒药陷阱触发或没有被触发两种状态的变量,确保门由开转关时。毒药陷阱的状态可以原封不动地转移到新的模型上。


 
 _ -> Failure "故障,观测不到!"


这是一个兜底代码,确保无法获取监测状态时返回一个消息通知。这种考虑周全的机制,也是elm编译运行不出错的优势。


**第二步,让我们添加门关时的更新逻辑,较为复杂,打开门时,里面涉及到锁的状态、毒药陷阱的状态。**


在门关的前提下,如果收到开门的消息,那么要检测毒药陷阱的状态,触发或没有触发。因为毒药陷阱的状态,关系到猫的死活。如果毒药陷阱一直不被触发,猫仍然存活。但这些在未打开房门的情况下,我们是无法监测而至的,现在添加门关情况下的代码。



   Closed ->
      casemsg of
         Open ->
            case alarmState of
                  Armed ->
                     DisplayingRoom Opened Triggered
                  _ ->
                     DisplayingRoom Opened alarmState
               Lock ->
               DisplayingRoom Locked alarmState
                  Arm ->
                  DisplayingRoom Closed Armed
                  Disarm ->
                     DisplayingRoom Closed Disarmed
      _ -> Failure "故障,观测不到!"


**第三步,让我们添加门锁住情况下的更新代码:**


```bash
   Locked ->
      case msg of
         Unlock ->
            DisplayingRoom Closed alarmState
         Arm -> DisplayingRoom Locked Armed
            Disarm -> DisplayingRoom Locked Disarmed
         _ -> Failure "故障,观测不到!"
```


好了,我们代码中最重要的引擎,update更新函数完成了。下一步是把更新后的模型通过UI视图显示出来,这对于elm来说轻而易举,因为它本身就是为了构建前端而生。


# 6.构建前端界面


elm有一个HTML库,它使我们可以在elm中编写HTML代码:



   import Html exposing (..)


现在通过来引入这个库,为简单起见,这里只是把房间的状态用文字在html界面中展示出来:


首先建立一个failure情况下的显示函数:



   failure message =
      div []
         [ p [] [ "故障,观测不到!" ] ]


然后把各种状态组合用文字展示出来,这里要注意的是,按照房间规则,门关时的状态要有两个可能的消息,但是门开时只需要一条消息即可。最后UI界面代码如下:



   View: Model -> Html Msg
   view model =
      Case model of
      Failure message =
            div []
                p [] [ "故障,观测不到!" ] ]
      DispalyingRoom doorState alarmState=
            div []
                [case doorState of
      Opened ->
      Div []
                     [ p [] [ "门开-> 关门!" ] ]
                 Closed =
      Div []
                     [ p [] [ "门关-> 开门!" ] ]
      Locked =
                   Div []
                     [ p [] [ "门锁-> 开锁!" ] ]
      ]
   ,div []
      [ case alarmState of
      Armed ->
      Div []
                     [ p [] [ "毒药陷阱被触发-> 开门!" ] ]
      disArmed ->
      Div []
                     [ p [] [ "毒药陷阱没有触发-> 毒药陷阱被触发!" ] ]
      Triggered ->
      Div []
                     [ p [] [ "拔掉保险丝-> 毒药陷阱被触发/毒药陷阱没有触发!" ] ]
         ]


为简单起见,这里只用文字显示作为HTML的内容,但elm的HTML库还有许多强大的功能,可以与react的JSX比美。下回我们再尝试构建更绚丽的图象或动画显示房间状态。



# 7.连接代码


模型、更新、界面代码已经写好,剩下的是把这几部分连接起来。我们需要ELM的主核心Browser模块里面的沙箱sandbox:



   Import Browser exposing (..)



沙箱允许您创建使用elm架构的应用程序,但不会与”外部世界”对话(即任何外部的API或JavaScript,如果需要与外部世界对话可以用Browser.element或其它)。建立沙箱前我们还要声明一下房间的初始状态,假说为门已经关闭、猫已经放入、毒药陷阱的保险丝已经拔开。



   initialModel : Model

   initialModel = DisplayingRoom Closed Armed

   main : Program () Model Msg

      main = Browser.sandbox

         { init = initialModel

         , view = view

         , update = update

         }



# 8.运行程序


在终端中运行命令:


 



 Elm make src/main.elm




Elm make 是elm的编译器命令,它把main.elm编译成一个html文件。可以用浏览器打个这个html文件,让我们作为观察者,通过开门、关门的点击操作来模拟这个薛定谔的猫的实验。


(备注:本周写的文章学习了尼莫的《我希望有的榆木示例》思路,三体状态不容易描述,用javascript写估计一大堆代码,elm容易读些,薛定谔的猫实验因为涉及到不确定性,应该还要引入随机发生器,有时间再慢慢改进。)