Elm 编程语言
Elm 是一种用于函数式编程 的语言,用于声明式地创建基于网络浏览器 的图形用户界面。
Elm 使用函数式响应式编程 样式和纯粹函数式 图形布局来构建用户界面,而无需任何破坏性更新。
Elm 的主要实现编译成 JavaScript 和 HTML,并使用 CSS 样式。
在 Elm 中,函数式响应式编程取代了事件处理程序和回调;它还自动管理所有屏幕更新。纯粹函数式图形布局取代了与 DOM 的交互。Elm 还允许直接嵌入Markdown。
Elm 采用Haskell 风格的语法,并受OCaml 和FSharp 的影响。例如,“具有类型”用单个冒号 (:)
编写,而前向和后向函数应用使用 (<|)
和 (|>)
运算符[1],其中 (f x)
等于 (f <| x)
等于 (x |> f)
。
Elm 具有可扩展的记录[2],它们安全地提供了Javascript 对象模型的大部分灵活性。
类型系统支持基本类型,如整数和浮点数,结构化数据,如元组和记录,以及自定义ADT。[3]
Elm 的函数式响应式编程 版本是事件驱动的,这意味着更新仅在必要时执行。它与事件驱动 FRP[4][5] 和 Arrowized FRP 最相似。[6][7]
以下程序显示鼠标在屏幕上移动时的位置,并实时自动更新屏幕。注意:Elm 的 import 对应于 Haskell 的 import qualified。[8]
import Graphics.Element exposing (..)
import Mouse -- qualified import
main = Signal.map show Mouse.position
main 的值显示在屏幕上。函数 Graphics.Element.show 将任何值转换为可显示的文本表示形式。Mouse.position 是一个随时间变化的值,称为信号。函数 Signal.map 确保每次鼠标移动时都将 show 应用于 Mouse.position。
Elm 有一组很小但表达能力强的语言结构,包括 if 表达式、let 表达式、case 表达式、匿名函数和列表插值。[1][9]
Elm 还具有模块系统和 JavaScript 的外部函数接口。[10]
预定义函数位于 Prelude 模块中。[11]
值是不可变的
- Bool、Int、Float。[11]
- Char,[12] String,[11] (样式化的) Text。[13]
- Time、Date。[14]
- Element(html 结构元素)。[15]
- 图形表示(称为 Forms)、形状和路径。[16]
toForm : Element -> Form>> toForm 将任何 Element 转换为 Form。这使您可以在拼贴画中使用文本、gif 和视频。这意味着您可以根据需要移动、旋转和缩放 Element。
- 记录值。
- 代数数据类型(data 子句)值。
信号是变化的值项,并且具有 (Signal value_type)
类型的。
它们包括随时间变化的项目(也称为行为)和事件驱动的源。
-- current time, updated every t (from Time lib)
every : Time -> Signal Time
-- current mouse position (from Mouse lib)
position : Signal (Int,Int)
Html 表单输入元素,可能在样式和状态上有所不同。它们的生成器函数大多返回一对 (元素信号,状态信号)[17],如
-- create a checkbox with a given start state.
checkbox : Bool -> (Signal Element, Signal Bool)
依赖信号就像电子表格中的公式 单元格。它们可能会对操作数信号的更新做出反应。
定义为 main 的信号开始扫描依赖关系/反应有向图 以找到要包含在源事件循环中的独立信号变量。
您可以使用信号过滤器函数或通过将提升的值函数应用于先前定义的信号来定义信号 公式。要应用 N 个参数的函数,您必须将 值 函数的类型提升为 信号 函数,方法是通过 lift<N> 函数或将 lift 应用于函数,然后应用信号应用项,如 ((~) signal),用于下面公开的函数参数应用。[18]
您可以通过使用 Signal.constant 函数提升其类型来在信号位置使用值。
查看 Functor 类定义。[19] 来自 Elm 的 Signal 库:[18]
-- lift a ''values'' function type to a ''signals'' function one
-- similar to Haskell's ''fmap'' and ''Control.Applicative.liftA''
lift : (a -> b) -> Signal a -> Signal b
-- (<~) is an alias for lift
查看 Applicative 类定义。[20] 来自 Elm 的 Signal 库:[18]
-- lift a Value
constant : a -> Signal a
-- signal application
(~) : Signal (a -> b) -> Signal a -> Signal b
-- sample from the second input every time an event occurs on the first input
-- like Haskell's Control.Applicative.(*>) sequence actions discarding the first result
sampleOn : Signal a -> Signal b -> Signal b
----
-- lift<N>: to lift a function of N value parameters (defined lift2 to lift8)
-- similar to Haskell's Control.Applicative.liftA<N>
result_signal = liftN fun_N_ary signal1 signal2 ... signalN
-- equivalent with binary infix operators
result_signal = fun_N_ary <~ signal1 ~ signal2 ~ ... ~ signalN
这用于生成信号转换器作为链。
具有类型 (Automaton input output)
的单个链节就像计算(副作用)函数一样,只有一个参数。
要将它们链接在一起,后者的输入类型必须与前者的输出结果类型匹配。
这个概念借鉴了 Haskell 的箭头 (效果 通过链接 态射 进行排序)。[7][21]
你可以使用 Automaton.run 库函数将它们应用于 Signal,指定 Automaton 和在没有输入值的情况下使用的默认结果。
来自 Elm 的 Automaton 库:[22]
-- generator from a pure mapping function
pure : (a -> b) -> Automaton a b
-- generators from an initial state and a function of input and state
state : b -> (a -> b -> b) -> Automaton a b
hiddenState : s -> (a -> s -> (s,b)) -> Automaton a b
-- automaton application
run : Automaton a b -> b -> Signal a -> Signal b
result_signal = run myAutomaton result_default input_signal
-- compose two automatons, chaining them together.
(>>>) : Automaton a b -> Automaton b c -> Automaton a c
参见参考文献 [23]
- List,Set,Dict
- Maybe(用于可选参数和部分定义的函数结果,如 Just v 或 Nothing)
- Either(错误感知结果,如 Right correct_result 或 Left error)
将信号列表作为一个整体进行处理
-- combine a list of signals into a signal of their value list
Signal.combine : [Signal a] -> Signal [a]
mkdir elm-compiler && cd elm-compiler
cabal-dev install elm elm-server
export PATH=$PWD/cabal-dev/bin:$PATH
导入的模块成员应使用限定词,除非在 import module (members...) 中列出,或者使用 import open 进行命名空间包含。 [8]
import Text exposing (Text)
import Color exposing (..)
import Graphics.Element as Elem exposing (Element) -- qualified import
unstyledText : Text
unstyledText = Text.fromString "test1"
styleIt : Text -> Text
styleIt = (Text.typeface ["serif"]) >> (Text.color red)
-- (f <| x = f x), used to avoid parentheses
alignTest : Int -> Element
alignTest commonWidth =
let elem1 = Elem.width commonWidth <| Elem.justified <| styleIt unstyledText
elem2 = Elem.width commonWidth <| Elem.centered <| styleIt <| Text.fromString "test2"
elem3 = Elem.width commonWidth <| Elem.rightAligned <| styleIt <| Text.fromString "test3"
in Elem.flow Elem.down [elem1, elem2, elem3]
main : Element
main = alignTest 200
你可以在 在线编辑器/编译器/执行器 中尝试它。
参见记录。 [24]
module MyModule where
import Color
type Named a = { a | name : String } -- records with a ''name'' field
getName : Named a -> String
getName {name} = name
dude = {name="John Doe", age=20}
lady = {name="Jane Doe", eyesColor = Color.blue}
names : [String]
names = [ getName dude, getName lady]
fullData = [show dude, show lady]
staticElement : Element
staticElement = flow down <| map plainText
["Names: " ++ show names
, show fullData
]
main = staticElement
- 将它嵌入 div 元素中
<!DOCTYPE HTML>
<!-- MyModule.html -->
<html>
<head><meta charset="UTF-8">
<title>MyModule</title>
<!-- elm-runtime.js and js compiled modules -->
<script type="text/javascript"
src="/your-install-directory/cabal-dev/share/Elm-N.N.N.N/elm-runtime.js"></script>
<script type="text/javascript" src="MyModule.js"></script>
</head>
<body>
<div id="myId" style="width:100;height:100;"></div><!-- Elm container must be a "div" element and must be empty -->
<script type="text/javascript">
var myContainer = document.getElementById('myId') ;
Elm.embed(Elm.MyModule, myContainer) ;
</script>
<noscript>Javascript is not enabled</noscript>
</body></html>
- 离线编译和测试
# compile to Javascript
$ elm --make -s --only-js MyModule.elm
# run
$ browser MyModule.html
port FFI(与 JavaScript 的外部函数接口)功能 [25] 提供了在 html 级别提供参数的机会。
module ElmMain where
port arg1 : String
port arg2 : Int
port arg3 : [String]
implode : [String] -> String
implode = concat . intersperse ", "
main = asText <| implode [arg1, show arg2, implode arg3]
<!DOCTYPE HTML>
<html>
<head><meta charset="UTF-8">
<title>Title</title>
<!-- elm-runtime.js and js compiled modules
(if compiled with --make the ElmMain.js contains also the possibly imported user modules) -->
<script type="text/javascript"
src="/your-install-directory/cabal-dev/share/Elm-N.N.N.N/elm-runtime.js"></script>
<script type="text/javascript" src="ElmMain.js"></script>
</head>
<body>
<div id="myId" style="width:100;height:100;"></div><!-- Elm container must be a "div" element and must be empty -->
<script type="text/javascript">
var myPorts = {arg1: "do re mi", // after "The Jackson five" "abc" lyrics
arg2: 123,
arg3: ["abc", "you and me"]
} ;
var myContainer = document.getElementById('myId') ;
Elm.embed(Elm.ElmMain, myContainer, myPorts) ;
</script>
<noscript>Javascript is not enabled</noscript>
</body></html>
- lift:类似于 Haskell 的 fmap 用于信号
lift : (a -> b) -> Signal a -> Signal b
- lift<N> : 将 N 个值的函数应用于 N 个信号;它就像 Haskell 的 Applicative liftA<N>。 [26]
- (<~) 和 (~) 是 Elm 替换 Haskell 中的 infix Applicative 操作符 (<$>) 和 (<*>)。 [27]
使用 Graphics.Collage 库。 [16]
myShape1 : Shape
myShape1 = circle 30
myShape2 = rect 60 60
myForm1 : Form
myForm1 = outlined (dashed green) myShape1
myForm2 = outlined (solid red) myShape2
forms : [Form]
forms = [myForm1 |> move (10, -10)
, myForm2 |> move (30, -30)
|> rotate (degrees 45)
|> scale 1.5
, plainText "mytext"
|> toForm
|> move (20, -20)
|> rotate (degrees 30)
|> scale 2
]
mapRotate : Float -> [Form] -> [Form]
mapRotate t = let f = rotate <| degrees <| t * 10
in map f
-- let's define the left-to-right function composition operator
f >> g = g . f
-- time signal in truncated seconds
tictac : Signal Float
tictac = let f = inSeconds >> truncate >> toFloat
in every (2 * second)
|> lift f
main : Signal Element
main = constant forms
|> lift2 mapRotate tictac
|> lift3 collage (constant 200) (constant 200)
-- equivalent with (<~) and (~) infix operators
main = let signal1 = mapRotate <~ tictac ~ constant forms
in collage <~ constant 200 ~ constant 200 ~ signal1
-- equivalent using ''constant'' to lift function types
main = let signal1 = constant mapRotate ~ tictac ~ constant forms
in constant collage ~ constant 200 ~ constant 200 ~ signal1
带颜色变化的提交按钮。
import Graphics.Input as Input
import Graphics.Element as Elem
-- button color modifier (pointfree)
passwdOkColour : String -> String -> Element -> Element
passwdOkColour passwd1 passwd2 =
if passwd1 == passwd2 && length passwd1 >= 6
then Elem.color green
else Elem.color red
display field1Elem field2Elem submitButtonElem =
field1Elem `above` field2Elem `above` submitButtonElem
dynamicElement : Signal Element
dynamicElement =
let (field1ElemSignal, fld1StateSignal) = Input.password "Type password (min. 6 characters)!"
labeledField1Signal = let prependLabel = beside (plainText "passwd: ")
in lift prependLabel field1ElemSignal
(field2ElemSignal, fld2StateSignal) = Input.password "Retype password!"
labeledField2Signal = let prependLabel = beside (plainText "control: ")
in lift prependLabel field2ElemSignal
(submitButton, pressedSignal) = Input.button "Submit"
coloredButSignal = constant submitButton
|> lift3 passwdOkColour fld1StateSignal fld2StateSignal
in lift3 display labeledField1Signal labeledField2Signal coloredButSignal
main = dynamicElement
注意,从 Elm 0.10 开始,字符串不再是字符列表,因此上面的代码变成了类似这样的代码
import Graphics.Input as Input
import Graphics.Element as Elem
import String as S
-- button color modifier (pointfree)
passwdOkColour : String -> String -> Element -> Element
passwdOkColour passwd1 passwd2 =
if S.length passwd1 >= 6 && passwd1 == passwd2
then Elem.color green
else Elem.color red
display field1Elem field2Elem submitButtonElem =
field1Elem `above` field2Elem `above` submitButtonElem
prependLabel = beside . plainText
dynamicElement : Signal Element
dynamicElement =
let (field1ElemSignal, fld1StateSignal) = Input.password "Type password (min. 6 characters)!"
labeledField1Signal = lift (prependLabel "passwd: ") field1ElemSignal
(field2ElemSignal, fld2StateSignal) = Input.password "Retype password!"
labeledField2Signal = lift (prependLabel "control: ") field2ElemSignal
(submitButton, _) = Input.button "Submit"
coloredButSignal = constant submitButton
|> lift3 passwdOkColour fld1StateSignal fld2StateSignal
in lift3 display labeledField1Signal labeledField2Signal coloredButSignal
main = dynamicElement
- ↑ a b Elm 的语法
- ↑ Elm - 可扩展记录
- ↑ Elm - 类型入门
- ↑ Wan,Zhanyong;Taha,Walid;Hudak,Paul (2002)。“事件驱动的 FRP”。实用声明性语言第四届国际研讨会论文集:155–172。
- ↑ Elm - 什么是函数式反应式编程?
- ↑ 注意:此模板({{cite doi}}) 已被弃用。要引用由 doi:10.1145/581690.581695 标识的出版物,请使用 {{cite journal}} 和
|doi=10.1145/581690.581695
代替。 - ↑ a b Elm - 您需要的库 Automaton 作为箭头实现
- ↑ a b 0.8 版本发布说明 “导入模块”部分
- ↑ 关于 Elm Elm 特性
- ↑ Elm - JavaScript 集成
- ↑ a b c d Elm 的 Prelude
- ↑ Char 库
- ↑ Text 库
- ↑ Date 库
- ↑ Element 库
- ↑ a b Collage 库
- ↑ Graphics.Input 库
- ↑ a b c Signal 库
- ↑ Haskell 类 Functor 定义
- ↑ Haskell 类 Applicative 定义
- ↑ Haskell 箭头介绍
- ↑ Automaton 库
- ↑ 文档
- ↑ 语法 - 记录
- ↑ JavaScript FFI
- ↑ Haskellwiki - Applicative functor
- ↑ Elm v.0.7 版本发布说明 部分:“Do you even lift?”