作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Vuk拥有IST硕士学位和十多年的Java和JavaScript全栈开发经验, 包括先进的算法.
当一个非常有趣和创新的项目的首席开发者建议从AngularJS切换到Elm时, 我的第一个想法是:为什么?
我们已经有了一个写得很好的AngularJS应用程序, well tested, 并在生产中得到验证. And Angular 4, 是AngularJS的一个值得升级, 可能是重写的自然选择——React或Vue也可以吗. 榆树似乎是一种奇怪的领域特定语言,人们几乎没有听说过.
那是在我了解埃尔姆之前. Now, 有一些经验, 尤其是从AngularJS过渡到它之后, 我想我可以回答这个“为什么”.”
In this article, 我们将通过Elm的优点和缺点,以及它的一些异国情调的概念如何完美地满足需求 前端web开发人员. 有关更像教程的Elm语言文章,您可以查看 this blog post.
如果您习惯使用Java或JavaScript编程,并且觉得这是编写代码的自然方式, 学榆树就像掉进兔子洞一样.
你会注意到的第一件事是奇怪的语法:没有大括号,有很多箭头和三角形.
您可能学会了不使用大括号,但是如何定义和嵌套代码块呢? Or, where is the for
循环,或者其他任何循环? 定义显式作用域 let
块,没有经典意义上的块,也没有循环.
Elm是一种纯函数式、强类型、响应式和事件驱动的web客户端语言.
您可能会开始怀疑这种编程方式是否可行.
In reality, 这些品质加起来会给你一个令人惊叹的编程和开发优秀软件的范例.
您可能认为使用较新版本的Java或ECMAScript 6, 你可以做函数式编程. 但是,这只是它的表面.
在这些编程语言中, 您仍然可以访问语言结构的兵工厂,并有可能求助于其中的非功能部分. 你真正注意到的区别是当你除了函数式编程什么都做不了的时候. 在这一点上,您最终开始感受到函数式编程是多么自然.
在Elm中,几乎所有东西都是函数. 记录名是一个函数, 联合类型值是一个函数——每个函数都由部分应用于其参数的函数组成. 甚至像加(+)和减(-)这样的运算符也是函数.
将一种编程语言声明为纯函数式语言, 而不是这种结构的存在, 其他一切的缺失是至关重要的. 只有这样,你才能开始以纯粹的功能方式思考.
Elm以成熟的函数式编程概念为模型, 它类似于Haskell和OCaml等其他函数式语言.
如果你用Java或TypeScript编程,你就会知道这意味着什么. 每个变量必须只有一种类型.
当然,存在一些差异. 和TypeScript一样,类型声明是可选的. 如果不存在,它将被推断. 但是没有“任何”类型.
Java支持泛型类型,但以一种更好的方式. Java中的泛型是后来添加的,所以除非另外指定,否则类型不是泛型的. 而且,为了使用它们,我们需要丑陋 <>
syntax.
在Elm中,除非另有指定,否则类型是泛型的. 让我们来看一个例子. 假设我们需要一个方法,它接受一个特定类型的列表并返回一个数字. 在Java中是:
public static int numFromList(List list){
return list.size();
}
用榆树的语言来说:
numFromList =
List.length list
虽然是可选的,但我强烈建议您总是添加类型声明. Elm编译器永远不会允许对错误类型进行操作. 对于人类来说,犯这样的错误要容易得多,尤其是在学习语言的时候. 所以上面带有类型注释的程序应该是:
numFromList: List a -> Int
numFromList =
List.length list
乍一看,在单独的行上声明类型似乎很不寻常, 但一段时间后,它开始看起来很自然.
这意味着Elm被编译成JavaScript,所以浏览器可以在网页上执行它.
鉴于此,它不是像Java或JavaScript with Node那样的通用语言.Js,而是一种特定于领域的语言,用于编写web应用程序的客户端部分. 更重要的是, Elm包括编写业务逻辑(JavaScript所做的), 以及表示部分(HTML所做的)——都是用一种函数式语言完成的.
所有这些都是以一种非常具体的类似框架的方式完成的,这种方式被称为Elm架构.
Elm架构是一个响应式web框架. 模型中的任何更改都会立即呈现在页面上,不需要显式的DOM操作.
在这方面,它类似于Angular或React. 但是,榆树也有自己的方式. 理解其基本原理的关键在于签名 view
and update
functions:
view : Model -> Html Msg
update : Msg -> Model -> Model
Elm视图不仅仅是模型的HTML视图. 它是HTML,可以产生类似的消息 Msg
, where Msg
是否定义了确切的联合类型.
任何标准页面事件都可以生成消息. And, 当产生消息时, Elm内部使用该消息调用更新函数, 然后根据消息和当前模型更新模型, 更新后的模型再次在内部呈现给视图.
与JavaScript非常相似,Elm是事件驱动的. 但不像Node.js, 在哪里为异步操作提供了单独的回调, 榆树事件被分组在离散的消息集合中, 在一个消息类型中定义. 而且,与任何联合类型一样,单独类型值所携带的信息可以是任何东西.
可以产生消息的事件源有三种:用户操作 Html
查看、执行我们订阅的命令和外部事件. 这就是为什么这三种类型, Html
, Cmd
, and Sub
contain msg
作为他们的论点. And, the generic msg
Type在所有三个定义中必须相同——与提供给更新函数的定义相同(在前面的示例中), it was the Msg
类型(大写M),其中所有消息处理都是集中的.
您可以在这里找到一个完整的Elm web应用程序示例 GitHub库. Although simple, 它展示了日常客户端编程中使用的大部分功能:从REST端点检索数据, 解码和编码JSON数据, using views, messages, 以及其他结构, 与JavaScript通信, 以及用Webpack编译和打包Elm代码所需要的一切.
应用程序显示从服务器检索到的用户列表.
以方便设置/演示过程, Webpack的开发服务器用于打包所有东西, including Elm, 并提供用户列表.
有些功能在Elm中,有些在JavaScript中. 这样做有一个重要的原因:显示互操作性. 你可能想从埃尔姆开始, 或者逐渐将现有的JavaScript代码转换为它, 或者在Elm语言中添加新的功能. 通过互操作性,您的应用程序可以继续使用Elm和JavaScript代码. 这可能是比在Elm中从头开始整个应用程序更好的方法.
示例代码中的Elm部分首先用JavaScript中的配置数据初始化, 然后检索用户列表并以Elm语言显示. 假设我们已经在JavaScript中实现了一些用户操作, 所以在Elm中调用用户操作只是将调用分派回给它.
该代码还使用了下一节中解释的一些概念和技术.
让我们来看看Elm编程语言在实际场景中的一些奇特概念.
这是纯金的榆树语言. 请记住,在结构上不同的数据需要用相同的算法使用的所有情况? 要模拟这些情况总是很困难的.
下面是一个示例:假设您正在为列表创建分页. 在每一页的末尾,应该有链接到前一页,下一页和所有页面的编号. 如何构建它来保存用户点击的链接的信息?
我们可以对previous使用多个回调, next, 点击页码, 或者我们可以使用一个或两个布尔字段来指示点击了什么, 或者为特定的整数值赋予特殊的含义, 比如负数, zero, etc. 但是这些解决方案都不能准确地模拟这种用户事件.
在Elm,这很简单. 我们将定义一个联合类型:
type NextPage
= Prev
| Next
| ExactPage Int
我们用它作为其中一条消息的参数:
type Msg
= ...
| ChangePage NextPage
最后,我们将函数更新为 case
检查类型 nextPage
:
更新MSG模型=
case msg of
ChangePage nextPage ->
case nextPage of
Prev ->
...
Next ->
...
ExactPage newPage ->
...
它使事情变得非常优雅.
<|
许多模块包括 map
函数,具有多个变体以应用于不同数量的参数. For example, List
has map
, map2
, … , up to map5
. 但是,如果我们有一个有六个参数的函数? There is no map6
. 但是,有一种技术可以克服这一点. It uses the <|
函数作为参数,部分函数,其中一些参数作为中间结果应用.
为简单起见,假设a List
only has map
and map2
,我们想应用一个函数,它在三个列表上有三个参数.
下面是实现的样子:
Map3 foo list1 list2 list3 =
let
partialResult =
List.Map2 foo list1 list2
in
List.map2 (<|) partialResult list3
假设我们想用 foo
,它只是将其数字参数相乘,定义如下:
foo a b c =
a * b * c
So the result of Map3 foo [1,2,3,4,5] [1,2,3,4,5] [1,2,3,4,5]
is [1,8,27,64,125]:列表号
.
让我们来解构一下这里发生了什么.
First, in partialResult =列表.Map2 foo list1 list2
, foo
部分适用于?中的每一对 list1
and list2
. The result is [foo 1 1, foo 2 2, foo 3 3, foo 4 4, foo 5 5]
, 一个函数列表,它接受一个参数(因为前两个参数已经应用)并返回一个数字.
Next in List.map2 (<|) partialResult list3
,它实际上是 List.map2 (<|) [foo 1 1, foo 2 2, foo 3 3, foo 4 4, foo 5 5] list3
. 对于这两个表中的每一对,我们称之为 (<|)
function. 例如,对于第一对,它是 (<|) (foo 1 1) 1
,等于 foo 1 1 <| 1
,等于 foo 1 1 1
, which produces 1
. 对于第二个,它将是 (<|) (foo 2 2) 2
, which is foo 2 2 2
,其计算结果为 8
, and so on.
这种方法对……特别有用 mapN
函数,用于解码具有许多字段的JSON对象,如 Json.Decode
将它们提供到 map8
.
假设我们有一个列表 Maybe
值,并且我们只想从具有1的元素中提取值. 例如,列表是:
list: list(可能是Int)
list =
[只有1,没有,只有3,没有,没有,只有6,只有7]
我们想要得到 [1,3,6,7]: List Int
. 解决方案是这一行表达式:
List.filterMap身份列表
让我们看看为什么会这样.
List.filterMap
期望第一个参数是一个函数 (a -> Maybe b)
, 它应用于所提供列表的元素(第二个参数), 并对结果列表进行过滤以省略所有内容 Nothing
值,然后从中提取实值 Maybe
s.
在我们的情况下,我们提供 identity
,所以得到的列表又是 [只有1,没有,只有3,没有,没有,只有6,只有7]
. 过滤之后,我们得到 [只要1,只要3,只要6,只要7]
,在价值提取之后,它就是 [1,3,6,7]
, as we wanted.
当我们对JSON解码(或反序列化)的需求开始超出JSON中所暴露的 Json.Decode
模块,我们可能在创建新的外来解码器时遇到麻烦. 这是因为这些解码器是从解码过程的中间调用的.g., within Http
methods, 它们的输入和输出并不总是很清楚, 特别是当提供的JSON中有很多字段时.
这里有两个示例来展示如何处理这类情况.
在第一个中,我们在传入JSON中有两个字段, a
and b
,表示矩形区域的边. 但是,在Elm对象中,我们只想存储它的面积.
import Json.解码暴露(..)
areaDecoder = map2(*)(字段“a”int)(字段“b”int)
result = decodeString areaDecoder """{"a":7,"b":4}""" "
——Ok 28:结果.结果字符串Int
字段分别用 field int
,然后将这两个值提供给中所提供的函数 map2
. 如乘法(*
)也是一个函数,它有两个参数,我们可以这样使用它. The resulting areaDecoder
是一个解码器,它在应用函数时返回结果,在本例中, a*b
.
在第二个例子中, 我们得到了一个混乱的状态字段, 也可以是空的, 或者任何字符串,包括empty, 但只有当它“OK”时,我们才知道手术成功了。. 在这种情况下,我们希望将它存储为 True
对于所有其他情况 False
. 我们的解码器是这样的:
okDecoder =
nullable string
|> andThen
(\ms ->
case ms of
Nothing -> succeed False
Just s -> if s == "OK" then succeed True else succeed False
)
让我们把它应用到一些json中:
decodeString (field "status" okDecoder) """{ "a":7, "status":"OK" }"""
-- Ok True
decodeString (field "status" okDecoder) """{ "a":7, "status":"NOK" }"""
-- Ok False
decodeString (field "status" okDecoder) """{ "a":7, "status":null }"""
-- Ok False
这里的键在提供给的函数中 andThen
,它接受前一个可空字符串解码器(它是一个 Maybe String
),将其转换为我们需要的任何内容,并在 succeed
.
从这些例子中可以看出, 对于Java和JavaScript开发人员来说,以函数式方式编程可能不是很直观. 需要一些时间来适应它,需要大量的试验和错误. 为了帮助理解它,您可以使用 elm-repl
练习和检查表达式的返回类型.
本文前面链接到的示例项目包含了更多自定义解码器和编码器的示例,这些示例也可能有助于您理解它们.
与其他客户端框架如此不同, Elm语言当然不是“又一个JavaScript库”.“因此,与他们相比,它有很多特征可以被认为是积极的或消极的.
让我们先从积极的一面开始.
最后,你有了一种语言,你可以做到这一切. 不再有分离,也不再有尴尬的混合. 无需在JavaScript中生成HTML,也无需使用一些精简逻辑规则的自定义模板语言.
在Elm中,你只有一种语法和一种语言.
因为几乎所有的概念都是基于函数的, 还有一些结构, 语法非常简洁. 您不必担心是否在实例或类级别上定义了某些方法, 或者如果它只是一个函数. 它们都是在模块级别定义的函数. 并且,迭代列表没有一百种不同的方法.
在大多数语言中, 关于代码是否按照语言的方式编写,总是有这样的争论. 很多习语需要掌握.
在Elm中,如果它编译,它可能是“Elm”的方式. 如果不是,好吧,它肯定不是.
Elm语法虽然简洁,但表达能力很强.
这主要是通过使用联合类型实现的, 正式类型声明, 功能性风格. 所有这些都激发了更小函数的使用. 最后,你得到的代码基本上是自文档化的.
当你长时间使用Java或JavaScript时, null
变成了对您来说完全自然的东西——编程中不可避免的一部分. 尽管我们经常看到 NullPointerException
s and various TypeError
我们仍然不认为真正的问题是……的存在 null
. It’s just so natural.
和Elm相处一段时间后,情况很快就明朗了. Not having null
不仅使我们免于一次又一次地看到运行时null引用错误, 它还通过清晰地定义和处理可能没有实际值的所有情况来帮助我们编写更好的代码, 这样也可以通过不推迟来减少技术债务 null
处理直到有东西坏掉.
创建语法正确的JavaScript程序可以非常快地完成. 但是,它真的会起作用吗? 那么,让我们重新加载页面并彻底测试之后看看.
榆树的情况正好相反. 使用静态类型检查和强制 null
检查,它需要一些时间来编译,特别是当一个初学者写一个程序. 但是,一旦编译完成,它就很有可能正常工作.
在选择客户端框架时,这可能是一个重要因素. 大型web应用的响应性通常对用户体验至关重要, 因此整个产品的成功. And, as tests show榆树跑得很快.
大多数传统的web框架都为创建web应用程序提供了强大的工具. 但这种能力是有代价的:过于复杂的架构,以及关于如何以及何时使用它们的许多不同概念和规则. 掌握这一切需要很多时间. 有控制器、组件和指令. 然后是编译和配置阶段,以及运行阶段. And then, 有服务, factories, 以及在提供的指令中使用的所有自定义模板语言——当我们需要调用时 $scope.$apply()
直接使页面刷新,等等.
Elm编译到JavaScript当然也非常复杂, 但是开发人员不必了解它的所有细节. 只需编写一些Elm,让编译器完成它的工作.
对榆树的赞美够多了. 现在,让我们看看它不太好的一面.
这确实是个大问题. 榆树语缺乏详细的手册.
官方教程只是略读了一下该语言,并留下了许多未回答的问题.
官方的API参考甚至更糟糕. 许多函数缺乏任何形式的解释或示例. And, 然后还有这样的句子:“如果这让你困惑, 学习榆树架构教程. 这真的很有帮助!这不是你想在官方API文档中看到的最好的一行.
希望这种情况能很快改变.
我不相信Elm会在如此糟糕的文档下被广泛采用, 尤其是那些来自Java或JavaScript的人, 这些概念和函数根本就不是直观的. 为了掌握它们,需要更好的带有大量示例的文档.
去掉大括号或圆括号,用空格来缩进看起来会不错. 例如,Python代码看起来非常整洁. 但是对于 elm-format
但这还不够.
用所有的双行空格, 表达式和赋值被分成多行, 榆树代码看起来更垂直而不是水平. 在古老的C语言中,怎样才能轻松地将一行代码扩展到Elm语言中的多个屏幕.
如果你的收入是按代码行数计算的,这听起来可能不错. But, 如果要将某些内容与较早开始的150行表达式对齐, 祝你找到正确的缩进.
和他们一起工作很困难. 修改记录字段的语法很难看. 没有简单的方法来修改嵌套字段或按名称任意引用字段. 如果您以通用的方式使用访问器函数, 正确的打字有很多麻烦.
In JavaScript, 记录或对象是可以构造的中心结构, accessed, 并在很多方面进行了修改. 甚至JSON也只是记录的序列化版本. 开发人员习惯于在web编程中处理记录, 因此,如果将它们用作主要数据结构,那么在Elm中处理它们的困难就会变得明显.
Elm比JavaScript需要编写更多的代码.
对于字符串和数字操作没有隐式类型转换, 有很多int-float转换,特别是 toString
需要打电话, 然后需要括号或函数应用程序符号来匹配正确的参数数量. Also, the Html.text
函数需要一个字符串作为参数. 对于所有这些,都需要大量的case表达式 Maybe
s, Results
, types, etc.
造成这种情况的主要原因是严格的类型系统,这可能是一个公平的代价.
更多类型真正突出的一个领域是JSON处理. What is simply a JSON.parse()
在Elm语言中,JavaScript中的调用可以跨越数百行.
当然,JSON和Elm结构之间需要某种映射. 但是需要为同一段JSON编写解码器和编码器是一个严重的问题. 如果您的REST api传输具有数百个字段的对象,这将是大量的工作.
我们已经看到了榆树, 是时候回答那些众所周知的问题了, 可能和编程语言本身一样古老:它比竞争对手更好吗? 我们应该在我们的项目中使用它吗?
第一个问题的答案可能是主观的, 正如不是所有的工具都是锤子,也不是所有的东西都是钉子. 在许多情况下,Elm可以比其他web客户端框架更出色,而在其他情况下则有所不足. 但它提供了一些真正独特的价值,可以使web前端开发比其他选择更安全、更容易.
第二个问题, 为了避免回答同样古老的“看情况而定”,简单的回答是:是的. 即使有上面提到的所有缺点, Elm给你的信心,你的程序是正确的,这是足够的理由使用它.
用Elm编码也很有趣. 对于习惯于“传统”web编程范式的人来说,这是一个全新的视角.
In real use, 你不需要立即将你的整个应用切换到Elm,或者完全在其中开始一个新的应用. 您可以利用它与JavaScript的互操作性来尝试一下, 从用Elm语言编写的部分接口或某些功能开始. 你很快就会发现它是否适合你的需要,然后扩大它的使用范围,或者离开它. 谁知道呢,你可能也会爱上函数式web编程的世界.
Elm是一种纯函数式、强类型、响应式和事件驱动的web客户端语言.
世界级的文章,每周发一次.
世界级的文章,每周发一次.