在 Erlang 中进行模式匹配映射时,为啥这个变量是未绑定的?

Posted

技术标签:

【中文标题】在 Erlang 中进行模式匹配映射时,为啥这个变量是未绑定的?【英文标题】:When pattern matching maps in Erlang, why is this variable unbound?在 Erlang 中进行模式匹配映射时,为什么这个变量是未绑定的? 【发布时间】:2018-03-04 18:12:03 【问题描述】:
-module(count).
-export([count/1]).

count(L) when is_list(L) ->
  do_count(L, #);
count(_) ->
  error(badarg).

do_count([], Acc) -> Acc;
do_count([H|T], #) -> do_count(T, # H => 1 );
do_count([H|T], Acc = # H := C ) -> do_count(T, Acc# H := C + 1);
do_count([H|T], Acc) -> do_count(T, Acc# H => 1 ).

在此示例中,映射键“H”存在并具有与之关联的计数的第三个子句将无法编译。编译器抱怨:

count.erl:11: variable 'H' is unbound    

为什么 H 未绑定?

顺便说一句,这是可行的:

do_count([], Acc) -> Acc;
do_count([H|T], Acc) -> do_count(T, maps:update_with(H, fun(C) -> C + 1 end, 1, Acc)).

但模式匹配似乎应该起作用,但它不起作用。

【问题讨论】:

这经常出现。如果您对此还有其他疑问,请通过Erlang/OTP chat room 提问,以便我更新我的答案。在 Richard 的出色回答和我自己的更具说明性/探索性的回答之间,我想在以后出现此问题时(尤其是在 ML 和 IRC 上)从这个问题页面创建一个规范的参考。 这并不是您“为什么 H 未绑定?”的问题的真正答案。但这是我的解决方法:```do_count(L) -> do_count(L, #)。 do_count([H|T], Acc) -> do_count(T, Acc# H => maps:get(H, Acc, 0) + 1); do_count([], Acc) -> Acc。 ``` 【参考方案1】:

答案与我最近在这里给出的答案几乎相同: https://***.com/a/46268109/240949.

当您在一个模式中多次使用同一个变量时,如本例中的 H:

do_count([H|T], Acc = # H := C ) -> ...

Erlang 中模式匹配的语义说这就像你写的一样

do_count([H|T], Acc = # H1 := C ) when H1 =:= H -> ...

也就是说,它们首先分别绑定,然后比较是否相等。但是需要知道映射模式中的一个键 - 它不能是像 H1 这样的变量,因此会出现错误(正如我链接到的答案中的二进制模式中的字段大小说明符一样)。

这个问题的主要区别是你有一个带有两个单独参数的函数头,你可能认为模式 [H|T] 应该首先匹配,在尝试第二个模式之前绑定 H,但是有没有这样的订购保证;就好像您使用了带有元组模式 [H|T], # H := C 的单个参数。

【讨论】:

啊,是的。您昨天给出的答案与我在(更冗长的)答案中链接的答案相同。我需要为这个特殊的解释添加书签,因为它涉及到很多关于 f(A, #A := _) 等同于统一到 f(A, #B := _) when B =:= A 的内容。对于新手来说,额外令人费解的部分是这确实case 中工作。如果我停下来思考,这也会让我感到困惑,我不倾向于这样做,因为我只是习惯那样。那,我不会到处使用地图。 ;-)【参考方案2】:

因为这种匹配发生在统一的上下文之外。事实上,虽然它没有在文档中明确禁止这样做,但文档明确仅声明matches with literals will work in function heads。我相信正在努力使这项建设工作,但还没有。

在职能负责人的不同上下文中,围绕统一 VS 分配的问题与前几天提出的另一个关于 matching internal size values within binaries in function heads 的问题有关。

(请记住,函数头不只是进行赋值,它还试图有效地选择执行路径。所以这实际上不是一个简单的问题。)

综上所述,count/1 函数的更 Erlangish(更简单)的版本可能是:

count(Items) ->
    count(Items, #).

count([], A) ->
    A;
count([H | T], A) ->
   NewA = maps:update_with(H, fun(V) -> V + 1 end, 1, A),
   count(T, NewA).

stdlib 预见到了您所针对的情况,我们在 maps 模块中有一个很好的解决方案,称为 maps:update_with/4

请注意,我们没有为 count/2 命名一个新名称。除非在程序中需要,否则在进行显式递归时,通常更容易将具有不同数量的辅助函数命名为相同的事物。函数的标识是Name/Arity,因此无论标签是否相同,它们都是两个完全独立的函数。另外,请注意我们没有检查参数类型,因为我们在 count/2 中有一个显式匹配,只能匹配一个列表,因此无论如何都会抛出 bad_arg 异常。

有时你会想要 Erlang 中的多态参数,而类型检查是合适的。不过,您几乎从不需要在 Erlang 中编写防御性代码

与名为foo 的模块的会话:

1> c(foo).
ok,foo
2> foo:count([1,3,2,4,4,2,2,2,4,4,1,2]).
#1 => 2,2 => 5,3 => 1,4 => 4

但是

我们希望避免显式递归,除非有调用,因为我们在 stdlib 中放置了所有这些漂亮的列表功能抽象。你真正在做的是试图将一个值列表压缩成一个任意聚合的单个值,这就是定义一个折叠。因此,我们可以将上述内容改写为更惯用的方式:

count2(Items) ->
    Count = fun(I, A) -> maps:update_with(I, fun(V) -> V + 1 end, 1, A) end,
    lists:foldl(Count, #, Items).

我们得到:

3> foo:count2([1,3,2,4,4,2,2,2,4,4,1,2]).
#1 => 2,2 => 5,3 => 1,4 => 4

关于case...

我写的关于函数头中的统一的内容——对于函数头,因为它们是一个完全空白的统一上下文。理查德的回答提供了记住为什么这很疯狂的最佳简写:

f(A, #A := _)

等价于

f(A, #B := _) when B =:= A

那是行不通的。他与元组匹配的比较是正确的。

...但是...

case 已经分配了主要对象,这一切都很好。因为,正如 Richard 在评论中提到的那样,下面的 case 中只有一个 A

1> M = #1 => "one", 2 => "two".
#1 => "one",2 => "two"
2> F = 
2>   fun(A) ->
2>     case M of
2>       #A := B -> B;
2>       _         -> "Oh noes! Not a key!"
2>     end
2>   end.
#Fun<erl_eval.6.87737649>
3> F(1).
"one"
4> F(2).
"two"
5> F(3).
"Oh noes! Not a key!"

所以这可能感觉有点特殊,但基于匹配/统一的规则是有意义的。这意味着您可以像上面那样在函数内部使用case 编写do_count/2,但不能作为一组函数头。

【讨论】:

在case(可以这么说)的情况下,没有问题,因为fun head中的模式只包含A的一个实例,模式匹配并选择了正确的子句(在如果有多个子句)。然后对子句主体的评估以 A bound 进行。尝试case的子句时,A是已知实体,可以作为map模式中的key。【参考方案3】:

我自己制定了这条规则:在函数子句头部使用map时,不保证匹配顺序。因此,在您的示例中,您不能指望 [H|T] 匹配来为 H 提供值。

地图的一些功能看起来应该可以使用,Joe Armstrong 说它们应该可以使用,但实际上并没有。这是 erlang 的一个愚蠢的部分。在这里见证我的怀疑:https://bugs.erlang.org/browse/ERL-88

更简单的例子:

do_stuff(X, [X|Y]) ->
    io:format("~w~n", [Y]).

test() ->
    do_stuff(a, [a,b,c]).

4> c(x).
ok,x

5> x:test().
[b,c]
ok

但是:

-module(x).
-compile(export_all).

do_stuff(X, #X := Y) ->
    io:format("~w~n", [Y]).

test() ->
    do_stuff(a, #a => 3).


8> c(x).
x.erl:4: variable 'X' is unbound

【讨论】:

这不是关于地图的特性,而是关于模式匹配中变量绑定的语言规则。详情见我的回答。 @RichardC,谢谢!

以上是关于在 Erlang 中进行模式匹配映射时,为啥这个变量是未绑定的?的主要内容,如果未能解决你的问题,请参考以下文章

与 Erlang 中的映射模式匹配时的非法表达式

Erlang 映射中的模式匹配键

为啥 Erlang 中的模式匹配记录会抛出错误

Erlang 记录与映射组

为什么在erlang中对此字符串进行模式匹配会导致尾部的“字符串”和列表的ascii值?

在erlang中列出尾部模式匹配