编程

elm 语言简介及示例

1142 2023-12-15 01:15:00

Elm 是一种可编译为 JavaScript 的函数式语言。它可以帮助你制作网站和 web 应用。它是一个非常强调简单和质量的工具。

本文将:

  • 讲解 Elm 编程的基本原理。
  • 向你展示如何使用 Elm 框架制作互动式 App。
  • 强调适用于任何语言编程的原则和模式。

我希望最后你不仅可以使用 Elm 创建出色的 web 应用,而且能够理解使 Elm 易于使用的核心思想和模式。

如果你持观望态度,我可以放心地保证,如果你给 Elm 一个机会,并在其中真正制作一个项目,你最终会写出更好的 JavaScript 代码。想法很容易转移!

快速示例

这里有一个小程序,可以让你增加和减少一个数:

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)

main =
  Browser.sandbox { init = 0, update = update, view = view }

type Msg = Increment | Decrement

update msg model =
  case msg of
    Increment ->
      model + 1

    Decrement ->
      model - 1

view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (String.fromInt model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

点击此处到在线编辑器中尝试一下。

代码一开始看起来肯定不熟悉,所以我们快速来了解这个例子是如何工作的!

为什么是函数式语言?

你可以从函数式编程中获得一些好处,但有些东西你只能从 Elm 这样的函数式语言中获得:

  • 实践中没有运行时错误。
  • 友好的错误消息。
  • 可靠的重构。
  • 为所有 Elm 包自动执行语义版本控制。

任何 JS 库的组合都不能为您提供所有这些保证。它们来自语言本身的设计!多亏了这些保证,Elm 程序员说他们在编程时从未如此自信是很常见的。有信心快速添加功能。有信心重构数千行。但没有背后的焦虑,你错过了一些重要的事情!

我非常强调让 Elm 易于学习和使用,所以我只要求你给 Elm 一个机会,看看你的想法。我希望你会感到惊喜!

核心语言

让我们先感受一下 Elm 代码!

这里的目标是熟悉值和函数,这样当我们稍后看到更大的示例时,您将更有信心阅读 Elm 代码。

Elm 中最小的构建块称为值。这包括诸如 42True“Hello!” 之类的内容。

让我们从数字开始:

> 1 + 1
2

使用字符串:

> "hello" 
"hello" 

> "butter" ++ "fly" 
"butterfly"

使用 (++) 运算符将一些字符串组合在一起

当我们开始编写函数来转换这些基元值时,它们会变得更加有趣!

注意: 你可以在文档中 Basics 模块了解更多像 (+)(/)(++) 这样的操作符。

函数

函数是转换值的一种方式。接受一个价值,然后产生另一个价值。

例如,这里有一个 greet 函数,它接受一个名字并打招呼:

> greet name = 
|   "Hello " ++ name ++ "!" 
| 
<function> 

> greet "Alice" 
"Hello Alice!" 

> greet "Bob" 
"Hello Bob!"

传入函数的值通常称之为参数,因此你可以说 "greet 函数接收一个参数“。

接下来尝试一下接收两个参数的 madlib 函数:

> madlib animal adjective = 
|     "The ostentatious " ++ animal ++ " wears " ++ adjective ++ " shorts." 
| 
<function>

> madlib "cat" "ergonomic" 
"The ostentatious cat wears ergonomic shorts." 

> madlib ("butter" ++ "fly") "metallic" 
"The ostentatious butterfly wears metallic shorts."

请注意,我们在第二个示例中使用圆括号将 "butter" ++ "fly" 组合在一起。每个参数都需要是一个像 “cat” 这样的基元值,或者它需要在括号中!

注意:来自 JavaScript 等语言的人可能会惊讶于此处函数看起来有所不同:

madlib "cat" "ergonomic"                  -- Elm
madlib("cat", "ergonomic")                // JavaScript

madlib ("butter" ++ "fly") "metallic"      -- Elm
madlib("butter" + "fly", "metallic")       // JavaScript

一开始这可能会令人惊讶,但这种样式最终使用的括号和逗号更少。一旦你习惯了,它会让语言感觉非常干净和简洁!

If 表达式

如果你想在 Elm 中使用条件性行为,你可以使用 if 表达式。

> greet name = 
|   if name == "Abraham Lincoln" then 
|     "Greetings Mr. President!" 
|   else 
|     "Hey!" 
| 
<function>

> greet "Tom" 
"Hey!" 

> greet "Abraham Lincoln" 
"Greetings Mr. President!"

列表(List)

列表是 Elm 中最常用的数据结构之一。它们包含一系列相关的东西,类似于 JavaScript 中的数组。

列表可以包含许多值。这些值都必须具有相同的类型。以下是使用列表模块中的函数的几个示例:

> names = 
|   [ "Alice", "Bob", "Chuck" ] 
| 
["Alice","Bob","Chuck"] 

> List.isEmpty names 
False 

> List.length names 
3 

> List.reverse names 
["Chuck","Bob","Alice"] 

> numbers = 
|   [4,3,2,1] 
| 
[4,3,2,1] 

> List.sort numbers 
[1,2,3,4] 

> increment n = 
|   n + 1 
| 
<function>

> List.map increment numbers 
[5,4,3,2]

请记住,所有列表的元素都必需是相同类型!

元组(Tuple)

元组是另外一个有用的数据结构。一个元组有两到三个值,每个值都可以具有任何类型。一个常见的用法是,如果你需要从一个函数返回多个值。以下函数获取一个名称并为用户提供一条消息:

> isGoodName name = 
|   if String.length name <= 20 then 
|     (True, "name accepted!") 
|   else 
|     (False, "name was too long; please limit it to 20 characters") 
| 
<function> 

> isGoodName "Tom" 
(True, "name accepted!")

这可能非常方便,但当事情开始变得更加复杂时,通常最好使用记录而不是元组。

记录(Record)

记录可以持有多个值,每个值都与一个名称相关联。

这是一条记录,表示经济学家  John A. Hobson:

> john = 
|   { first = "John" 
|   , last = "Hobson" 
|   , age = 81 
|   } 
|   
{ age = 81, first = "John", last = "Hobson" } 

> john.last 
"Hobson"

我们使用三个字段(fileds)定义了一条记录,包含 John 的名字和年龄信息、

你也可以使用”字段访问函数“访问记录字段:

> john = { first = "John", last = "Hobson", age = 81 } 
{ age = 81, first = "John", last = "Hobson" } 

> .last john 
"Hobson" 

> List.map .last [john,john,john] 

["Hobson","Hobson","Hobson"]

在记录中更新值也很有用:

> john = { first = "John", last = "Hobson", age = 81 } 
{ age = 81, first = "John", last = "Hobson" } 

> { john | last = "Adams" }
{ age = 81, first = "John", last = "Adams" } 

> { john | age = 22 }
{ age = 22, first = "John", last = "Hobson" }

如果将这些表达式用语言表达,你会说,“我想要一个新版本的约翰,他的姓氏是 Adams” 或“约翰的年龄是 22 岁”。

请注意,当我们更新 john 的一些字段时,我们会创建一个全新的记录。它不会覆盖现有的。Elm 通过共享尽可能多的内容来提高效率。如果更新十个字段中的一个,则新记录将共享九个未更改的值。

因此,更新年龄的函数可能如下所示:

> celebrateBirthday person = 
| { person | age = person.age + 1 } 
| 
<function> 

> john = { first = "John", last = "Hobson", age = 81 } 
{ age = 81, first = "John", last = "Hobson" } 

> celebrateBirthday john 
{ age = 82, first = "John", last = "Hobson" }

像这样更新记录字段很常见!

Elm 架构

Elm 架构是一种用于构建交互式程序(如网络应用和游戏)的模式。

这种架构似乎在 Elm 中自然而然地出现了。早期的 Elm 程序员在他们的代码中不断发现相同的基本模式,而不是有人发明了它。看到人们在没有提前计划的情况下最终获得了架构良好的代码,真是有点可怕!

因此,Elm 架构在 Elm 中很简单,但在任何前端项目中都很有用。事实上,像 Redux 这样的项目受到了 Elm Architecture 的启发,所以你可能已经看到了这种模式的衍生物。重点是,即使你最终还不能在工作中使用 Elm,你也会从使用 Elm 和内化这种模式中得到很多。

基本模式

Elm 程序看起来总是这样

Diagram of The Elm Architecture

Elm 程序生成 HTML 以在屏幕上显式,然后计算机发回正在发生的事情的消息。“他们点击了一个按钮!

那么 Elem 程序发生了什么?它总能分解成三个部分:

  • 模型(Model) — 应用的状态
  • 视图(View) — 一个将状态转换成 HTML 的方式
  • 更新(Update) — 一个基于消息更新状态的方式

这三个概念是 Elm 架构的核心

Follow Along

更多示例,可以在在线编辑器中获取:

Buttons

我们的第一个例子是一个可以递增或递减的计数器。

我在下面列出了完整的程序。

import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)



