avataravatarjfmengels' blog About Home About

编译器提醒

作者

发布于 2025年4月27日

尽管很少这样称呼,但编译器提醒是 Elm 中一个非常有用的特性,也是使 Elm 代码具有可维护性的核心所在。

其核心思想是,每当代码中的更改会导致其他代码需要同时修改时,我们都会收到一个编译器错误,提醒我们需要进行某种更改。

我们在 Elm 中非常喜欢这种机制,以至于对于初学者来说,一个常见的任务是采用基本的 Elm counter example 并添加一个将计数器重置为 0 的按钮。这通常进展顺利(取决于不同的语法给他们带来多少困扰),因为编译器会告诉他们下一步该怎么做。

(我将逐步介绍,如果您想自己尝试,请立即暂停并尝试一下

  1. 我们添加一个按钮: button [ onClick Reset ] [ text "Reset" ]view 中的某个位置。

-- NAMING ERROR --------------------------------------------------- src/Main.elm

I cannot find a `Reset` variant:

38|     , button [ onClick Reset ] [ text "Reset" ]

^^^^^


  1. 我们收到一个编译器错误,指出 Reset 值未知,因此我们将其添加到 Msg 类型的变体列表中。

type Msg

  = Increment

  | Decrement

  | Reset


-- MISSING PATTERNS ----------------------------------------------- src/Main.elm

This `case` does not have branches for all possibilities:

25|>  case msg of

26|>    Increment ->

27|>      { model | count = model.count + 1 }

28|>

29|>    Decrement ->

30|>      { model | count = model.count - 1 }

Missing possibilities include:

Reset

I would have to crash if I saw one of those. Add branches for them!


  1. 我们收到一个编译器错误,指出 update 函数没有 Reset 变体的分支(因为编译器的穷举检查),因此我们添加将计数器设置为 0 的分支。

update : Msg -> Model -> Model

update msg model =

  case msg of

    -- ...other branches...

    Reset ->

      { model | count = 0 }

然后该功能就完成了!

我们添加了一段代码,得到了 2 个编译器错误,这些错误需要更多的更改——并且或多或少地指出了如何解决问题——一旦我们这样做,一切都按预期工作。

在 Elm 社区(以及其他社区中),我们经常将其称为遵循编译器(或编译器驱动开发),并将最终结果称为“如果它编译,它就能工作”。

有些人甚至制作了一个 workshop to teach Elm (法语)基于练习,你所需要做的就是遵循编译器错误消息。

类型检查和穷举检查是此示例背后的主要驱动力(还有其他因素)。因此,你可以说编译器提醒与类型安全和静态类型语言携手并进,但这不一定是必须的。

类型安全是指编译器确认我们是否以一种有意义的方式正确连接了所有内容,并避免了类型错误。编译器提醒是建立在此之上的技术,我们确保某些更改将迫使我们进行其他更改。

例如,假设我们没有显式处理所有情况,而是使用了默认/通配符分支:


update : Msg -> Model -> Model

update msg model =

  case msg of

    Increment ->

      { model | count = model.count + 1 }

   -- was previously

   -- Decrement ->

    _ ->

      { model | count = model.count - 1 }

那么我们不会收到处理 Reset 变体的第二个提醒,因为我们(在某种程度上不正确地)已经处理了它。

这就是为什么我们经常建议 Elm 开发人员在 case 表达式中列出所有分支,而不是使用通配符。即使有时会感到乏味,它也会增加进行更改导致获得编译器提醒的情况数量。

而且在我看来,编译器提醒对于可维护的代码库来说是一个非常重要的工具。

非编译器提醒

“提醒”的概念不限于编译器或类型检查器。

例如,如果我们在代码中引入一个变量,那么 linter 会告诉我们它未使用,从而提醒我们使用它(不要告诉我你从未添加一个变量并忘记使用它)。

同样,当删除变量的最后一次使用时,相同的 linter 也会告诉我们也要删除该变量。我们收到一个清理提醒。

如果我们可以定义自己的 linting 规则,那么我们可以创建自定义 linter 提醒。例如,假设我们有一些值旨在保存类型的所有不同变体:


type UserKind = User | Admin

allUserKinds : List UserKind

allUserKinds =

 [ User, Admin ]

假设我们添加了一种新的用户类型,例如 Guest,我们不希望忘记在 allUserTypes 中添加该值(那可能是我们或同事)。在工作中,我们有一条 linter 规则来提醒我们添加它。不是编译器提醒,但想法和好处相同。

不同的工具可以产生不同类型的提醒(或 guarantees)。甚至编写测试也可以用来创建一个。

提醒很重要,因为它们不会让我们忘记执行必要的任务,而且因为它们将相同的信息提供给可能没有被教导(并且可能永远不会被教导)代码库某些规则的同事。

高度可维护的代码库大量使用提醒。我们只需要为每个问题找到最合适的提醒类型。

Older post← Beyond Html.Lazy's argument limit Jeroen Engels • © 2025 • jfmengels' blog