Elm 原生代码和效果管理器指南 - 第一部分:命令
本机/内核代码
效果管理器
附录
本指南旨在帮助您了解 Elm 如何实现其核心库,以及如何编写自己的库。
本文要求您具备 Elm 和 JavaScript 的编写经验。
Elm 使用以下资源组合来实现核心库(属于elm
和elm-explorations
组织,例如elm/core
和elm-explorations/test
):
- 常规 Elm 代码
- 本机/内核代码
- 效果管理器
- 编译器魔法
我们这里就不讨论最后一个了。
需要注意的是,有些包只使用了其中的几个,例如elm/project-metadata-utils
纯 Elm 实现的。
注意:原生代码和效果管理器只能由
elm
和elm-explorations
1组织编译,因此普通用户无法使用。
如果您仍想使用这些功能,可以使用编译器的分支版本,但我不会在这里讨论这个问题。
即使您对构建自己的本机模块不感兴趣,我仍然认为了解它们的工作原理很有趣,因为它们是 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
现在我们进入更有趣的细节,我们需要实现init
、onEffects
和onSelfMessage
。
要了解每个组件的用途,我们需要了解以下内容:效果管理器与 Elm 应用程序非常相似,它们具有:
- A ,通常在此上下文中
Model
称为State
- 一个
init
函数,它确定初始的State
,唯一的区别是你被允许使用一个任务(从而执行副作用) - 最后,两个
update
函数:onEffects
和onSelfMessage
,我们稍后讨论它们
对于我们的情况,我们实际上不需要维护任何状态,因此我们可以使用一个空记录{}
,而且我们的init
意愿也非常简单。
type alias State =
{}
init : Task Never State
init =
Task.succeed {}
最后,onEffects
也是onSelfMessage
魔法开始发生的地方,我们暂时忽略它的第一个参数,Platform.Router
。每当应用程序分派一个(或多个,通过 之类的方式)我们的命令时,
我们的就会被调用,我们会在第二个参数中接收它。我们的第三个参数将接收当前的。在这个函数中,我们应该处理这些命令,并在需要时更新我们的状态:onEffects
Cmd.batch
State
-- 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 应用程序一样,虽然我们的命令有点像Msg
s,但我们无法控制它们何时发送,而用户可以控制。事实证明,在这些模块中也拥有自主控制的消息机制非常有用,所以我们就这么做了!
从上面的类型签名来看,event
s 就是我们的msg
s。路由器允许我们通过 向自己发送消息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
函数,让我们可以将MyCmd
s 转换为正确的、应用程序可用的Cmd
s 。
alert : String -> Cmd msg
alert text =
command (Alert text)
我们终于完成了!您可以在这里
查看(但不能编译)此效果管理器的完整代码。
注意:在这里,我们使用了名称
MyCmd
、、、MySub
等来表示我们定义的类型,但实际上您可以为每一个类型选择任何您想要的名称,这只是效果管理器中通常使用的State
约定。Event
elm/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
。