-- MAIN


main =
  Browser.sandbox { init = init, update = update, view = view }



-- MODEL

type alias Model = Int

init : Model
init =
  0


-- UPDATE

type Msg = Increment | Decrement

update : Msg -> Model -> Model
update msg model =
  case msg of
    Increment ->
      model + 1

    Decrement ->
      model - 1


-- VIEW

view : Model -> Html Msg
view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (String.fromInt model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

现在你已经对代码进行了一点了解,您可能会有一些问题。main 值在做什么?不同的部分是如何组合在一起的?让我们浏览一下代码并讨论一下。

注意: 这里的代码使用类型注释、类型别名和自定义类型。不过,本节的重点是了解 Elm Architecture,所以我们稍后才会介绍它们。如果你在这些方面陷入困境,我鼓励你向前看!T

Main

main 值在 Elm 中是特殊的。它描述了屏幕上显示的内容。在这种情况下,我们将使用 init 值初始化我们的应用程序,view 函数将在屏幕上显示所有内容,用户输入将被送入到 update 函数中。可以将此视为对我们程序的高级描述。

Model

数据模型在 Elm 中十分重要。该模型(model)重点是将有关应用的所有细节捕获为数据。

要制作计数器,我们需要跟踪一个上下变化的数字。这意味着我们的模型这次真的很小:

type alias Model = Int

我们只需要一个 Int 值来跟踪当前计数。我们可以在初始化值中看到: value:

init : Model
init =
  0

初始值为 0,它会在按钮按下后增加或者减少。

View

我们有了模型,不过怎么在屏幕上显式呢?这就是 view 函数的作用了:

view : Model -> Html Msg
view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (String.fromInt model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

该函数使用 Model 作为参数。输出 HTML。所以我们说,我们想要显示一个递减按钮,当前计数,和一个递增按钮。

请注意,每个按钮都有一个 onClick handler。也就是说,当有人点击按钮时,生成一条消息。随意, 加号按钮生成一条 Increment 消息。这是什么呢,它会去到哪里呢? 到 update 函数。

Update

update 函数描述了我们的模型将如何随时间变化。

我们定义了两条它可能受到的消息:

type Msg = Increment | Decrement

到这里,update 函数描绘了当收到这些消息时,该如何处理。

update : Msg -> Model -> Model
update msg model =
  case msg of
    Increment ->
      model + 1

    Decrement ->
      model - 1

如果你收到 Increment 消息,你让模型增加。如果收到 Decrement 消息,则让模型减少。

因此,每当我们收到消息时,我们都会通过 update 来获得新的模型。然后,我们调用 view 来计算如何在屏幕上显示新模型。然后重复!用户输入生成一条消息,update 更新模型,在屏幕上查看 view。等

概览

既然您已经看到了 Elm 程序的所有部分,那么可能更容易看到它们如何融入我们之前看到的图表中:

 

Diagram of The Elm Architecture

Elm 首先在屏幕上呈现初始值。从那里你进入这个循环:

  1. 等待用户输入
  2. 发送消息给 update
  3. 生成新的 Model
  4. 调用 view 获取新的 HTML
  5. 在屏幕上展示新的 HTML 
  6. 重复!

这便是 Elm 架构的本质。

 

更多详情,点击此处