Elm 中的原生代码和效果管理器指南 - 第一部分:命令 原生/内核代码 效果管理器 附录

2025-06-07

Elm 原生代码和效果管理器指南 - 第一部分:命令

本机/内核代码

效果管理器

附录

本指南旨在帮助您了解 Elm 如何实现其核心库,以及如何编写自己的库。
本文要求您具备 Elm 和 JavaScript 的编写经验。

Elm 使用以下资源组合来实现核心库(属于elmelm-explorations组织,例如elm/coreelm-explorations/test):

  • 常规 Elm 代码
  • 本机/内核代码
  • 效果管理器
  • 编译器魔法

我们这里就不讨论最后一个了。
需要注意的是,有些包只使用了其中的几个,例如elm/project-metadata-utils纯 Elm 实现的。

注意:原生代码和效果管理器只能由elmelm-explorations1组织编译,因此普通用户无法使用。
如果您仍想使用这些功能,可以使用编译器的分支版本,但我不会在这里讨论这个问题。

即使您对构建自己的本机模块不感兴趣,我仍然认为了解它们的工作原理很有趣,因为它们是 Elm 架构在底层的实现方式。

本机/内核代码

我们将从原生代码(或内核代码)开始。这是 Elm 用来从 Elm 访问 JavaScript 代码的术语。

我能找到的最简单的例子是包Debug.log中的函数elm/core

Debug.log : String -> a -> a

调用Debug.log "My label" { a = 2 }将会打印"My label: { a = 2 }"到控制台,并返回与第二个参数相同的值。
如果我们看一下源代码,就会发现它的实现只是委托给了一个名为的函数Elm.Kernel.Debug.log

-- src/Debug.elm
log : String -> a -> a
log =
  Elm.Kernel.Debug.log

如果你浏览代码库,你不会找到Elm/Kernel/Debug.elm模块,而会找到一个 JavaScript 文件。在这个文件中,我们找到了我们想要的函数:

// src/Elm/Kernel/Debug.js
var _Debug_log__DEBUG = F2(function(tag, value)
{
    console.log(tag + ': ' + _Debug_toString(value));
    return value;
});

正如预期的那样,该函数仅记录我们的标签和值(在使用另一个 javascript 函数将其转换为字符串之后)并返回它。

注意:在上面的文件中,您可以看到名称中带有__PROD和后缀的各种函数__DEBUG。Elm 编译器会根据您是否使用优化编译代码来选择您使用的版本:它将使用__PROD优化代码的版本,以及__DEBUG未优化/调试代码的版本。

注意:内核模块必须在目录层次结构下定义/Elm/Kernel/。在 Elm 中,您可以像导入常规模块一样导入它们。
原生模块中的函数必须遵循类似 的命名规范_MyKernelModule_myFunction

module MyElmModule

-- Imports the kernel module:
-- src/Elm/Kernel/MyKernelModule.js
import Elm.Kernel.MyKernelModule


foo =
    Elm.Kernel.MyKernelModule.myFunction

最后,您应该为 Elm 中的核函数提供类型注释,
如果不这样做,编译器将接受您提供的任何类型,并且您容易受到类型错误导致的运行时异常的影响。

内核代码的介绍到此结束。如果您对更详细的内容感兴趣,请阅读附录。

效果管理器

Effect Manager 是一种特殊类型的 Elm 模块,用于实现命令(例如Random.generate)和订阅(例如Browser.Events.onResize)。
在本文中,我们将从头开始实现一个命令,订阅则更为复杂,我们将在下一篇文章中讨论。

我们将实现一个命令(Cmd从现在开始),当该命令(通过常规update函数)分派时,将显示window.alert带有用户选择的文本。

使用它的应用程序如下所示:

module Main exposing (main)

-- Our effect manager
import Alert
import Browser
import Html exposing (Html, button, text)
import Html.Events exposing (onClick)


