Skip to content

Commit 12dabed

Browse files
author
José Valim
committed
Improve contracts for Dict
1. Functions like merge/2 and equal?/2 are only polymorphic in the Dict module. Implementations of the Dict module only need to care about equality and merging of the same types. 2. take/2, drop/2 and split/2 accept any Enum as argument.
1 parent 24da600 commit 12dabed

File tree

6 files changed

+88
-83
lines changed

6 files changed

+88
-83
lines changed

lib/elixir/lib/dict.ex

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ defmodule Dict do
3434
## Match
3535
3636
Dictionaries are required to implement all operations
37-
using the match (`===`) operator. Any deviation from
38-
this behaviour should be avoided and explicitly documented.
37+
using the match (`===`) operator.
3938
"""
4039

4140
use Behaviour
@@ -48,7 +47,7 @@ defmodule Dict do
4847
defcallback new(Enum.t) :: t
4948
defcallback new(Enum.t, (any -> { key, value })) :: t
5049
defcallback delete(t, key) :: t
51-
defcallback drop(t, [key]) :: t
50+
defcallback drop(t, Enum.t) :: t
5251
defcallback empty(t) :: t
5352
defcallback equal?(t, t) :: boolean
5453
defcallback get(t, key) :: value
@@ -63,9 +62,10 @@ defmodule Dict do
6362
defcallback pop(t, key, value) :: {value, t}
6463
defcallback put(t, key, value) :: t
6564
defcallback put_new(t, key, value) :: t
65+
defcallback reduce(t, Enumerable.acc, Enumerable.reducer) :: Enumerable.result
6666
defcallback size(t) :: non_neg_integer()
67-
defcallback split(t, [key]) :: {t, t}
68-
defcallback take(t, [key]) :: t
67+
defcallback split(t, Enum.t) :: {t, t}
68+
defcallback take(t, Enum.t) :: t
6969
defcallback to_list(t) :: list()
7070
defcallback update(t, key, value, (value -> value)) :: t
7171
defcallback update!(t, key, (value -> value)) :: t | no_return
@@ -258,12 +258,15 @@ defmodule Dict do
258258
end
259259

260260
@doc """
261-
Merges the given `enum` into `dict`. If one of the `enum` keys
262-
already exists in `dict`, the `dict` value is replaced by the `enum`
263-
value.
261+
Merges the dict `b` into dict `a`.
262+
263+
If one of the dict `b` entries already exists in the `dict`,
264+
the functions in entries in `b` have higher precedence unless a
265+
function is given to resolve conflicts.
264266
265-
The `enum` must yield tuples with two elements on enumeration,
266-
where the first element represents the key and the second the value.
267+
Notice this function is polymorphic as it can merge dicts of any
268+
type. Each dict implementation also provides a `merge` function,
269+
but they can only merge dicts of the same type.
267270
268271
## Examples
269272
@@ -273,22 +276,6 @@ defmodule Dict do
273276
...> [a: Dict.get(d, :a), b: Dict.get(d, :b), d: Dict.get(d, :d)]
274277
[a: 3, b: 2, d: 4]
275278
276-
"""
277-
@spec merge(t, t) :: t
278-
def merge(dict, enum) do
279-
merge(dict, enum, fn(_k, _v1, v2) -> v2 end)
280-
end
281-
282-
@doc """
283-
Merges the given `enum` into `dict`. If one of the `enum` entries
284-
already exists in `dict`, the given function is invoked to resolve
285-
the conflict.
286-
287-
The `enum` must yield tuples with two elements on enumeration,
288-
where the first element represents the key and the second the value.
289-
290-
## Examples
291-
292279
iex> d1 = dict_impl.new([a: 1, b: 2])
293280
...> d2 = dict_impl.new([a: 3, d: 4])
294281
...> d = Dict.merge(d1, d2, fn(_k, v1, v2) ->
@@ -299,8 +286,17 @@ defmodule Dict do
299286
300287
"""
301288
@spec merge(t, t, (key, value, value -> value)) :: t
302-
def merge(dict, enum, fun) do
303-
target(dict).merge(dict, enum, fun)
289+
def merge(a, b, fun // fn(_k, _v1, v2) -> v2 end) do
290+
a_target = target(a)
291+
b_target = target(b)
292+
293+
if a_target == b_target do
294+
a_target.merge(a, b, fun)
295+
else
296+
b_target.reduce(b, { :cont, a }, fn({ k, v }, acc) ->
297+
{ :cont, a_target.update(acc, k, v, fn(other) -> fun.(k, other, v) end) }
298+
end) |> elem(1)
299+
end
304300
end
305301

306302
@doc """
@@ -347,11 +343,6 @@ defmodule Dict do
347343
target(dict).update!(dict, key, fun)
348344
end
349345

350-
@doc false
351-
def update(dict, key, fun) do
352-
target(dict).update(dict, key, fun)
353-
end
354-
355346
@doc """
356347
Update a value in `dict` by calling `fun` on the value to get a new value. If
357348
`key` is not present in `dict` then `initial` will be stored as the first
@@ -423,8 +414,8 @@ defmodule Dict do
423414
end
424415

425416
@doc """
426-
Returns a new dict where only the keys in `keys` from `dict` are
427-
included.
417+
Returns a new dict where only the keys in `keys` from `dict` are included.
418+
428419
Any non-member keys are ignored.
429420
430421
## Examples
@@ -462,13 +453,16 @@ defmodule Dict do
462453
end
463454

464455
@doc """
465-
Check if two dicts are equal using `===`. If the dicts are
466-
of different types, they are first converted to lists.
456+
Check if two dicts are equal using `===`.
457+
458+
Notice this function is polymorphic as it can merge dicts of any
459+
type. Each dict implementation also provides an `equal?` function,
460+
but they can only compare dicts of the same type.
467461
468462
## Examples
469463
470464
iex> a = dict_impl.new(a: 2, b: 3, f: 5, c: 123)
471-
...> b = ListDict.new(a: 2, b: 3, f: 5, c: 123)
465+
...> b = [a: 2, b: 3, f: 5, c: 123]
472466
...> Dict.equal?(a, b)
473467
true
474468
@@ -488,7 +482,12 @@ defmodule Dict do
488482
a_target.equal?(a, b)
489483

490484
a_target.size(a) == b_target.size(b) ->
491-
ListDict.equal?(a_target.to_list(a), b_target.to_list(b))
485+
a_target.reduce(a, { :cont, true }, fn({ k, v }, _acc) ->
486+
case b_target.fetch(b, k) do
487+
{ :ok, ^v } -> { :cont, true }
488+
_ -> { :halt, false }
489+
end
490+
end) |> elem(1)
492491

493492
true ->
494493
false

lib/elixir/lib/dict/behaviour.ex

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,21 +88,16 @@ defmodule Dict.Behaviour do
8888
update(dict, key, value, fn(v) -> v end)
8989
end
9090

91-
def drop(dict, []), do: dict
92-
93-
def drop(dict, [key|keys]) do
94-
drop(delete(dict, key), keys)
91+
def drop(dict, keys) do
92+
Enum.reduce keys, dict, &delete(&2, &1)
9593
end
9694

9795
def take(dict, keys) do
98-
take(dict, keys, new)
99-
end
100-
101-
defp take(_dict, [], acc), do: acc
102-
defp take(dict, [key|keys], acc) do
103-
case fetch(dict, key) do
104-
{ :ok, value } -> take(dict, keys, put(acc, key, value))
105-
:error -> take(dict, keys, acc)
96+
Enum.reduce keys, new, fn key, acc ->
97+
case fetch(dict, key) do
98+
{ :ok, value } -> put(acc, key, value)
99+
:error -> acc
100+
end
106101
end
107102
end
108103

@@ -130,19 +125,18 @@ defmodule Dict.Behaviour do
130125
false -> false
131126
true ->
132127
reduce(dict1, { :cont, true }, fn({ k, v }, _acc) ->
133-
unless fetch(dict2, k) == { :ok, v } do
134-
{ :halt, false }
135-
else
136-
{ :cont, true }
128+
case fetch(dict2, k) do
129+
{ :ok, ^v } -> { :cont, true }
130+
_ -> { :halt, false }
137131
end
138132
end) |> elem(1)
139133
end
140134
end
141135

142-
def merge(dict, enumerable, callback // fn(_k, _v1, v2) -> v2 end) do
143-
Enum.reduce(enumerable, dict, fn({key, value}, acc) ->
144-
update(acc, key, value, fn(v1) -> callback.(key, v1, value) end)
145-
end)
136+
def merge(dict1, dict2, fun // fn(_k, _v1, v2) -> v2 end) do
137+
reduce(dict1, { :cont, dict2 }, fn { k, v1 }, acc ->
138+
{ :cont, update(acc, k, v1, &fun.(k, v1, &1)) }
139+
end) |> elem(1)
146140
end
147141

148142
defoverridable merge: 2, merge: 3, equal?: 2, to_list: 1, keys: 1,

lib/elixir/lib/hash_dict.ex

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -115,17 +115,11 @@ defmodule HashDict do
115115
end
116116

117117
def split(dict, keys) do
118-
split(keys, new, dict)
119-
end
120-
121-
defp split([], including, excluding) do
122-
{ including, excluding }
123-
end
124-
125-
defp split([key|keys], including, excluding) do
126-
case dict_delete(excluding, key) do
127-
{ excluding, value } -> split(keys, put(including, key, value), excluding)
128-
:error -> split(keys, including, excluding)
118+
Enum.reduce keys, { new, dict }, fn key, { inc, exc } = acc ->
119+
case dict_delete(exc, key) do
120+
{ exc, value } -> { put(inc, key, value), exc }
121+
:error -> acc
122+
end
129123
end
130124
end
131125

@@ -141,10 +135,6 @@ defmodule HashDict do
141135
end) |> elem(1)
142136
end
143137

144-
def merge(trie() = dict, enumerable, callback) do
145-
super(dict, enumerable, callback)
146-
end
147-
148138
## General helpers
149139

150140
defp dict_delete(trie(root: root, size: size), key) do

lib/elixir/lib/keyword.ex

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ defmodule Keyword do
324324
325325
"""
326326
@spec equal?(t, t) :: boolean
327-
def equal?(left, right) do
327+
def equal?(left, right) when is_list(left) and is_list(right) do
328328
:lists.sort(left) == :lists.sort(right)
329329
end
330330

@@ -339,7 +339,7 @@ defmodule Keyword do
339339
340340
"""
341341
@spec merge(t, t) :: t
342-
def merge(d1, d2) do
342+
def merge(d1, d2) when is_list(d1) and is_list(d2) do
343343
d2 ++ lc({ k, _ } = tuple inlist d1, not has_key?(d2, k), do: tuple)
344344
end
345345

@@ -356,7 +356,7 @@ defmodule Keyword do
356356
357357
"""
358358
@spec merge(t, t, (key, value, value -> value)) :: t
359-
def merge(d1, d2, fun) do
359+
def merge(d1, d2, fun) when is_list(d1) and is_list(d2) do
360360
do_merge(d2, d1, fun)
361361
end
362362

@@ -456,7 +456,7 @@ defmodule Keyword do
456456
acc = { [], [] }
457457

458458
{ take, drop } = Enum.reduce dict, acc, fn({ k, v }, { take, drop }) ->
459-
case :lists.member(k, keys) do
459+
case k in keys do
460460
true -> { [{k, v}|take], drop }
461461
false -> { take, [{k, v}|drop] }
462462
end
@@ -481,7 +481,7 @@ defmodule Keyword do
481481
482482
"""
483483
def take(dict, keys) do
484-
lc { k, _ } = tuple inlist dict, :lists.member(k, keys), do: tuple
484+
lc { k, _ } = tuple inlist dict, k in keys, do: tuple
485485
end
486486

487487
@doc """
@@ -500,7 +500,7 @@ defmodule Keyword do
500500
501501
"""
502502
def drop(dict, keys) do
503-
lc { k, _ } = tuple inlist dict, not :lists.member(k, keys), do: tuple
503+
lc { k, _ } = tuple inlist dict, not k in keys, do: tuple
504504
end
505505

506506
@doc """

lib/elixir/lib/list_dict.ex

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ defmodule ListDict do
9494
acc = { [], [] }
9595

9696
{take, drop} = Enum.reduce dict, acc, fn({ k, v }, { take, drop }) ->
97-
if :lists.member(k, keys) do
97+
if k in keys do
9898
{ [{k, v}|take], drop }
9999
else
100100
{ take, [{k, v}|drop] }
@@ -105,11 +105,11 @@ defmodule ListDict do
105105
end
106106

107107
def take(dict, keys) do
108-
lc { k, _ } = tuple inlist dict, :lists.member(k, keys), do: tuple
108+
lc { k, _ } = tuple inlist dict, k in keys, do: tuple
109109
end
110110

111111
def drop(dict, keys) do
112-
lc { k, _ } = tuple inlist dict, not :lists.member(k, keys), do: tuple
112+
lc { k, _ } = tuple inlist dict, not k in keys, do: tuple
113113
end
114114

115115
def update!([{key, value}|dict], key, fun) do
@@ -142,5 +142,10 @@ defmodule ListDict do
142142
:lists.keysort(1, dict) === :lists.keysort(1, other)
143143
end
144144

145+
def reduce(_, { :halt, acc }, _fun), do: { :halted, acc }
146+
def reduce(list, { :suspend, acc }, fun), do: { :suspended, acc, &reduce(list, &1, fun) }
147+
def reduce([], { :cont, acc }, _fun), do: { :done, acc }
148+
def reduce([{_,_}=h|t], { :cont, acc }, fun), do: reduce(t, fun.(h, acc), fun)
149+
145150
def to_list(dict), do: dict
146151
end

lib/elixir/test/elixir/dict_test.exs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,13 @@ defmodule DictTest.Common do
263263
assert drop == dict
264264
end
265265

266+
test "split/2 with enum" do
267+
dict = int_dict()
268+
{ take, drop } = Dict.split(dict, 1..3)
269+
assert take == dict
270+
assert drop == new_dict([])
271+
end
272+
266273
test "take/2" do
267274
dict = new_dict()
268275
take = Dict.take(dict, ["unknown_key"])
@@ -278,6 +285,11 @@ defmodule DictTest.Common do
278285
assert Dict.take(dict, [1.0]) == new_dict([])
279286
end
280287

288+
test "take/2 with enum" do
289+
dict = int_dict()
290+
assert Dict.take(dict, 1..3) == dict
291+
end
292+
281293
test "drop/2" do
282294
dict = new_dict()
283295
drop = Dict.drop(dict, ["unknown_key"])
@@ -293,6 +305,11 @@ defmodule DictTest.Common do
293305
assert Dict.drop(dict, [1.0]) == dict
294306
end
295307

308+
test "drop/2 with enum" do
309+
dict = int_dict()
310+
assert Dict.drop(dict, 1..3) == new_dict([])
311+
end
312+
296313
test "empty" do
297314
assert Dict.empty(new_dict) == new_dict([])
298315
end

0 commit comments

Comments
 (0)