Elm编程语言入门

本文概述

当一个非常有趣和创新的项目的首席开发人员建议从AngularJS切换到Elm时, 我的第一个想法是:为什么?

我们已经有一个编写良好的AngularJS应用程序, 该应用程序处于固态, 经过良好测试并在生产中得到证明。 Angular 4是AngularJS值得升级的版本, 它本来是重写的自然选择-React或Vue也是如此。Elm似乎是人们很少听说的一种奇怪的领域特定语言。

榆木编程语言

好吧, 那是在我对Elm一无所知之前。现在, 有了一些经验, 尤其是从AngularJS过渡到它之后, 我想我可以回答这个”为什么”。

在本文中, 我们将探讨Elm的利弊, 以及其某些奇特的概念如何完全适合前端Web开发人员的需求。有关更多类似教程的Elm语言文章, 你可以查看此博客文章。

Elm:一种纯粹的函数编程语言

如果你习惯于使用Java或JavaScript进行编程, 并且觉得这是编写代码的自然方式, 那么学习Elm就像掉进了兔子坑。

你会注意到的第一件事是奇怪的语法:没有花括号, 很多箭头和三角形。

你可能会学到没有大括号, 但是如何定义和嵌套代码块?或者, 对于该问题, for循环或任何其他循环在哪里?虽然可以使用let块定义显式范围, 但经典意义上没有块, 也没有循环。

Elm是一种纯函数性, 强类型, 响应式和事件驱动的Web客户端语言。

你可能会开始怀疑, 是否可以通过这种方式进行编程。

实际上, 这些品质加起来为你提供了令人惊叹的编程和开发优秀软件的范例。

纯函数

你可能会认为, 通过使用Java或ECMAScript 6的较新版本, 可以进行函数式编程。但这仅仅是表面。

在那些编程语言中, 你仍然可以使用语言构造库, 并且倾向于使用其非函数性部分。你真正注意到的区别是, 你只能执行函数式编程。在这一点上, 你最终开始感觉到函数式编程的自然程度。

在Elm中, 几乎所有东西都是函数。记录名称是一个函数, 联合类型值是一个函数-每个函数都由部分应用到其参数的函数组成。连加号(+)和减号(-)之类的运算符都是函数。

要声明一种编程语言纯粹是函数性的, 而不是仅仅存在这样的构造, 最重要的是没有其他所有内容。只有这样, 你才能开始以纯粹的函数方式进行思考。

Elm以成熟的函数式编程概念为模型, 并且类似于Haskell和OCaml等其他函数式语言。

强类型

如果你使用Java或TypeScript编程, 那么你知道这意味着什么。每个变量必须只有一种类型。

当然存在一些差异。与TypeScript一样, 类型声明是可选的。如果不存在, 将进行推断。但是没有”任何”类型。

Java支持通用类型, 但是以更好的方式。 Java中的泛型是在以后添加的, 因此除非另有说明, 否则类型不是泛型的。而且, 要使用它们, 我们需要难看的<>语法。

在Elm中, 除非另有说明, 否则类型是通用的。让我们看一个例子。假设我们需要一个方法, 该方法接受特定类型的列表并返回一个数字。在Java中, 它将是:

public static <T> int numFromList(List<T> list){
  return list.size();
}

并且, 以Elm语言:

numFromList list =
  List.length list

尽管是可选的, 但我强烈建议你始终添加类型声明。 Elm编译器绝不允许对错误类型进行操作。对于人类来说, 犯这样的错误要容易得多, 尤其是在学习语言时。因此, 上面带有类型注释的程序将是:

numFromList: List a -> Int
numFromList list =
  List.length list

起初在单独的一行上声明类型似乎很不寻常, 但是一段时间后, 它开始看起来很自然。

网络客户端语言

这仅意味着Elm可以编译为JavaScript, 因此浏览器可以在网页上执行它。