type Msg
    = SendAlert String


view _ =
    button
        [ onClick (SendAlert "Hi there!") ]
        [ text "Click to get an alert" ]


update msg model =
    case msg of
        SendAlert text ->
            ( model, Alert.alert text )


main =
    Browser.element
        { init = \_ -> ( {}, Cmd.none )
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }

(也可在此处获取)

单击该按钮后,您会收到一条带有文本的警报"Hi there!"

让我们来实现Alert效果管理器。

效果管理器的剖析

与通常的module MyModule exposing (myFunction)模块声明不同,效果管理器的启动方式如下:

effect module Alert where { command = MyCmd } exposing (alert)

效果管理器(定义命令)需要实现一种类型(此处为MyCmd,但您可以随意命名),以及以下功能:

type MyCmd msg

cmdMap : (a -> b) -> MyCmd a -> MyCmd b

init : Task Never state

onEffects : Platform.Router msg event -> List (MyCmd msg) -> state -> Task Never state

onSelfMsg : Platform.Router msg Never -> Never -> state -> Task Never state

让我们通过示例代码来了解每个模块的功能Alert
类型MyCmd描述了我们的模块能够生成和执行哪些类型的命令。

我们只提供一个命令,该命令只需要发送带有用户选择的文本的警报,不需要其他信息:

type MyCmd msg = Alert String

这很容易。

现在,我们来看一下cmdMap。每当用户调用Cmd.map我们的某个命令时,都会使用此函数。由于我们不存储msg或任何与之相关的内容,因此我们的实现非常简单:

cmdMap : (a -> b) -> MyCmd a -> MyCmd b
cmdMap _ (Alert text) = Alert text

现在我们进入更有趣的细节,我们需要实现initonEffectsonSelfMessage

要了解每个组件的用途,我们需要了解以下内容:效果管理器与 Elm 应用程序非常相似,它们具有:

  • A ,通常在此上下文中Model称为State
  • 一个init函数,它确定初始的State,唯一的区别是你被允许使用一个任务(从而执行副作用)
  • 最后,两个 update函数:onEffectsonSelfMessage,我们稍后讨论它们

对于我们的情况,我们实际上不需要维护任何状态,因此我们可以使用一个空记录{},而且我们的init意愿也非常简单。

type alias State =
    {}

init : Task Never State
init =
    Task.succeed {}

最后,onEffects也是onSelfMessage魔法开始发生的地方,我们暂时忽略它的第一个参数,Platform.Router每当应用程序分派一个(或多个,通过 之类的方式)我们的命令时,
我们的就会被调用,我们会在第二个参数中接收它。我们的第三个参数将接收当前的。在这个函数中,我们应该处理这些命令,并在需要时更新我们的状态:onEffectsCmd.batchState

-- src/Alert.elm
onEffects : Platform.Router msg event -> List (MyCmd msg) -> State -> Task Never State
onEffects router commands state =
    case commands of
        [] ->
            Task.succeed state

        (Alert text) :: rest ->
            let
                _ =
                    Elm.Kernel.Alert.alert text
            in
            onEffects router rest state

换句话说,如果我们没有任何命令需要处理,就返回当前状态(因为签名强制要求,所以将其封装在一个任务中)。否则,处理第一个命令(在本例中,我们通过调用内核函数来实现),并递归处理其余命令。

如果你感到好奇,我们的内核代码其实非常简单(__Utils_Tuple0只是一种返回 Elm 空元组的方法()):

// src/Elm/Kernel/Alert.js
function _Alert_alert (text) {
  window.alert(text)

  return __Utils_Tuple0
}

对于我们的例子来说,基本上就是这样!可惜的是,无论如何我们都必须实现它onSelfMsg。在此之前,我们先了解一下它的第一个参数onEffect是什么。

