elm 语言简介及示例
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 中最小的构建块称为值。这包括诸如 42
、True
和 “Hello!”
之类的内容。
让我们从数字开始:
> 1 + 1
2
使用字符串:
> "hello"
"hello"
> "butter" ++ "fly"
"butterfly"
使用 (++)
运算符将一些字符串组合在一起
当我们开始编写函数来转换这些基元值时,它们会变得更加有趣!
函数
函数是转换值的一种方式。接受一个价值,然后产生另一个价值。
例如,这里有一个 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 程序看起来总是这样
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 程序的所有部分,那么可能更容易看到它们如何融入我们之前看到的图表中:
Elm 首先在屏幕上呈现初始值。从那里你进入这个循环:
- 等待用户输入
- 发送消息给
update
- 生成新的
Model
- 调用
view
获取新的 HTML - 在屏幕上展示新的 HTML
- 重复!
这便是 Elm 架构的本质。
更多详情,点击此处。