鉴于此, 它不是Java或带有Node.js的JavaScript这样的通用语言, 而是用于编写Web应用程序客户端部分的特定于域的语言。不仅如此, Elm包括用一种函数语言编写业务逻辑(JavaScript做什么)和表示部分(HTML做什么)。

所有这些都是以非常特定的框架式方式完成的, 称为Elm体系结构。

反应性

Elm体系结构是一个反应式Web框架。模型中的任何更改都将立即显示在页面上, 而无需显式的DOM操作。

这样, 它类似于Angular或React。但是, Elm也以自己的方式做到这一点。了解其基础知识的关键在于查看和更新​​函数的签名:

view : Model -> Html Msg
update : Msg -> Model -> Model

Elm视图不仅是模型的HTML视图。它是可以产生Msg类型消息的HTML, 其中Msg是你定义的确切联合类型。

任何标准页面事件都可以产生一条消息。并且, 当生成消息时, Elm在内部使用该消息调用更新函数, 然后该函数根据消息和当前模型更新模型, 并将更新后的模型再次内部呈现给视图。

事件驱动

与JavaScript一样, Elm是事件驱动的。但是与Node.js不同, 在Node.js中, 为异步操作提供了单独的回调, 而Elm事件则分组在离散的消息集中, 并以一种消息类型定义。而且, 像任何联合类型一样, 单独的类型值所携带的信息也可以是任何东西。

可以产生消息的事件有三种来源:Html视图中的用户操作, 命令的执行以及我们订阅的外部事件。因此, Html, Cmd和Sub这三种类型都包含msg作为其参数。并且, 通用msg类型在所有三个定义中必须相同-提供给更新函数的定义相同(在上一示例中, 它是Msg类型, 具有大写M), 其中所有消息处理都集中在一个地方。

实际示例的源代码

你可以在此GitHub存储库中找到完整的Elm Web应用程序示例。尽管很简单, 但它显示了日常客户端编程中使用的大多数功能:从REST端点检索数据, 解码和编码JSON数据, 使用视图, 消息和其他结构, 与JavaScript进行通信以及编译和打包所需的所有功能Webpack的Elm代码。

ELM网站示例

该应用程序显示从服务器检索的用户列表。

为了简化设置/演示过程, Webpack的开发服务器用于打包所有内容(包括Elm)并提供用户列表。

Elm中有一些功能, JavaScript中有一些功能。这样做是出于一个重要的原因:显示互操作性。你可能想要尝试启动Elm, 或逐渐将现有的JavaScript代码切换到它, 或以Elm语言添加新功能。通过互操作性, 你的应用程序可以继续使用Elm和JavaScript代码。与从Elm重新启动整个应用程序相比, 这可能是一种更好的方法。

示例代码中的Elm部分首先使用来自JavaScript的配置数据初始化, 然后以Elm语言检索并显示用户列表。假设我们已经在JavaScript中实现了一些用户操作, 因此在Elm中调用用户操作只是将调用分派给它。

该代码还使用了下一节中介绍的一些概念和技术。

Elm概念的应用

让我们在实际场景中研究Elm编程语言的一些奇特概念。

联合类型

这是Elm语言的纯金。还记得所有需要使用相同算法的结构不同数据的情况吗?很难模拟这些情况。

这是一个示例:假设你正在为列表创建分页。在每页的末尾, 应该有按其编号指向上一页, 下一页和所有页面的链接。你如何构造它来保存用户单击了哪个链接的信息?

我们可以将多个回调用于上一个, 下一个和页面编号的单击, 也可以使用一个或两个布尔字段来指示单击的内容, 或者为特定的整数值赋予特殊含义, 例如负数, 零等。这些解决方案可以精确地为这种用户事件建模。

在Elm中, 这非常简单。我们将定义一个联合类型:

type NextPage
    = Prev
    | Next
    | ExactPage Int

我们将其用作消息之一的参数:

type Msg
    = ...
    | ChangePage NextPage

