
在哈布雷(Habré)上,您可以找到许多出版物,这些出版物揭示了Monad的理论及其应用实践。 这些文章大多数都是关于Haskell的。 我不会第n次重述该理论。 今天,我们将讨论一些Erlang问题,使用monad解决问题的方法,部分使用功能以及来自erlando的语法糖(来自RabbitMQ团队的一个很棒的库)。
引言
Erlang具有不变性,但没有monads * 。 但是由于语言中的parse_transform功能和erlando实现,仍然有可能在Erlang中使用monad。
关于故事一开始的豁免权,我并不是偶然地讲的。 免疫几乎无处不在,而且永远都是-Erlang的主要思想之一。 功能的免疫力和纯度使您可以专注于特定功能的开发,而不必担心副作用。 但是,例如来自Java或Python的Erlang新手发现,很难理解和接受Erlang的想法。 特别是当您回想起Erlang语法时。 那些尝试开始使用Erlang的人可能会注意到它的不寻常和独立性。 无论如何,我已经积累了很多来自初学者的反馈,“奇怪”的语法在评分中居于领先地位。
厄兰多
Erlando是一个Erlang扩展集,可为我们提供:
- 部分使用/带有类似方案的削减功能
- 类似于Haskell的符号
- import-as-从其他模块导入功能的语法糖。
注意:我以下面的代码示例为例,从Matthew Sackman的演讲中说明了erlando的功能,并通过我的代码和说明对它们进行了部分稀释。
抽象切
直奔重点。 考虑实际项目中的几个功能:
info_all(VHostPath, Items) -> map(VHostPath, fun (Q) -> info(Q, Items) end). backing_queue_timeout(State = #q{ backing_queue = BQ }) -> run_backing_queue( BQ, fun (M, BQS) -> M:timeout(BQS) end, State). reset_msg_expiry_fun(TTL) -> fun (MsgProps) -> MsgProps #message_properties{ expiry = calculate_msg_expiry(TTL)} end.
所有这些功能旨在将参数替换为简单表达式。 实际上,这是部分应用程序,因为在调用之前某些参数将是未知的。 这些功能加上灵活性,给我们的代码增加了噪音。 通过稍微更改语法-输入cut-可以改善情况。
值_
- _可以在模板中使用
- 剪切使您可以使用_外部模式
- 如果它在模板之外,它将成为其所在表达式的参数
- 在同一表达式中多次使用_会导致此表达式中多个参数的替换
- Cut不能替代瓶盖(熔炉)
- 在cut函数之前先评估参数。
Cut在表达式中使用_指示应在何处应用抽象。 Cut仅在表达式中包装最接近的级别,但不禁止嵌套cut。
例如list_to_binary([1, 2, math:pow(2, _)]).
list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]).
到list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]).
但不是fun (X) -> list_to_binary([1, 2, math:pow(2, X)]) end.
。
听起来有些混乱,让我们使用cut重写上面的示例:
info_all(VHostPath, Items) -> map(VHostPath, fun (Q) -> info(Q, Items) end). info_all(VHostPath, Items) -> map(VHostPath, info(_, Items)).
backing_queue_timeout(State = #q{ backing_queue = BQ }) -> run_backing_queue( BQ, fun (M, BQS) -> M:timeout(BQS) end, State). backing_queue_timeout(State = #q{backing_queue = BQ}) -> run_backing_queue(BQ, _:timeout(_), State).
reset_msg_expiry_fun(TTL) -> fun (MsgProps) -> MsgProps #message_properties { expiry = calculate_msg_expiry(TTL) } end. reset_msg_expiry_fun(TTL) -> _ #message_properties { expiry = calculate_msg_expiry(TTL) }.
参数计算顺序
为了说明参数的计算顺序,请考虑以下示例:
f1(_, _) -> io:format("in f1~n"). test() -> F = f1(io:format("test line 1~n"), _), F(io:format("test line 2~n")).
由于参数是在cut函数之前求值的,因此将显示以下内容:
test line 2 test line 1 in f1
减少各种类型和代码模式的抽象
- 元组
F = {_, 3}, {a, 3} = F(a).
- 清单
dbl_cons(List) -> [_, _ | List]. test() -> F = dbl_cons([33]), [7, 8, 33] = F(7, 8).
- 记录
-record(vector, { x, y, z }). test() -> GetZ = _#vector.z, 7 = GetZ(#vector { z = 7 }), SetX = _#vector{x = _}, V = #vector{ x = 5, y = 4 } = SetX(#vector{ y = 4 }, 5).
- 案例
F = case _ of N when is_integer(N) -> N + N; N -> N end, 10 = F(5), ok = F(ok).
- 地图
test() -> GetZ = maps:get(z, _), 7 = GetZ(#{ z => 7 }), SetX = _#{x => _}, V = #{ x := 5, y := 4 } = SetX(#{ y => 4 }, 5).
- 匹配列表和构造二进制数据
test_cut_comprehensions() -> F = << <<(1 + (X*2))>> || _ <- _, X <- _ >>,
优点
- 代码变得更小,因此更易于维护。
- 代码变得更加简单和整洁。
- 有趣的声音消失了。
- 对于Erlang的初学者来说,编写Get / Set函数更加方便。
缺点
- 提高了经验丰富的Erlang开发人员的入门门槛,同时降低了初学者的入门门槛。 现在,团队需要了解cut并了解另一种语法。
做记号
软逗号是一种计算绑定构造。 Erlang没有惰性计算模型。 让我们想象一下,如果Erlang像Haskell一样懒惰会发生什么
my_function() -> A = foo(), B = bar(A, dog), ok.
为了保证执行顺序,我们需要通过定义逗号来显式链接计算。
my_function() -> A = foo(), comma(), B = bar(A, dog), comma(), ok.
继续转换:
my_function() -> comma(foo(), fun (A) -> comma(bar(A, dog), fun (B) -> ok end)).
根据结论,逗号/ 2是惯用函数>>=/2
。 monad仅需要三个函数: >>=/2
, return/1
和fail/1
。
一切都会好的,但是语法很糟糕。 我们使用erlando
语法转换erlando
。
do([Monad || A <- foo(), B <- bar(A, dog), ok]).
单声道的类型
由于do-block是参数化的,因此我们可以使用各种类型的monad。 在do块内部,将调用return/1
和fail/1
部署到Monad:return/1
和Monad:fail/1
。
身份单子。
相同的monad是最简单的monad,它不更改值的类型并且不参与计算过程的控制。 它与变压器一起使用。 执行链接表达式-上面讨论的软件逗号。
也许是单子。
计算缺失值的单子计算。 将参数与参数化的计算相关联是将参数转移到计算,将不存在的参数链接到参数化的计算则是缺少结果。
考虑一个maybe_m的例子:
if_safe_div_zero(X, Y, Fun) -> do([maybe_m || Result <- case Y == 0 of true -> fail("Cannot divide by zero"); false -> return(X / Y) end, return(Fun(Result))]).
如果未返回任何内容,则表达式的求值停止。
{just, 6} = if_safe_div_zero(10, 5, _+4) ## 10/5 = 2 -> 2+4 -> 6 nothing = if_safe_div_zero(10, 0, _+4)
错误单子。
与maybe_m类似,仅具有错误处理。 有时,“崩溃让它”原理不适用,并且必须在错误发生时进行处理。 在这种情况下,案例中的阶梯通常会出现在代码中,例如:
write_file(Path, Data, Modes) -> Modes1 = [binary, write | (Modes -- [binary, write])], case make_binary(Data) of Bin when is_binary(Bin) -> case file:open(Path, Modes1) of {ok, Hdl} -> case file:write(Hdl, Bin) of ok -> case file:sync(Hdl) of ok -> file:close(Hdl); {error, _} = E -> file:close(Hdl), E end; {error, _} = E -> file:close(Hdl), E end; {error, _} = E -> E end; {error, _} = E -> E end.
make_binary(Bin) when is_binary(Bin) -> Bin; make_binary(List) -> try iolist_to_binary(List) catch error:Reason -> {error, Reason} end.
读起来很不愉快,看起来像JS中的回调面。 Error_m可以解决:
write_file(Path, Data, Modes) -> Modes1 = [binary, write | (Modes -- [binary, write])], do([error_m || Bin <- make_binary(Data), Hdl <- file:open(Path, Modes1), Result <- return(do([error_m || file:write(Hdl, Bin), file:sync(Hdl)])), file:close(Hdl), Result]). make_binary(Bin) when is_binary(Bin) -> error_m:return(Bin); make_binary(List) -> try error_m:return(iolist_to_binary(List)) catch error:Reason -> error_m:fail(Reason) end.
- List-monad。
值是可以解释为一次计算的几种可能结果的列表。 如果一个计算依赖于另一个,则对第一个结果的每个结果执行第二个计算,并将结果(第二个计算)收集在一个列表中。
考虑带有经典毕达哥拉斯三元组的示例。 我们计算它们时不包含monad:
P = [{X, Y, Z} || Z <- lists:seq(1,20), X <- lists:seq(1,Z), Y <- lists:seq(X,Z), math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2)].
与list_m相同:
P = do([list_m || Z <- lists:seq(1,20), X <- lists:seq(1,Z), Y <- lists:seq(X,Z), monad_plus:guard(list_m, math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2)), return({X,Y,Z})]).
- 状态单子。
有状态计算的Monad。
在本文的开头,我们讨论了初学者在使用可变状态时遇到的困难。 该代码通常看起来像这样:
State1 = init(Dimensions), State2 = plant_seeds(SeedCount, State1), {DidFlood, State3} = pour_on_water(WaterVolume, State2), State4 = apply_sunlight(Time, State3), {DidFlood2, State5} = pour_on_water(WaterVolume, State4), {Crop, State6} = harvest(State5), ...
使用转换符和剪切符号,可以更紧凑和易读的形式重写此代码:
StateT = state_t:new(identity_m), SM = StateT:modify(_), SMR = StateT:modify_and_return(_), StateT:exec( do([StateT || StateT:put(init(Dimensions)), SM(plant_seeds(SeedCount, _)), DidFlood <- SMR(pour_on_water(WaterVolume, _)), SM(apply_sunlight(Time, _)), DidFlood2 <- SMR(pour_on_water(WaterVolume, _)), Crop <- SMR(harvest(_)), ... ]), undefined).
- 欧米茄单子。
类似于list_m monad。 但是,通道是对角线的。
隐藏的错误处理
可能是我最喜欢的error_m
monad功能之一。 无论错误发生在何处,monad始终会返回{ok, Result}
或{error, Reason}
。 一个示例说明此行为:
do([error_m || Hdl <- file:open(Path, Modes), Data <- file:read(Hdl, BytesToRead), file:write(Hdl, DataToWrite), file:sync(Hdl), file:close(Hdl), file:rename(Path, Path2), file:delete(Path), return(Data)]).
Import_as
对于小吃,我们有语法import_as sugar。 -import / 2属性的标准语法使您可以将其他函数导入本地模块。 但是,此语法不允许您为导入的函数分配替代名称。 Import_as解决了此问题:
-import_as({my_mod, [{size/1, m_size}]}) -import_as({my_other_mod, [{size/1, o_size}]})
这些表达式分别扩展为实际的局部函数:
m_size(A) -> my_mod:size(A). o_size(A) -> my_other_mod:size(A).
结论
当然,通过monad,您可以通过更具表现力的方法来控制计算过程,节省代码和时间来支持它。 另一方面,它们为未经培训的团队成员增加了额外的复杂性。
* -实际上,在埃尔朗(Erlang)中,单子存在而没有erlando。 逗号分隔表达式是线性化和链接计算的构造。
PS最近,作者将erlando图书馆标记为档案。 我一年多以前写了这篇文章。 但是,与现在一样,那时在哈布雷(Habré)还没有关于埃尔朗(Erlang)单子的信息。 为了纠正这种情况,尽管有些迟,我还是发表了这篇文章。
要在erlang> = 22中使用erlando,您需要使用已弃用的erlang来解决该问题:get_stacktrace / 0。 可以在我的fork中找到一个修复示例: https : //github.com/Vonmo/erlando/commit/52e23ecedd2b8c13707a11c7f0f14496b5a191c2
感谢您的宝贵时间!