Skip to content

Commit 297bd84

Browse files
committed
Type Map.update/4
1 parent e609d29 commit 297bd84

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

lib/elixir/lib/module/types/apply.ex

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ defmodule Module.Types.Apply do
256256
{Map, :put_new_lazy, [{[open_map(), term(), fun(0)], open_map()}]},
257257
{Map, :replace, [{[open_map(), term(), term()], open_map()}]},
258258
{Map, :replace_lazy, [{[open_map(), term(), fun(1)], open_map()}]},
259+
{Map, :update, [{[open_map(), term(), term(), fun(1)], open_map()}]},
259260
{Map, :update!, [{[open_map(), term(), fun(1)], open_map()}]},
260261
{:maps, :from_keys, [{[list(term()), term()], open_map()}]},
261262
{:maps, :find,
@@ -529,6 +530,42 @@ defmodule Module.Types.Apply do
529530
map_update_or_replace_lazy(:replace_lazy, args_types, stack, "do nothing")
530531
end
531532

533+
defp remote_apply(Map, :update, _info, [map, key, default, fun] = args_types, stack) do
534+
try do
535+
{map, default} =
536+
case default do
537+
%{dynamic: default} -> {dynamic(map), default}
538+
_ -> {map, default}
539+
end
540+
541+
{map, fun} =
542+
case fun do
543+
%{dynamic: fun} -> {dynamic(map), fun}
544+
_ -> {map, fun}
545+
end
546+
547+
fun_apply = fn optional?, arg_type ->
548+
if empty?(arg_type) do
549+
default
550+
else
551+
case fun_apply(fun, [arg_type]) do
552+
{:ok, res} -> if optional?, do: union(res, default), else: res
553+
reason -> throw({:badapply, reason, [arg_type]})
554+
end
555+
end
556+
end
557+
558+
map_update_fun(map, key, fun_apply, false, true)
559+
catch
560+
{:badapply, reason, args_types} ->
561+
{:error, {:badapply, fun, args_types, reason}}
562+
else
563+
{_value, descr, _errors} -> {:ok, return(descr, args_types, stack)}
564+
:badmap -> {:error, badremote(Map, :update, args_types)}
565+
{:error, _errors} -> {:ok, map}
566+
end
567+
end
568+
532569
defp remote_apply(Map, :update!, _info, args_types, stack) do
533570
map_update_or_replace_lazy(:update!, args_types, stack, "raise")
534571
end

lib/elixir/test/elixir/module/types/map_test.exs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,96 @@ defmodule Module.Types.MapTest do
11861186
end
11871187
end
11881188

1189+
describe "Map.update/4" do
1190+
test "checking" do
1191+
assert typecheck!(Map.update(%{}, :key, :default, fn _ -> :value end)) ==
1192+
dynamic(closed_map(key: atom([:default])))
1193+
1194+
assert typecheck!(Map.update(%{key: 123}, :key, :default, fn _ -> :value end)) ==
1195+
dynamic(closed_map(key: atom([:value])))
1196+
1197+
assert typecheck!([x], Map.update(x, :key, :default, fn _ -> :value end)) ==
1198+
dynamic(open_map(key: atom([:value, :default])))
1199+
1200+
# If one of them succeeds, we are still fine!
1201+
assert typecheck!(
1202+
[condition?],
1203+
Map.update(%{foo: 123}, if(condition?, do: :foo, else: :bar), :default, fn _ ->
1204+
"123"
1205+
end)
1206+
) ==
1207+
dynamic(
1208+
union(
1209+
closed_map(foo: binary()),
1210+
closed_map(foo: integer(), bar: atom([:default]))
1211+
)
1212+
)
1213+
1214+
# Both succeed but different clauses
1215+
assert typecheck!(
1216+
[condition?],
1217+
Map.update(
1218+
%{key1: :foo, key2: :bar},
1219+
if(condition?, do: :key1, else: :key2),
1220+
:default,
1221+
fn
1222+
:foo -> 123
1223+
:bar -> 123.0
1224+
end
1225+
)
1226+
) ==
1227+
dynamic(
1228+
union(
1229+
closed_map(key1: atom([:foo]), key2: float()),
1230+
closed_map(key1: integer(), key2: atom([:bar]))
1231+
)
1232+
)
1233+
1234+
assert typecheck!([x], Map.update(x, 123, :default, fn _ -> 456 end)) == dynamic(open_map())
1235+
1236+
integer_to_integer_float_atom =
1237+
dynamic(
1238+
closed_map([
1239+
{domain_key(:integer), integer() |> union(float()) |> union(atom([:default]))}
1240+
])
1241+
)
1242+
1243+
assert typecheck!([], Map.update(%{123 => 456}, 123, :default, fn x -> x * 1.0 end)) ==
1244+
integer_to_integer_float_atom
1245+
1246+
assert typecheck!([], Map.update(%{123 => 456}, 456, :default, fn x -> x * 1.0 end)) ==
1247+
integer_to_integer_float_atom
1248+
end
1249+
1250+
test "inference" do
1251+
assert typecheck!(
1252+
[x],
1253+
(
1254+
_ = Map.update(x, :key, :default, fn _ -> :value end)
1255+
x
1256+
)
1257+
) == dynamic(open_map())
1258+
end
1259+
1260+
test "errors" do
1261+
assert typeerror!(Map.update(%{key: :foo}, :key, :default, fn :bar -> :value end))
1262+
|> strip_ansi() =~
1263+
"""
1264+
incompatible types given on function call within Map.update/4:
1265+
1266+
Map.update(%{key: :foo}, :key, :default, fn :bar -> :value end)
1267+
1268+
given types:
1269+
1270+
dynamic(:foo)
1271+
1272+
but function has type:
1273+
1274+
(:bar -> dynamic(:value))
1275+
"""
1276+
end
1277+
end
1278+
11891279
describe "Map.update!/3" do
11901280
test "checking" do
11911281
assert typecheck!(Map.update!(%{key: 123}, :key, fn _ -> :value end)) ==
@@ -1219,6 +1309,9 @@ defmodule Module.Types.MapTest do
12191309

12201310
assert typecheck!([], Map.update!(%{123 => 456}, 123, fn x -> x * 1.0 end)) ==
12211311
dynamic(closed_map([{domain_key(:integer), union(integer(), float())}]))
1312+
1313+
assert typecheck!([], Map.update!(%{123 => 456}, 456, fn x -> x * 1.0 end)) ==
1314+
dynamic(closed_map([{domain_key(:integer), union(integer(), float())}]))
12221315
end
12231316

12241317
test "inference" do

0 commit comments

Comments
 (0)