最后, 我们更新该函数以提供一种情况来检查nextPage的类型:

update msg model =
    case msg of
      ChangePage nextPage ->
        case nextPage of
            Prev ->
              ... 

            Next ->
              ...
            ExactPage newPage ->
              ...

它使事情变得非常优雅。

使用<|创建多个地图函数

许多模块都包含一个map函数, 并具有多种变体以应用于不同数量的参数。例如, List具有map, map2, …, 直到map5。但是, 如果我们有一个需要六个参数的函数呢?没有地图6。但是, 有一种技术可以克服这一问题。它使用<|函数作为参数, 以及部分函数, ​​其中一些参数用作中间结果。

为简单起见, 假设一个List仅具有map和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

因此map3 foo [1, 2, 3, 4, 5] [1, 2, 3, 4, 5] [1, 2, 3, 4, 5]的结果为[1, 8, 27, 64, 125]:清单编号。

让我们解构这里发生的事情。

首先, 在partialResult = List.map2 foo list1 list2中, foo被部分应用于list1和list2中的每对。结果为[foo 1 1, foo 2 2, foo 3 3, foo 4 4, foo 5 5], 这些函数采用一个参数(因为已经应用了前两个参数)并返回一个数字。

接下来在List.map2(<|)部分结果list3中, 它实际上是List.map2(<|)[foo 1 1, foo 2 2, foo 3 3, foo 4 4, foo 5 5] list3。对于这两个列表的每对, 我们都在调用(<|)函数。例如, 对于第一对, 它是(<|)(foo 1 1)1, 与foo 1 1 <|相同1, 它与产生1的foo 1 1 1相同。第二个是(<|)(foo 2 2)2, 它是foo 2 2 2, 其值为8, 依此类推。

该方法在mapN函数中用于解码具有多个字段的JSON对象时特别有用, 因为Json.Decode最多可以将它们提供给map8。

从Maybes列表中删除所有值

假设我们有一个Maybe值列表, 并且我们只想从具有一个值的元素中提取值。例如, 列表为:

list : List (Maybe Int)
list =
    [ Just 1, Nothing, Just 3, Nothing, Nothing, Just 6, Just 7 ]

并且, 我们想要得到[1, 3, 6, 7]:List Int。解决方法是这一行表达式:

List.filterMap identity list

让我们来看看为什么这行得通。

List.filterMap期望第一个参数是一个函数(a->也许b), 该函数应用于提供的列表的元素(第二个参数), 并且对结果列表进行过滤以忽略所有Nothing值, 然后将实数过滤掉值是从Maybes中提取的。

在我们的例子中, 我们提供了身份, 因此结果列表再次为[Just 1, Nothing, Just 3, Nothing, Nothing, Just 6, Just 7]。过滤后, 我们得到[Just 1, Just 3, Just 6, Just 7], 在值提取后, 根据需要, 它是[1, 3, 6, 7]。

自定义JSON解码

随着我们对JSON解码(或反序列化)的需求开始超过Json.Decode模块中公开的需求, 我们可能很难创建新的奇异解码器。这是因为这些解码器是从解码过程的中间(例如在Http方法中)调用的, 并且并不总是清楚它们的输入和输出是什么, 尤其是在提供的JSON中有很多字段的情况下。

这是两个示例, 说明如何处理此类情况。

在第一个中, 传入的JSON中有两个字段a和b, 表示矩形区域的边。但是, 在Elm对象中, 我们只想存储其面积。

import Json.Decode exposing (..)
areaDecoder = map2 (*) (field "a" int) (field "b" int)
result = decodeString areaDecoder """{ "a":7, "b":4 }"""
-- Ok 28 : Result.Result String Int

使用字段int解码器分别对字段进行解码, 然后将这两个值都提供给map2中提供的函数。由于乘法(*)也是一个函数, 并且需要两个参数, 因此可以像这样使用它。所得的areaDecoder是一个解码器, 在应用时返回函数的结果, 在这种情况下为a * b。

