当模式匹配映射到Erlang时,为什么这个变量没有绑定?

时间:2017-09-23 07:06:42

标签: erlang maps pattern-matching

-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)).

但似乎模式匹配应该才能工作,而且它没有。

3 个答案:

答案 0 :(得分:10)

答案与我最近给出的答案几乎相同: https://stackoverflow.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}}的参数一样。

答案 1 :(得分:3)

因为这种匹配在统一的上下文之外发生。事实上,虽然它没有在文档中明确禁止这一点,但文档明确仅表明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:update_with/4的漂亮解决方案。

请注意,我们没有将count/2命名为新名称。除非在程序中有必要,否则在执行显式递归时,通常更容易将具有不同arity的辅助函数命名为相同的东西。函数的标识是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}

<强> BUT

我们希望避免显式递归,除非有调用它,因为我们在stdlib中有所有这些漂亮的listy函数抽象。你真正在做的是尝试将值列表压缩成任意聚合的单值,根据定义,它是 fold 。因此,我们可以将上述内容重写为:

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 ...

我在函数头中写的关于统一的内容是 - for function head ,因为它们是一个完全空白的统一上下文。理查德的答案提供了记住这个疯狂原因的最佳简写:

f(A, #{A := _})

相当于

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

那就是不会飞。他与元组匹配的比较是现场的。

...但是...

在已分配主要对象的case 中,一切正常。因为,正如理查德在评论中提到的那样, A下面只有一个case

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!"

所以这可能会感觉有点特殊,但根据匹配/统一的规则是有道理的。并且意味着您可以使用函数内部的do_count/2以上面的方式编写case,而不是作为一组函数头。

答案 2 :(得分:-1)

我为自己制定了这条规则:在函数子句的头部使用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