我确实说过,效果管理器就像普通的 Elm 应用程序一样,虽然我们的命令有点像Msgs,但我们无法控制它们何时发送,而用户可以控制。事实证明,在这些模块中也拥有自主控制的消息机制非常有用,所以我们就这么做了!

从上面的类型签名来看,events 就是我们的msgs。路由器允许我们通过 向自己发送消息Platform.sendToSelf。这些事件将被路由到我们的onSelfMsg函数,我们可以用它们来更新状态。此外,有些命令会将消息返回给应用程序,我们可以使用 来实现Platform.sendToApp

回到我们的例子,考虑到我们实际上不需要任何自发消息,我们可以将事件类型设置为并以唯一可能的方式Never实现:onSelfMsg

type alias Event =
    Never

onSelfMsg : Platform.Router msg Event -> Event -> State -> Task Never State
onSelfMsg _ _ state =
    Task.succeed state

现在,我们已经实现了编译器为 Effect Manager 所需的所有部分,但是我们还缺少开始实现模块的原因,即alert命令!

我们的第一个尝试是执行以下操作:

alert : String -> Cmd msg
alert text =
    Alert text

但这会报类型错误,Alert text是 is a MyCmd msg,而不是 a Cmd msg
现在,还记得where { command = MyCmd }模块声明中那个奇怪的部分吗?它给了我们一个神奇的command函数,让我们可以将MyCmds 转换为正确的、应用程序可用的Cmds 。

alert : String -> Cmd msg
alert text =
    command (Alert text)

我们终于完成了!您可以在这里
查看(但不能编译)此效果管理器的完整代码

注意:在这里,我们使用了名称MyCmd、、、MySub来表示我们定义的类型,但实际上您可以为每一个类型选择任何您想要的名称,这只是效果管理器中通常使用的State约定Eventelm/core

附录

与 JavaScript 交互的细微差别

虽然本指南更多的是作为阅读本机代码的资源,但如果您有兴趣编写一些代码,我可以提供一些指点。

在常规 Elm 中,所有函数都是柯里化的,因此,具有多个参数的函数实际上每次只接受一个参数,并为每个参数返回一个函数。JavaScript 中并非如此。

为了克服这种不匹配,Elm 在 JavaScript 端使用了一些辅助函数,回到我们的Debug.log示例,在JS 源代码中您将看到以下内容:

var _Debug_log__DEBUG = F2(function(tag, value)
{
    console.log(tag + ': ' + _Debug_toString(value));
    return value;
});

F2函数实际上是对我们传递给它的双参数函数进行柯里化,以便使其能够与 Elm 处理函数调用的方式相匹配。在源代码中,您可以找到一些函数,F2用于F9将带有 2 到 9 个参数的 JavaScript 函数转换为柯里化版本。

A2相反,通过调用一系列函数A9可以更轻松地从 JS 调用 Elm 函数:

这意味着:

// JavaScript
myElmFunction(a)(b)(c)(d)

我们可以使用我们的A4助手来更自然地调用这个函数:

// JavaScript
A4(myElmFunction, a, b, c, d)

这些函数还执行了一些优化技巧,以减少函数调用/分配的次数。如果您对此感兴趣,可以阅读源代码中它们的具体功能,网址为

内核代码的捆绑

一些内核(JavaScript)代码将需要使用来自其他模块的函数,为此,Elm 使用相同的import Module exposing (value)语法,以“魔术”JS 注释的形式。

您可以在包的Elm.Kernel.Char模块中看到这样的示例elm/core

/*

import Elm.Kernel.Utils exposing (chr)

*/

在同一个文件中,该chr函数将可以作为 访问__Utils_chr

文章来源:https://dev.to/jjant/a-guide-to-native-code-and-effect-managers-in-elm-part-1-commands-1k6n
PREV
像自由职业者一样自由,还是像正式员工一样有保障?还有第三种选择:承包商 获取免费的 Dev Contractor 路线图
NEXT
函数式编程、面向对象编程、过程式编程