在第二个示例中, 我们得到了一个混乱的状态字段, 该字段可以为null或任何包含空的字符串, 但是我们知道只有在” 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)的结果, 将其转换为我们需要的任何内容, 并在成功的帮助下将结果作为解码器返回。

重点介绍

从这些示例可以看出, 对于Java和JavaScript开发人员, 以功能方式进行编程可能不是很直观。要花一些时间才能习惯它, 并且要经过反复试验。为了帮助理解它, 你可以使用elm-repl进行练习并检查表达式的返回类型。

本文前面链接的示例项目包含更多自定义解码器和编码器的示例, 这也可能有助于你理解它们。

但是, 为什么选择Elm呢?

与其他客户端框架如此不同, Elm语言当然不是”另一个JavaScript库”。因此, 它具有很多特征, 与之相比, 它们可以被认为是积极的或消极的。

让我们从正面开始。

没有HTML和JavaScript的客户端编程

最后, 你拥有一种可以完成所有任务的语言。没有更多的分离, 并且它们混合起来很尴尬。没有使用JavaScript生成HTML, 也没有使用某些简化的逻辑规则的自定义模板语言。

使用Elm, 你将只有一种语法和一种语言。

均匀度

由于几乎所有概念都基于函数和少量结构, 因此语法非常简洁。你不必担心是否在实例或类级别上定义了某些方法, 或者仅仅是一个函数。它们都是在模块级别定义的功能。而且, 没有一百种不同的方法可以迭代列表。

在大多数语言中, 始终存在关于是否以语言方式编写代码的争论。许多习语需要掌握。

在Elm中, 如果可以编译, 则可能是” Elm”方式。如果没有, 那肯定不是。

表现力

尽管简洁, 但Elm语法却很有表现力。

这主要是通过使用联合类型, 形式类型声明和功能样式来实现的。所有这些都激发了较小功能的使用。最后, 你将获得几乎可以自我记录的代码。

没有空

当你长时间使用Java或JavaScript时, null对你来说是完全自然的事情–这是编程的必然部分。而且, 尽管我们经常看到NullPointerExceptions和各种TypeError, 但我们仍然不认为真正的问题是null的存在。很自然

与Elm待了一段时间后, 情况很快变得清晰起来。不使用null不仅使我们免于一遍又一遍地看到运行时null参考错误, 而且还可以通过清晰地定义和处理所有可能没有实际价值的情况来帮助我们编写更好的代码, 从而通过不推迟null来减少技术负担处理直到出现故障。

对它有用的信心

创建语法正确的JavaScript程序可以很快完成。但是, 它真的可以工作吗?好吧, 让我们看看重新加载页面并进行全面测试之后。

与Elm相反。使用静态类型检查和强制执行的空检查, 它需要一些时间来编译, 尤其是在初学者编写程序时。但是, 一旦编译完成, 很有可能会正常运行。

快速

在选择客户端框架时, 这可能是一个重要因素。广泛的Web应用程序的响应能力通常对于用户体验至关重要, 因此对于整个产品的成功至关重要。而且, 如测试所示, Elm非常快。

Elm与传统框架的优点

大多数传统的Web框架都提供了用于创建Web应用程序的强大工具。但是, 这种能力带来了代价:过于复杂的架构, 其中包含许多关于如何使用它们以及何时使用它们的概念和规则。掌握这一切需要花费很多时间。有控制器, 组件和指令。然后是编译和配置阶段, 以及运行阶段。然后, 提供的指令中包含服务, 工厂和所有自定义模板语言, 在所有这些情况下, 我们需要直接调用$ scope。$ apply()来刷新页面等等。

Elm编译为JavaScript当然也很复杂, 但是保护开发人员不必知道它的所有细节。只需编写一些Elm, 然后让编译器完成工作即可。

而且, 为什么不选择Elm呢?

赞扬Elm就足够了。现在, 让我们看看它的一些不太好的方面。

文献资料

这确实是一个重大问题。Elm语言缺少详细的手册。

官方教程只是浏览了该语言, 并留下了许多未解决的问题。

官方API参考甚至更糟。许多功能缺少任何形式的解释或示例。而且, 还有一些句子:”如果这令人困惑, 请阅读Elm Architecture Tutorial。确实有帮助!”不是你想在官方API文档中看到的最伟大的一行。

希望这会很快改变。

我认为Elm不能被如此差劲的文档所广泛采用, 尤其是对于那些来自Java或JavaScript的人来说, 这些概念和功能根本不直观。为了掌握它们, 需要具有许多示例的更好的文档。

格式和空格

除去花括号或括号并使用空格进行缩进看起来不错。例如, Python代码看起来非常整洁。但是对于elm格式的创作者来说, 这还不够。

由于所有的双行都有空格, 并且表达式和赋值分成多行, 所以Elm代码看起来更垂直而不是水平。在老式C语言中, 单线的语言很容易扩展到Elm语言的多个屏幕。

如果你通过编写的代码行付款, 这听起来不错。但是, 如果你想使某个内容与早于150行开始的表达式对齐, 那么祝你找到正确的缩进成为好运。

记录处理

与他们合作很困难。修改记录字段的语法很丑陋。没有简单的方法可以修改嵌套字段或通过名称任意引用字段。而且, 如果你以通用方式使用访问器函数, 则正确键入会带来很多麻烦。

在JavaScript中, 记录或对象是可通过许多方式构造, 访问和修改的中央结构。甚至JSON也只是记录的序列化版本。开发人员习惯于在Web编程中处理记录, 因此, 如果将它们用作主要数据结构, 则在Elm中处理它们的困难会变得很明显。

更多打字

Elm比JavaScript需要编写更多的代码。

字符串和数字操作没有隐式类型转换, 因此需要大量的int-float转换, 尤其是toString调用, 然后需要括号或函数应用程序符号来匹配正确数量的参数。此外, Html.text函数需要字符串作为参数。对于所有这些Maybes, Results, Type等, 都需要很多case表达式。

造成这种情况的主要原因是严格的类型系统, 并且可能要付出合理的代价。

JSON解码器和编码器

JSON处理是其中更多键入真正突出的领域。 JavaScript中的简单JSON.parse()调用可以跨越Elm语言的数百行。

当然, JSON和Elm结构之间需要某种映射。但是需要为同一块JSON编写解码器和编码器是一个严重的问题。如果你的REST API传输具有数百个字段的对象, 则将需要大量工作。

本文总结

我们已经了解了Elm, 现在该回答一些可能与编程语言一样古老的著名问题了:它比竞争对手好吗?我们应该在项目中使用它吗?

第一个问题的答案可能是主观的, 因为不是每个工具都是锤子, 也不是所有东西都是钉子。在许多情况下, Elm可以脱颖而出, 并成为其他Web客户端框架的更好选择, 而在其他情况下则显得不足。但是它提供了一些真正独特的价值, 可以使Web前端开发比其他选择更安全, 更轻松。

对于第二个问题, 为了避免也回答古老的”取决于情况”, 简单的答案是:是的。即使提到了所有缺点, 但Elm给你的信心是你的程序正确的信心才足以使用它。

在Elm中编码也很有趣。对于习惯了”常规”网络编程范例的任何人来说, 这都是一个全新的视角。

在实际使用中, 你不必立即将整个应用切换到Elm或完全在其中启动一个新应用。你可以利用它与JavaScript的互操作性进行尝试, 从界面的一部分或以Elm语言编写的某些功能开始。你将迅速找出它是否适合你的需求, 然后扩大其用途或放弃它。谁知道, 你也可能会爱上功能性Web编程领域。

相关:挖掘用于前端开发的ClojureScript

微信公众号
手机浏览(小程序)
0
分享到:
没有账号? 忘记密码?